@font-face {
  font-family: "Aero";
  /* WOFF2 primero: 13,9 KB frente a los 43,4 KB del TrueType. */
  src: url("./tipografias/Aero.woff2") format("woff2"),
       url("./tipografias/Aero.ttf") format("truetype");
  font-display: swap;
}

@font-face {
  font-family: "Bebas Neue";
  src: url("./tipografias/BebasNeue-Regular.woff2") format("woff2");
  font-display: swap;
}

/* ---------------------------------------------------------------------------
   Fuentes de reserva con métricas ajustadas.

   Con `font-display: swap` el navegador pinta primero con una fuente del
   sistema y sustituye al llegar la real. Si las dos no ocupan lo mismo, el
   texto se recompone y arrastra todo lo que tiene debajo: era el origen del
   desplazamiento de maquetación medido (CLS 0,127 en móvil).

   Los porcentajes están medidos, no estimados: se comparó el ancho de una
   misma cadena renderizada con cada familia, y el alto se tomó de las
   métricas del fichero de fuente.
--------------------------------------------------------------------------- */

@font-face {
  font-family: "Bebas Fallback";
  src: local("Arial Narrow"), local("Helvetica Neue Condensed"), local("Impact");
  size-adjust: 75.3%;      /* 1496 px reales / 1987 px de Arial Narrow */
  ascent-override: 90%;
  descent-override: 20%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Aero Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
  size-adjust: 100.6%;     /* 2426 px reales / 2411 px de Arial */
  ascent-override: 69%;    /* Aero: ascent 690 / unitsPerEm 1000 */
  descent-override: 13.8%; /* Aero: descent 138 / unitsPerEm 1000 */
  line-gap-override: 0%;
}

:root {
  --black: #0b0c0e;
  --navy: #061724;
  --panel: rgba(0, 8, 15, 0.76);
  --panel-strong: rgba(2, 16, 28, 0.92);
  --blue: #15506d;
  --blue-light: #36a4de;
  /* --- Estado de interacción ---------------------------------------------
     Era cian. Dejó de serlo al encender el botón en azul: `#22d3ff` está a
     192° y el cian estaba a 198°, así que eran el mismo color intentando
     significar dos cosas —«pulsa aquí» y «estás aquí»—. El estado pasa a
     blanco, que no compite con nada y sube el foco de 10,56:1 a 19,56:1. */
  --estado: #f5fbff;
  --white: #f5fbff;
  --muted: rgba(245, 251, 255, 0.78);
  /* --- La acción, encendida -------------------------------------------
     El núcleo va MÁS CLARO que el halo, y no es un descuido: un tubo de neón
     encendido se sobreexpone al blanco por dentro y el color aparece
     alrededor. Pintar el botón del color del halo da un botón de color, no un
     botón encendido. Medido: a saturación máxima no se puede subir el brillo
     sin que el tono se vaya del azul (198°) al aqua (186°) — el brillo tiene
     que salir del halo, no del relleno.

     Por qué hay halo, cuando en julio se retiró de toda la interfaz con el
     argumento de que «un halo alrededor del botón que gana dinero es el
     recurso más barato que existe»: ROT3X fabrica rótulos de neón. Aquí el
     halo no decora el botón, enseña el producto. Es la única excepción y
     queda declarada.

     NO se anima nunca —ni en hover ni al pulsar—: animar `box-shadow` obliga
     a repintar el elemento en cada frame. En hover cambia solo el relleno; al
     pulsar, solo el `transform`. Medido: 0,04 ms por frame con 60
     desplazamientos forzados sobre la cabecera fija, que es el peor caso.

     Texto negro sobre el relleno: 11,49:1. */
  /* --- Alto de la cabecera ---------------------------------------------
     Se declara aquí porque nunca se había declarado: solo existía como
     respaldo dentro de cada `var()`, y con cuatro valores distintos —78, 110,
     68 y 92 px— ninguno de ellos correcto. Como `body` reserva ese hueco con
     `padding-top`, el navegador pintaba la página con el hueco equivocado y
     `script.js` lo corregía al cargar: TODO el documento daba un salto.
     Es el desplazamiento de maquetación que quedaba en el sitio.

     Los valores están medidos sobre la cabecera real, no estimados, y los
     puntos de ruptura son donde de verdad cambia de alto. `script.js` sigue
     ajustándolo en caliente para casos límite; ahora coincide con el CSS y no
     hay salto que corregir. */
  --header-height: 89px;

  /* Retardo base de la entrada del héroe. Vale 0 salvo cuando corre el
     encendido de marca, que lo desplaza para que las dos cosas se sucedan en
     lugar de pisarse. */
  --hero-espera: 0ms;

  --accion:      #22d3ff;
  --accion-alta: #63e2ff;   /* hover y foco */
  --halo:
    0 0  1px rgba(190, 245, 255, 0.90),   /* filo encendido */
    0 0 12px rgba(0, 200, 255, 0.62),     /* halo cerrado   */
    0 0 32px rgba(0, 170, 255, 0.40),     /* halo medio     */
    0 0 68px rgba(0, 140, 255, 0.22);     /* derrame        */
  --green: #05d968;
  --radius: 0;
  --ui-font: "Bebas Neue", "Bebas Fallback", "Arial Narrow", "Bahnschrift Condensed", Impact, sans-serif;
  --aero-font: "Aero", "Aero Fallback", "Bebas Neue", Impact, sans-serif;
  --body-font: Arial, Helvetica, sans-serif;

  /* HubSpot Forms: el nuevo editor toma estos valores desde la raíz. */
  --hsf-global__color: #ffffff;
  --hsf-richtext__color: #ffffff;
  --hsf-heading__color: #ffffff;
  --hsf-background__background-color: #173a52;
  --hsf-background__border-color: #65c6ee;
  --hsf-background__border-style: solid;
  --hsf-background__border-width: 0;
  --hsf-field-label__color: #ffffff;
  --hsf-field-label-requiredindicator__color: #ff835f;
  --hsf-field-description__color: #ffffff;
  --hsf-field-footer__color: #ffffff;
  --hsf-field-checkbox__color: #ffffff;
  --hsf-field-radio__color: #ffffff;
  --hsf-infoalert__color: #ffffff;
  --hsf-erroralert__color: #ffb39f;
  --hsf-field-input__color: #071a2a;
  --hsf-field-input__background-color: #f5f8fa;
  --hsf-field-input__placeholder-color: #52677a;
  --hsf-field-input__border-color: #65c6ee;
  --hsf-field-textarea__color: #071a2a;
  --hsf-field-textarea__background-color: #f5f8fa;
  --hsf-field-textarea__placeholder-color: #52677a;
  --hsf-field-textarea__border-color: #65c6ee;
}

/* Con el encendido en marcha, el héroe espera: así el titular entra JUSTO
   cuando cae el telón, en vez de haberse animado a escondidas detrás.
   Va en su propia regla, DESPUÉS de `:root`. Declararla partiendo el bloque
   de tokens dejaba todo lo que venía detrás —el acento, el halo, el estado,
   las superficies, los filetes— dentro de esta regla, así que solo existían
   mientras el encendido estaba en pantalla. */
html.con-encendido { --hero-espera: 760ms; }

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

html {
  min-height: 100%;
  scroll-behavior: smooth;
  /* `clip`, no `hidden`. `overflow-x: hidden` obliga al navegador a tratar
     este elemento como contenedor de scroll, y eso ROMPE `position: sticky`
     en todo lo que tenga dentro: la barra de anclas se iba con el scroll en
     lugar de quedarse. `clip` recorta igual pero no crea contenedor. */
  overflow-x: clip;
}

body {
  margin: 0;
  min-height: 100%;
  padding-top: var(--header-height);
  color: var(--white);
  background: var(--black);
  font-family: var(--ui-font);
  letter-spacing: 0.08em;
  /* `clip`, no `hidden`. `overflow-x: hidden` obliga al navegador a tratar
     este elemento como contenedor de scroll, y eso ROMPE `position: sticky`
     en todo lo que tenga dentro: la barra de anclas se iba con el scroll en
     lugar de quedarse. `clip` recorta igual pero no crea contenedor. */
  overflow-x: clip;
}

body::before,
body::after {
  content: "";
  position: fixed;
  z-index: -1;
  top: 0;
  width: min(42vw, 330px);
  height: min(72vh, 680px);
  opacity: 0.34;
  pointer-events: none;
  background: url("./assets/silueta-lateral.svg") top left / contain no-repeat;
}

body::before {
  left: 0;
}

body::after {
  right: 0;
  transform: scaleX(-1);
}

a {
  color: inherit;
  text-decoration: none;
}

img,
svg {
  max-width: 100%;
  height: auto;
}

img {
  display: block;
}

button,
input,
select,
textarea {
  font: inherit;
}

input,
select,
textarea {
  letter-spacing: 0.02em;
}

.site-shell {
  min-height: 100vh;
  /* `clip`, no `hidden`. `overflow-x: hidden` obliga al navegador a tratar
     este elemento como contenedor de scroll, y eso ROMPE `position: sticky`
     en todo lo que tenga dentro: la barra de anclas se iba con el scroll en
     lugar de quedarse. `clip` recorta igual pero no crea contenedor. */
  overflow-x: clip;
  background:
    linear-gradient(180deg, rgba(6, 6, 8, 0.3), rgba(6, 6, 8, 0.92) 72%),
    url("./assets/fondo-web.svg") center top / cover fixed no-repeat;
}

.site-header {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 1000;
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  align-items: center;
  gap: clamp(12px, 2vw, 28px);
  width: 100%;
  min-height: 78px;
  padding: 12px 14px;
  background: linear-gradient(180deg, rgba(2, 5, 11, 0.96), rgba(5, 25, 39, 0.86));
  border-bottom: 1px solid var(--line-mid);
  backdrop-filter: blur(10px);
}

.brand-link {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  min-width: 0;
}

.brand-link img {
  width: min(116px, 30vw);
  max-width: 164px;
  height: auto;
}

.desktop-nav {
  display: none;
}

.site-nav {
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  overflow: visible;
  scrollbar-width: none;
  justify-self: stretch;
  padding-bottom: 2px;
}

.site-nav::-webkit-scrollbar {
  display: none;
}

.site-nav a,
.header-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 8px 10px;
  color: var(--white);
  text-transform: uppercase;
  font-size: 16px;
  line-height: 1;
  white-space: nowrap;
}

.site-nav a.is-active {
  color: var(--estado);
}

/* K10 · Aquí vivían `background: var(--green)` y una sombra verde de la
   paleta anterior. Comprobado en el navegador: las dos las pisa el bloque de
   la acción encendida, así que el botón ya se pintaba cian con su halo. Se
   retiran porque describían un botón que no existe. */
.header-cta {
  flex: 0 0 auto;
  max-width: 276px;
  border-radius: 0;
  text-align: center;
}

main {
  overflow-x: clip;
  overflow-y: visible;
}

main [id] {
  scroll-margin-top: calc(var(--header-height) + 16px);
}

/* `.section-pad` toma su relleno de la regla unificada del ritmo vertical,
   más abajo, junto a `.section` y `.page-hero`. */

.eyebrow {
  margin: 0 0 12px;
  color: var(--estado);
  font-family: var(--aero-font);
  font-size: 14px;
  line-height: 1.2;
  text-transform: uppercase;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1,
h2,
h3 {
  text-transform: uppercase;
}

h1 {
  margin-bottom: 16px;
  font-family: var(--aero-font);
  font-size: clamp(30px, 9vw, 56px);
  line-height: 0.98;
  letter-spacing: 0.02em;
  max-width: 100%;
}

h2 {
  margin-bottom: 12px;
  font-family: var(--aero-font);
  font-size: clamp(26px, 7.2vw, 42px);
  line-height: 1.02;
  letter-spacing: 0.02em;
}

h3 {
  margin-bottom: 8px;
  font-size: clamp(22px, 5.6vw, 31px);
  line-height: 1.02;
}

p,
li {
  font-family: var(--body-font);
  letter-spacing: 0;
}

p {
  color: var(--muted);
  font-size: 16px;
  line-height: 1.48;
}

.hero-copy {
  min-width: 0;
}

.hero-lead {
  margin-bottom: 18px;
  color: var(--white);
  font-size: 18px;
}

.hero-services {
  display: grid;
  gap: 9px;
  margin-bottom: 20px;
}

.hero-services a,
.local-tags span,
.service-tag {
  display: inline-flex;
  align-items: center;
  width: fit-content;
  min-height: 32px;
  padding: 7px 11px;
  color: var(--white);
  font-family: var(--ui-font);
  letter-spacing: 0.08em;
  line-height: 1;
  text-transform: uppercase;
  background: rgba(54, 164, 222, 0.14);
  border: 1px solid var(--line-strong);
}

.cta-row {
  display: grid;
  gap: 10px;
  margin: 20px 0;
}

.btn {
  min-height: 50px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 13px 18px;
  border: 0;
  cursor: pointer;
  font-family: var(--ui-font);
  font-size: 20px;
  letter-spacing: 0.08em;
  line-height: 1;
  text-transform: uppercase;
  text-align: center;
}

/* Relleno plano, sin degradado: un degradado de 135 grados sobre un botón
   sólido de 50 px no se percibe como profundidad, se percibe como un botón de
   2012. El halo sí se queda, y el porqué está junto a `--halo` en `:root`. */
.btn-primary {
  color: var(--black);
  background: var(--accion);
  box-shadow: var(--halo);
}

/* El secundario no tiene fondo. Antes llevaba un relleno azul al 12 % y el
   mismo alto, tamaño de letra y ancho aproximado que el primario: en la
   portada, «PEDIR PRESUPUESTO POR WHATSAPP» y «VER TRABAJOS E IDEAS» no eran
   primario y secundario, eran un empate. Dos cajas del mismo peso no son una
   jerarquía. Sin fondo, el naranja es lo único que pide algo en pantalla.
   El tinte azul se retira además por lo mismo que los bordes: era otra dosis
   de cian disuelto. */
.btn-ghost {
  color: var(--white);
  background: transparent;
  border: 1px solid var(--line-strong);
}

.trust-strip {
  display: grid;
  gap: 7px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.trust-strip li {
  position: relative;
  padding-left: 18px;
  color: var(--white);
  font-size: 14px;
}

.trust-strip li::before {
  content: "";
  position: absolute;
  top: 0.62em;
  left: 0;
  width: 7px;
  height: 7px;
  /* Una viñeta no es un estado de interacción: es puntuación. En cian
     competía con el foco y con la navegación activa, que sí lo son. */
  background: rgba(245, 251, 255, 0.76);
  transform: translateY(-50%) rotate(45deg);
}

.bevel-card,
.service-card,
.local-card,
.lead-form,
.proof-grid article,
.process-list li,
.faq-list details,
.service-detail-card,
.image-requirements {
  position: relative;
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.16), rgba(1, 10, 18, 0.78)),
    var(--panel);
  border: 1px solid var(--line-mid);
  border-radius: 0;
}

.hero-visual {
  margin: 0;
  min-width: 0;
  max-width: 100%;
  justify-self: stretch;
}

.bevel-card img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.bevel-card figcaption {
  padding: 10px 14px 14px;
  color: var(--estado);
  font-size: 16px;
  text-transform: uppercase;
}

.section-heading {
  max-width: 820px;
  margin-bottom: 24px;
}

.priority,
.work-preview,
.process,
.faq {
  background: rgba(0, 6, 12, 0.64);
}

.priority-grid,
.proof-grid,
.gallery-grid,
.process-list,
.faq-list,
.service-detail-grid {
  display: grid;
  gap: 16px;
}

.service-card {
  display: grid;
  gap: 16px;
  padding: 14px;
}

/* K7 · Aquí había una segunda declaración de `.service-card img` con
   `aspect-ratio: 16/10`. La ganaban por cascada las dos posteriores —cuadrado
   fuera de la galería, 3:2 dentro de `.priority-grid`—, así que no pintaba
   nada salvo engañar a quien leyera la hoja de arriba abajo. Solo se conserva
   el ancho, que sí es suyo. */
.service-card img {
  width: 100%;
}

.service-card ul,
.service-detail-card ul {
  display: grid;
  gap: 7px;
  margin: 12px 0;
  padding-left: 19px;
}

.service-card li,
.service-detail-card li {
  color: var(--white);
  font-size: 16px;
}

.service-tag {
  margin: 0 0 12px;
  color: var(--estado);
  font-size: 14px;
}

.text-link {
  display: inline-flex;
  margin-top: 8px;
  color: var(--estado);
  font-size: 20px;
  text-transform: uppercase;
}

.conversion-band {
  background:
    linear-gradient(90deg, rgba(0, 0, 0, 0.18), transparent 20% 80%, rgba(0, 0, 0, 0.18)),
    rgba(2, 21, 34, 0.82);
}

.proof-grid article,
.process-list li,
.faq-list details,
.service-detail-card,
.image-requirements {
  padding: 20px;
}

/* Los ordinales 01/02/03 son metadato, no énfasis: informan del orden de
   lectura, no piden nada. En naranja competían con el botón que sí pide algo,
   y el usuario aprendía que naranja significa a la vez «habla con ROT3X»,
   «estás aquí» y «paso uno». El naranja identifica UNA cosa: la acción que
   genera contacto.

   Al 50 % de blanco y no al 32 % que pedía la dirección de arte: 32 % da
   2,67:1 en el peor fondo del sitio y el ordinal es texto: incumpliría el
   3:1 que exige la norma para texto grande. Al 50 % son 5,00:1 y recula
   igual. */
.proof-grid strong,
.process-list span {
  display: inline-flex;
  margin-bottom: 12px;
  color: rgba(245, 251, 255, 0.5);
  font-size: 28px;
  line-height: 1;
}

.gallery-grid figure {
  margin: 0;
}

.gallery-grid.wide {
  grid-template-columns: 1fr;
}

.media-card img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

.media-card figcaption {
  padding: 10px 14px 14px;
  color: var(--estado);
  font-size: 16px;
  text-transform: uppercase;
}

/* `.hero-mosaic*` y `.construction-card` se retiran: ninguno aparece en las 19
   páginas ni en `script.js`. El mosaico se quedó atrás cuando el héroe se
   rehízo como muro; la tarjeta de obras nunca llegó a usarse. Eran 13 reglas
   que el navegador parseaba en cada carga sin poder aplicar nunca. */

.local-card {
  padding: 24px;
}

.local-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  margin-top: 18px;
}

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

.quote {
  display: grid;
  gap: 22px;
  align-items: start;
  background: linear-gradient(180deg, rgba(26, 26, 29, 0.88), rgba(0, 6, 12, 0.86));
}

.lead-stack {
  display: grid;
  gap: 14px;
}

.lead-advice {
  padding: 18px;
  background:
    linear-gradient(135deg, rgba(229, 139, 8, 0.15), rgba(54, 164, 222, 0.1)),
    rgba(1, 10, 18, 0.82);
  border: 1px solid var(--line-mid);
}

.lead-advice strong {
  display: block;
  margin-bottom: 6px;
  color: var(--estado);
  font-size: 24px;
  line-height: 1;
  text-transform: uppercase;
}

.lead-advice p {
  margin: 0;
}

.lead-form {
  display: grid;
  gap: 14px;
  padding: 18px;
}

.lead-form label {
  display: grid;
  gap: 6px;
  color: var(--white);
  font-size: 18px;
  text-transform: uppercase;
}

.lead-form input,
.lead-form select,
.lead-form textarea {
  width: 100%;
  min-height: 46px;
  padding: 12px 13px;
  color: var(--white);
  font-family: var(--body-font);
  font-size: 16px;
  background: rgba(54, 164, 222, 0.16);
  border: 1px solid var(--line-strong);
  border-radius: 0;
  outline: 0;
}

.lead-form textarea {
  min-height: 128px;
  resize: vertical;
}

.lead-form .btn {
  width: 100%;
}

.lead-form input:focus,
.lead-form select:focus,
.lead-form textarea:focus {
  border-color: var(--estado);
  box-shadow: 0 0 0 3px var(--line-hair);
}

.form-note,
.lead-help {
  margin: 0;
  font-size: 14px;
}

.lead-help {
  color: var(--white);
}

.form-note a {
  color: var(--estado);
}

.faq-list summary {
  cursor: pointer;
  font-size: 20px;
  text-transform: uppercase;
}

.faq-list p {
  margin: 12px 0 0;
}

.site-footer {
  display: grid;
  gap: 24px;
  padding: 44px 18px 92px;
  background: linear-gradient(180deg, rgba(9, 36, 55, 0.92), rgba(1, 4, 12, 0.98));
  border-top: 1px solid var(--line-mid);
}

.site-footer > div > img {
  width: 180px;
  margin-bottom: 12px;
}

.site-footer nav,
.footer-contact {
  display: grid;
  gap: 10px;
}

.site-footer a {
  color: var(--white);
  font-size: 18px;
  text-transform: uppercase;
}

.cookie-settings-link {
  width: fit-content;
  padding: 0;
  color: var(--white);
  font: inherit;
  font-size: 18px;
  text-align: left;
  text-transform: uppercase;
  background: transparent;
  border: 0;
  border-radius: 0;
  cursor: pointer;
}

.cookie-settings-link:hover,
.cookie-settings-link:focus-visible {
  color: var(--estado);
}

.social-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

.footer-contact .social-links,
.contact-social {
  margin-top: 4px;
}

.social-link {
  width: 42px;
  height: 42px;
  display: inline-grid;
  place-items: center;
  color: var(--estado);
  font-size: 0;
  text-transform: none;
  background: rgba(54, 164, 222, 0.1);
  border: 1px solid var(--line-strong);
  box-shadow: 0 0 14px var(--line-hair);
}

.social-link:hover,
.social-link:focus-visible {
  color: var(--white);
  background: var(--line-hair);
  border-color: var(--line-field);
  outline: 0;
  box-shadow: 0 0 0 3px var(--line-hair);
}

.social-icon {
  width: 22px;
  height: 22px;
  display: block;
  background: currentColor;
  mask: center / contain no-repeat;
  -webkit-mask: center / contain no-repeat;
}

.social-icon-image {
  width: 24px;
  height: 24px;
  display: block;
  object-fit: contain;
}

.social-link--instagram .social-icon {
  mask-image: url("./assets/icons/social/instagram.svg");
  -webkit-mask-image: url("./assets/icons/social/instagram.svg");
}

.social-link--facebook .social-icon {
  mask-image: url("./assets/icons/social/facebook.svg");
  -webkit-mask-image: url("./assets/icons/social/facebook.svg");
}

.social-link--youtube .social-icon {
  mask-image: url("./assets/icons/social/youtube.svg");
  -webkit-mask-image: url("./assets/icons/social/youtube.svg");
}

.social-link--tiktok .social-icon {
  mask-image: url("./assets/icons/social/tiktok.svg");
  -webkit-mask-image: url("./assets/icons/social/tiktok.svg");
}

.social-link--linkedin .social-icon {
  mask-image: url("./assets/icons/social/linkedin.svg");
  -webkit-mask-image: url("./assets/icons/social/linkedin.svg");
}

.footer-address {
  color: var(--muted);
  font-family: var(--body-font);
  font-size: 14px;
  line-height: 1.35;
  letter-spacing: 0;
  text-transform: none;
}

.footer-map {
  display: grid;
  gap: 6px;
  color: var(--estado);
  font-size: 16px;
}

.footer-map img {
  width: min(100%, 320px);
  aspect-ratio: 16 / 9;
  object-fit: cover;
  margin: 0;
  border: 1px solid var(--line-mid);
}

.mobile-leadbar {
  position: fixed;
  z-index: 30;
  inset: auto 0 0;
  display: none;
  grid-template-columns: 0.72fr 1.28fr;
  background: rgba(2, 5, 11, 0.94);
  border-top: 1px solid var(--line-mid);
  box-shadow: 0 -10px 28px rgba(0, 0, 0, 0.34);
}

.mobile-leadbar a {
  min-height: 64px;
  display: grid;
  place-items: center;
  padding: 8px;
  color: var(--white);
  font-size: 20px;
  line-height: 0.96;
  text-align: center;
  text-transform: uppercase;
}

.mobile-leadbar a:last-child {
  color: var(--black);
  background: var(--green);
  font-size: 20px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
}

.service-hero {
  display: grid;
  gap: 24px;
}

.service-hero .bevel-card {
  margin: 0;
}

.service-intro {
  background: rgba(0, 6, 12, 0.64);
}

.service-detail-card h3 {
  color: var(--estado);
}

.breadcrumbs {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 14px;
  color: var(--muted);
  font-family: var(--body-font);
  font-size: 14px;
  letter-spacing: 0;
}

.breadcrumbs a {
  color: var(--estado);
}

.image-requirements ul {
  margin: 12px 0 0;
  padding-left: 20px;
}

@media (min-width: 720px) {
  .section-pad {
    padding: 70px clamp(28px, 5vw, 64px);
  }

  /* `.hero` salió de esta lista: por debajo de 1000 px es `display: flex` y
     una plantilla de columnas ahí no pinta nada. */
  .quote,
  .service-hero {
    grid-template-columns: minmax(0, 1.05fr) minmax(280px, 0.95fr);
  }

  .cta-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .priority-grid,
  .proof-grid,
  .gallery-grid,
  .gallery-grid.wide,
  .process-list,
  .service-detail-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .site-footer {
    grid-template-columns: 1.1fr 0.9fr 0.9fr;
    padding-bottom: 46px;
  }

  .mobile-leadbar {
    display: none;
  }
}

@media (max-width: 719px) {
  .site-header {
    grid-template-columns: 1fr;
    gap: 6px;
    min-height: 0;
    padding: 10px 14px 9px;
  }

  .header-cta {
    display: none;
  }

  .site-nav {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: repeat(4, max-content);
    justify-content: center;
    gap: 4px 7px;
    overflow-x: visible;
  }

  .site-nav a {
    flex: 0 0 auto;
    min-height: 30px;
    font-size: 14px;
    padding: 5px 3px;
  }

  .hero.section-pad {
    padding-top: 22px;
    padding-bottom: 34px;
  }

  .hero-copy,
  .quote-copy,
  .section-heading,
  .service-hero > div {
    max-width: 100%;
    min-width: 0;
  }

  .hero-lead {
    max-width: none;
    margin-bottom: 14px;
    font-size: 16px;
    line-height: 1.38;
  }

  .cta-row {
    gap: 9px;
    margin: 14px 0;
  }

  .btn {
    min-height: 48px;
    padding: 12px 14px;
    font-size: 20px;
  }

  h1 {
    max-width: 100%;
    margin-bottom: 12px;
    font-size: clamp(27px, 8.1vw, 34px);
    line-height: 1.02;
  }

  .trust-strip {
    gap: 5px;
  }

  .trust-strip li {
    font-size: 14px;
  }

  .hero-visual {
    margin-top: 6px;
  }

  .mobile-leadbar {
    display: grid;
  }
}

@media (min-width: 1100px) {
  .site-header {
    min-height: 92px;
    padding: 16px clamp(36px, 6vw, 88px);
  }

  .brand-link img {
    width: 164px;
  }

  .site-nav {
    justify-content: center;
    gap: 18px;
  }

  /* `.header-cta` salió de aquí: su propio bloque de este mismo punto de
     ruptura declara `font-size` más abajo y siempre gana. */
  .site-nav a {
    font-size: 20px;
  }

  .hero-copy,
  .section-heading,
  .quote-copy {
    max-width: 760px;
  }

  .hero-lead {
    font-size: 20px;
  }

  .hero-services {
    grid-template-columns: repeat(3, max-content);
  }

  .service-card {
    padding: 18px;
  }
}

@media (max-width: 420px) {
  .brand-link img {
    width: 104px;
  }

  .site-nav {
    order: 3;
  }

  .section-pad {
    padding-inline: 16px;
  }
}

/* Menú completo y páginas SEO: conserva el lenguaje visual original de ROT3X. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

body.menu-open {
  overflow: hidden;
}

.menu-toggle {
  display: inline-grid;
  width: 50px;
  height: 50px;
  place-content: center;
  justify-self: end;
  padding: 10px;
  color: var(--white);
  background: rgba(54, 164, 222, 0.08);
  border: 1px solid var(--line-strong);
  border-radius: 0;
  cursor: pointer;
}

.menu-toggle span:not(.sr-only) {
  display: block;
  width: 26px;
  height: 2px;
  margin: 3px 0;
  background: currentColor;
  transform-origin: center;
  transition: opacity 180ms ease, transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
}

.menu-toggle[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(8px) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] span:nth-child(2) {
  opacity: 0;
}

.menu-toggle[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(-8px) rotate(-45deg);
}

.nav-drawer {
  position: fixed;
  z-index: 999;
  inset: var(--header-height) 0 0;
  display: none;
  overflow-y: auto;
  padding: 22px 18px 48px;
  background:
    linear-gradient(180deg, rgba(6, 6, 8, 0.98), rgba(14, 14, 17, 0.97)),
    url("./assets/fondo-web.svg") center top / cover fixed no-repeat;
  clip-path: inset(0 0 100% 100%);
  opacity: 0;
  overscroll-behavior: contain;
  pointer-events: none;
  scrollbar-width: none;
  touch-action: pan-y;
  transform: translate(20px, -16px) scale(0.98);
  transform-origin: top right;
  /* `display` en lugar de `visibility`. Con `visibility` el cajón seguía
     oculto durante los 360 ms de la transición, y un navegador ignora
     `.focus()` sobre un elemento invisible: quien abría el menú con teclado
     se quedaba en el botón (WCAG 2.4.3). Con `allow-discrete`, `display`
     conmuta al PRINCIPIO de la entrada, así que el cajón ya es enfocable
     cuando el JavaScript mueve el foco. De regalo, cerrado es `display: none`
     y sale del orden de tabulación sin depender de `inert`. */
  transition: clip-path 360ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease, transform 360ms cubic-bezier(0.22, 1, 0.36, 1), display 360ms allow-discrete;
  -webkit-overflow-scrolling: touch;
}

.nav-drawer::-webkit-scrollbar {
  display: none;
}

.nav-drawer.is-open {
  display: block;
  clip-path: inset(0);
  opacity: 1;
  pointer-events: auto;
  transform: translate(0) scale(1);
}

/* Estado de partida de la entrada. Sin él, un elemento que pasa de
   `display: none` a visible no tiene desde dónde interpolar y aparecería
   de golpe. Donde `@starting-style` no exista, ocurre exactamente eso:
   el cajón abre instantáneo, que es la degradación aceptable. */
@starting-style {
  .nav-drawer.is-open {
    clip-path: inset(0 0 100% 100%);
    opacity: 0;
    transform: translate(20px, -16px) scale(0.98);
  }
}

.nav-drawer-inner {
  display: grid;
  width: min(100%, 1220px);
  gap: 10px;
  margin: 0 auto;
}

.nav-home,
.nav-group-toggle {
  display: flex;
  width: 100%;
  min-height: 52px;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  color: var(--white);
  font-family: var(--aero-font);
  font-size: clamp(21px, 3vw, 28px);
  letter-spacing: 0.04em;
  line-height: 1;
  text-align: left;
  text-transform: uppercase;
  background: rgba(54, 164, 222, 0.08);
  border: 1px solid var(--line-mid);
  border-radius: 0;
  cursor: pointer;
}

/* --- El cajón no tenía botón de presupuesto ------------------------------
   `.header-cta` está oculto por debajo de 720 px, y lo estaba desde antes de
   este proyecto. Consecuencia: en móvil se abría el menú y no había ni una
   sola forma de pedir presupuesto dentro — había que cerrarlo y buscar el
   flotante, o bajar al pie.

   Va al final del cajón y no al principio a propósito: quien abre un menú
   normalmente sabe a dónde va, y quien no lo sabe se encuentra la acción al
   terminar de leer las opciones. Ponerlo arriba lo convertiría en un peaje.

   Comparte medida y tipografía con `.nav-home` para no romper el ritmo de la
   columna, y color, halo y estados con `.header-cta` —se declaran en los
   bloques que ya comparten los dos— para que sea el mismo botón que en
   escritorio, no uno nuevo. Centrado, porque es una acción y no una entrada
   de navegación. */
.nav-cta {
  display: flex;
  width: 100%;
  min-height: 52px;
  align-items: center;
  justify-content: center;
  margin-block-start: 4px;
  padding: 12px 14px;
  font-family: var(--aero-font);
  font-size: clamp(21px, 3vw, 28px);
  letter-spacing: 0.04em;
  line-height: 1;
  text-align: center;
  text-transform: uppercase;
  border: 0;
  border-radius: 0;
  cursor: pointer;
}

.nav-group-toggle::after {
  width: 9px;
  height: 9px;
  content: "";
  border-right: 2px solid var(--estado);
  border-bottom: 2px solid var(--estado);
  transform: rotate(45deg);
  transition: transform 220ms ease;
}

.nav-group-toggle[aria-expanded="true"]::after {
  transform: rotate(225deg);
}

.nav-home:hover,
.nav-group-toggle:hover,
.nav-group-toggle[aria-expanded="true"] {
  color: var(--estado);
  border-color: var(--line-field);
  background: rgba(54, 164, 222, 0.15);
}

.nav-group-panel {
  display: grid;
  max-height: 0;
  overflow: hidden;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  padding: 0;
  opacity: 0;
  transform: translateY(-8px);
  transition: max-height 440ms cubic-bezier(0.22, 1, 0.36, 1), padding 260ms ease, opacity 220ms ease, transform 280ms ease;
}

.nav-group-panel.is-open {
  max-height: 1800px;
  padding: 12px 0 4px;
  opacity: 1;
  transform: translateY(0);
}

/* --- El acordeón del menú, animado sobre su altura real -------------------
   Medido antes del cambio: el panel anima `max-height: 0 → 1800px` en 440 ms,
   pero mide 314 px. A los 77 ms ya estaba al 97 % de su altura; los 363 ms
   restantes animaban espacio invisible. Del `cubic-bezier` el usuario solo
   veía el primer 12 % de la curva —el tramo más brusco—, así que percibía un
   salto, no una apertura. Es lo que se sentía barato en este menú.

   `interpolate-size: allow-keywords` permite interpolar hacia `auto`, así que
   la animación recorre la altura real y se ve entera. Donde no exista, se
   queda el `max-height` de arriba: exactamente el comportamiento de hoy.

   El `padding` se anima junto a la altura y con la misma curva. Lo ideal
   sería dejarlo estático (animar `padding` es animar layout), pero con
   `box-sizing: border-box` un panel cerrado con `padding` seguiría midiendo
   16 px y asomaría bajo el epígrafe. Un panel de 314 px dentro de un overlay
   fijo hace ese trabajo sin despeinarse. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }

  .nav-group-panel {
    max-height: none;
    height: 0;
    overflow: clip;
    transition: height 320ms cubic-bezier(0.22, 1, 0.36, 1),
                padding-block 320ms cubic-bezier(0.22, 1, 0.36, 1),
                opacity 220ms ease,
                transform 280ms ease;
  }

  .nav-group-panel.is-open {
    height: auto;
  }
}

.nav-group-panel--compact {
  grid-template-columns: minmax(240px, 420px);
}

.menu-column {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 2px;
  padding: 16px;
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.15), rgba(1, 10, 18, 0.82)),
    var(--panel);
  border: 1px solid var(--line-mid);
}

.menu-column a {
  padding: 6px 0;
  color: var(--muted);
  font-family: var(--body-font);
  font-size: 14px;
  letter-spacing: 0;
  line-height: 1.3;
  text-transform: none;
}

.menu-column a:hover,
.menu-column a:focus-visible {
  color: var(--white);
}

.menu-column .menu-column-title {
  margin-bottom: 7px;
  color: var(--estado);
  font-family: var(--ui-font);
  font-size: 20px;
  letter-spacing: 0.06em;
  line-height: 1;
  text-transform: uppercase;
}

/* «Dónde estoy» no es «qué hago». El estado de página actual iba en naranja,
   el mismo color que el botón de contacto: el menú parecía tener un CTA
   suelto. Ahora se marca con estructura —filete a la izquierda— y el texto
   sube a blanco pleno, que es lo que de verdad distingue el elemento activo
   de los demás enlaces del menú. */
.nav-home.is-current,
.menu-column a.is-current {
  color: var(--white);
  box-shadow: inset 2px 0 0 var(--estado);
  padding-inline-start: 10px;
}

.call-float {
  position: fixed;
  z-index: 900;
  right: auto;
  bottom: 26px;
  left: 0;
  display: inline-flex;
  min-height: 52px;
  align-items: center;
  justify-content: center;
  padding: 13px 18px;
  color: var(--white);
  /* `#173a52` era una segunda escala de superficies en paralelo a
     `--surface-*`. El flotante es algo que flota, así que le corresponde el
     escalón elevado de la escala, no un azul propio. */
  /* K5 · Baja de peso: con `--surface-3` empataba con el botón de WhatsApp,
     que es la conversión declarada del proyecto. Dos llamadas del mismo peso
     no son dos oportunidades — son una decisión que el visitante tiene que
     tomar antes de actuar.

     NO se deja transparente del todo, aunque «fantasma» lo pidiera: es un
     elemento FIJO sobre contenido que cambia al desplazarse, y con relleno
     transparente el texto blanco puede acabar sobre una fotografía clara.
     Ninguna herramienta automática detecta eso —el contraste sobre
     `backdrop-filter` no es calculable—, así que el velo oscuro se queda y
     garantiza el 4,5:1 pase lo que pase por debajo. Lo que baja es el peso,
     no el contraste. */
  background: rgba(11, 12, 14, 0.72);
  backdrop-filter: blur(8px);
  border: 1px solid var(--line-strong);
  border-radius: 0;
  box-shadow: none;
  font-family: var(--ui-font);
  font-size: 20px;
  letter-spacing: 0.08em;
  line-height: 1;
  text-align: center;
  text-transform: uppercase;
  /* La sombra sale de la transición: animar `box-shadow` obliga a repintar
     el elemento en cada frame. Aparece de golpe —nadie mira la sombra— y el
     movimiento lo lleva el `transform`, que va al compositor. El aspecto en
     reposo no cambia. */
  transition: transform 180ms ease, background-color 180ms ease;
}

.call-float:hover,
.call-float:focus-visible {
  color: var(--white);
  background: #1b2838;   /* un escalon por encima de --surface-3 en la misma escala */
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(35, 151, 211, 0.28);
}

.whatsapp-float {
  position: fixed;
  z-index: 900;
  right: 20px;
  bottom: 20px;
  display: inline-flex;
  width: 64px;
  height: 64px;
  min-height: 64px;
  align-items: center;
  justify-content: center;
  padding: 0;
  overflow: hidden;
  background: #25d366;
  border: 0;
  border-radius: 50%;
  /* Sin halo. En julio se retiró el resplandor de `.btn-primary`, `.header-cta`
     y `.social-link` con el argumento de que «un halo alrededor del botón que
     gana dinero es el recurso más barato que existe». El flotante se quedó
     fuera de aquella limpieza — y es el único elemento que está en pantalla
     todo el rato, en las 19 páginas. La elevación la da el propio círculo
     verde sobre fondo casi negro; no necesita ayuda. */
  box-shadow: none;
  transition: transform 180ms ease;
}

.whatsapp-float:hover,
.whatsapp-float:focus-visible {
  transform: translateY(-2px) scale(1.04);
}

.whatsapp-float img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Compatibilidad visual para las nuevas páginas de contenido SEO. */
.container {
  width: min(100%, 1220px);
  margin-inline: auto;
}

/* --- Ritmo vertical: tres pesos de sección, no uno ------------------------
   Medido antes del cambio en `rotulacion-vehiculos.html`: las 16 secciones
   tenían exactamente el mismo relleno, 86/86 px a 1440 y 54/54 a 375. Sin una
   excepción. Eso no es una composición, es un metrónomo: 16 lajas idénticas
   donde el ojo nunca encuentra dónde el diseño sube la voz.

   Tres pesos, y el relleno superior distinto del inferior —una sección respira
   algo más por debajo, porque el título ya aporta aire por arriba. Es el
   detalle que hace que un documento parezca compuesto y no apilado.

     tight    40/40 · 56/56     bandas de apoyo (anclas, avisos, tira de confianza)
     base     56/64 · 88/96     la sección normal de contenido
     chapter  88/96 · 128/144   cambio de capítulo (antes de conversión, galería, pie)

   Se aplica cambiando una regla y añadiendo dos modificadores. Los 16
   selectores no se tocan. */
/* ===========================================================================
   PUNTOS DE RUPTURA — la escalera, declarada · Xpandex, 2026-08-10
   ---------------------------------------------------------------------------
   P10 pedía consolidarlos. Había 17 valores; quedan 15, y conviene saber por
   qué no bajan más: OCHO de ellos son CUATRO puntos expresados en las dos
   direcciones, que es lo correcto y no se toca.

     719 / 720          frontera móvil ↔ escritorio (la principal del sitio)
     999 / 1000         el héroe pasa de columna a dos columnas
     1099,98 / 1100     aparece la navegación de escritorio
     1199,98 / 1200     tres columnas donde el contenedor ya da de sí

   Los demás son de componente, cada uno con su motivo:

      420   la marca y el menú se aprietan en pantallas muy estrechas
      440   segundo escalón del alto de cabecera (89 · 97 · 103 · 118)
      540   las tablas de las páginas legales
      640   el visor de fotografía a pantalla completa
      900   el panel del menú desplegable
     1024   el flotante de llamada se retira
     1440   nada estructural: solo el reparto de dos rejillas

   Fusionados el 10/08: **700 → 720** (el formulario de presupuesto tenía punto
   propio para 20 px de diferencia) y **1240 → 1200** (el filete vertical de
   marca de agua; medido el canalón, entra de sobra en 1200). Retirados además
   dos `@media` VACÍOS, uno de ellos con `.hero-visual`, clase que ya no existe
   en ninguna página.

   Regla para lo que venga: antes de añadir un valor nuevo, comprobar si el
   componente puede vivir en uno de los cuatro principales.
   =========================================================================== */

:root {
  --sec-y-start: 56px;
  --sec-y-end:   64px;
  --sec-x:       18px;
}

.section,
.page-hero,
.section-pad {
  padding-block: var(--sec-y-start) var(--sec-y-end);
  padding-inline: var(--sec-x);
}

.section--tight   { --sec-y-start: 40px; --sec-y-end: 40px; }
.section--chapter { --sec-y-start: 88px; --sec-y-end: 96px; }

.section-sm {
  padding: 42px 18px;
}

.page-hero,
.section-dark {
  background:
    linear-gradient(180deg, rgba(26, 26, 29, 0.88), rgba(8, 8, 10, 0.88)),
    url("./assets/fondo-web.svg") center / cover fixed no-repeat;
}

.section-surface {
  background: rgba(0, 6, 12, 0.64);
}

.page-hero-grid,
.split,
.quote-layout {
  display: grid;
  gap: 28px;
  align-items: center;
}

.page-hero-media,
.split-media {
  min-width: 0;
  margin: 0;
  background: var(--panel);
  border: 1px solid var(--line-mid);
}

.page-hero-media img,
.split-media img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.lead {
  color: var(--white);
  font-size: 18px;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
}

.btn-secondary,
.btn-outline,
.btn-outline-light {
  color: var(--white);
  background: rgba(54, 164, 222, 0.12);
  border: 1px solid var(--line-strong);
}

.anchor-nav {
  position: sticky;
  z-index: 10;
  top: var(--header-height);
  padding: 10px 18px;
  background: rgba(2, 5, 11, 0.94);
  border-block: 1px solid var(--line-mid);
}

.anchor-nav-inner {
  display: flex;
  overflow-x: auto;
  gap: 8px;
  scrollbar-width: none;
}

.anchor-nav-inner::-webkit-scrollbar {
  display: none;
}

.anchor-nav a {
  flex: 0 0 auto;
  padding: 7px 10px;
  color: rgba(245, 251, 255, 0.72);
  font-size: 16px;
  text-transform: uppercase;
  background: transparent;
  border: 1px solid var(--line);
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}

.anchor-nav a:hover,
.anchor-nav a:focus-visible {
  color: var(--white);
  border-color: var(--line-strong);
}

/* --- Scrollspy: dónde estás dentro de una página larga -------------------
   Las páginas de servicio miden hasta 16.900 px en un teléfono. Sin esto, la
   barra de anclas dice a dónde puedes ir pero no dónde estás.

   El estado activo se marca con BLANCO y filete, no con el color de acción:
   «estás aquí» no es «pulsa aquí». Es la misma regla que se aplicó al menú.
   La clase la pone `setupScrollspy()`, que además escribe `aria-current`. */
.anchor-nav a.es-activa {
  color: var(--black);
  background: var(--estado);
  border-color: var(--estado);
}

.feature-grid,
.solution-grid {
  display: grid;
  gap: 16px;
}

.feature-card,
.solution-card,
.callout,
.info-panel,
.contact-card {
  position: relative;
  padding: 20px;
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.16), rgba(1, 10, 18, 0.78)),
    var(--panel);
  border: 1px solid var(--line-mid);
  border-radius: 0;
}

/* Mismos ordinales, misma razón que en `.proof-grid strong`. */
.feature-index,
.mini-label,
.step-number {
  display: inline-flex;
  margin-bottom: 12px;
  color: rgba(245, 251, 255, 0.5);
  font-size: 24px;
  line-height: 1;
}

.solution-card > a {
  position: absolute;
  inset: 0;
}

.check-list {
  display: grid;
  gap: 8px;
  padding-left: 20px;
}

.check-list li {
  color: var(--white);
}

.text-link {
  color: var(--estado);
}

.mt-30 {
  margin-top: 30px;
}

.contact-stack {
  display: grid;
  gap: 12px;
}

.contact-card {
  display: grid;
  gap: 4px;
}

.contact-card span {
  color: var(--estado);
  font-size: 16px;
  text-transform: uppercase;
}

.contact-card strong {
  color: var(--white);
  font-family: var(--body-font);
  letter-spacing: 0;
}

.section-heading.centered {
  margin-inline: auto;
  text-align: center;
}

.section-dark .feature-card h3,
.section-dark h2,
.section-dark h3,
.info-panel h3 {
  color: var(--white);
}

.form-footer {
  display: grid;
  gap: 12px;
}

.legal-hero .container,
.legal-document {
  max-width: 980px;
}

.legal-document {
  margin-inline: auto;
}

.legal-meta {
  margin-top: 18px;
  color: var(--estado);
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.legal-summary,
.legal-block {
  padding: 24px;
  background: rgba(9, 36, 55, 0.72);
  border: 1px solid var(--line-mid);
}

.legal-summary {
  display: grid;
  gap: 14px;
  margin-bottom: 30px;
}

.legal-block + .legal-block {
  margin-top: 18px;
}

.legal-block h2,
.legal-block h3 {
  color: var(--white);
}

.legal-block h2 {
  margin-bottom: 16px;
  font-size: clamp(26px, 5vw, 42px);
}

.legal-block h3 {
  margin-top: 24px;
  margin-bottom: 10px;
  font-size: 20px;
}

.legal-block p,
.legal-block li,
.legal-summary p,
.legal-summary dd,
.legal-summary dt {
  color: rgba(255, 255, 255, 0.86);
}

.legal-block p + p,
.legal-block ul + p,
.legal-block p + ul {
  margin-top: 12px;
}

.legal-block ul {
  display: grid;
  gap: 8px;
  margin: 12px 0 0;
  padding-left: 22px;
}

.legal-document a:not(.btn) {
  color: var(--estado);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.legal-data {
  display: grid;
  grid-template-columns: minmax(110px, 0.35fr) minmax(0, 1fr);
  gap: 8px 16px;
  margin: 0;
}

.legal-data dt {
  font-weight: 700;
}

.legal-data dd {
  margin: 0;
}

.legal-table-wrap {
  margin-top: 18px;
  overflow-x: auto;
  border: 1px solid var(--line-mid);
}

.legal-table {
  width: 100%;
  min-width: 720px;
  border-collapse: collapse;
}

.legal-table th,
.legal-table td {
  padding: 12px 14px;
  color: rgba(255, 255, 255, 0.86);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--line-hair);
}

.legal-table th {
  color: var(--white);
  font-size: 14px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: rgba(54, 164, 222, 0.14);
}

.legal-table tr:last-child td {
  border-bottom: 0;
}

.form-privacy-note {
  margin-top: 12px;
  color: rgba(255, 255, 255, 0.72);
  font-size: 14px;
}

.form-privacy-note a {
  color: var(--estado);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* --- El formulario deja de invertir el tema ------------------------------
   Era el elemento más ruidoso de la web, y justo en la página que más
   autoridad necesita: panel `#173a52` —fuera de la escala `--surface-*`, una
   segunda paleta en paralelo—, una pletina de cian de 4 px incrustada, y
   nueve campos casi blancos `#f5f8fa` sobre un sitio negro. El formulario no
   parecía parte de la web: parecía pegado encima.

   La sombra se retira porque sobre negro no se ve; solo ensucia. En tema
   oscuro la profundidad se construye con luminosidad y con filete.

   Contrastes del resultado, calculados: texto de campo 16,4:1 · borde de
   campo 5,9:1 · marcador de posición 5,2:1. Todos por encima de AA. */
.budget-form {
  position: relative;
  color: var(--white);
  background: var(--surface-3);
  border-color: var(--line-mid);
  box-shadow: none;
}

.budget-form-intro {
  margin: 10px 0 8px;
  color: #ffffff;
  font-family: var(--body-font);
  font-size: 16px;
  line-height: 1.45;
  letter-spacing: 0;
}

.budget-form-grid {
  display: grid;
  gap: 16px;
}

.budget-form label,
.budget-form label > span {
  color: #ffffff;
}

.budget-form label > span b {
  color: #ff835f;
}

/* El campo va por DEBAJO del panel en la escala de superficies, no por
   encima: un input hundido es la señal correcta de «aquí se escribe». Con
   `--surface-1` dentro de un panel `--surface-3` el campo se lee recogido, y
   el filete fuerte (`--line-field`, 7,07:1 sobre el campo) hace el trabajo
   que la norma pide para el contorno de un control (WCAG 1.4.11, 3:1). */
.budget-form input,
.budget-form select,
.budget-form textarea {
  color: var(--white);
  background: var(--surface-1);
  border-color: var(--line-field);
}

.budget-form input::placeholder,
.budget-form textarea::placeholder {
  color: rgba(245, 251, 255, 0.52);   /* 5,4:1: legible sin competir con lo escrito */
  opacity: 1;
}

/* La lista desplegable la pinta el sistema operativo, así que se le declaran
   los dos colores explícitamente o hereda el blanco del navegador. */
.budget-form select option,
.budget-form select optgroup {
  color: var(--white);
  background: var(--surface-1);
}

.budget-consent {
  display: flex !important;
  align-items: flex-start;
  gap: 11px !important;
  color: #ffffff;
  font-family: var(--body-font);
  font-size: 14px !important;
  line-height: 1.45;
  letter-spacing: 0;
  text-transform: none !important;
}

.budget-consent input {
  flex: 0 0 20px;
  width: 20px;
  min-height: 20px;
  margin: 1px 0 0;
  accent-color: var(--estado);
}

.budget-consent a,
.budget-form .form-privacy-note a {
  color: #9edfff;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.budget-honeypot {
  position: absolute !important;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.budget-form-status {
  min-height: 22px;
  margin: 0;
  color: #ffffff;
  font-family: var(--body-font);
  font-size: 16px;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0;
}

.budget-form-status.is-success {
  color: #8dffbd;
}

.budget-form-status.is-error {
  color: #ffd1c5;
}

.budget-form [data-budget-submit]:disabled {
  cursor: wait;
  opacity: 0.68;
}

.budget-form .form-privacy-note {
  margin-top: 0;
  padding-top: 14px;
  color: rgba(245, 251, 255, 0.82);
  font-family: var(--body-font);
  line-height: 1.45;
  letter-spacing: 0;
  border-top: 1px solid var(--line-mid);
}

/* Antes en 700 px. Se alinea con la frontera móvil/escritorio del sitio —719 y
   720— en lugar de mantener un punto de ruptura propio para 20 px: entre 700 y
   719 el formulario pasa a una columna, igual que el resto de la página. */
@media (min-width: 720px) {
  .budget-form-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .budget-field-full {
    grid-column: 1 / -1;
  }
}

@media (max-width: 540px) {
  .legal-summary,
  .legal-block {
    padding: 20px 16px;
  }

  .legal-data {
    grid-template-columns: 1fr;
    gap: 2px;
  }

  .legal-data dd + dt {
    margin-top: 10px;
  }
}

@media (min-width: 720px) {
  .page-hero-grid,
  .split,
  .quote-layout {
    grid-template-columns: minmax(0, 1.05fr) minmax(280px, 0.95fr);
  }

  .split.reverse > :first-child {
    order: 2;
  }

  .feature-grid,
  .solution-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  :root {
    --sec-y-start: 72px;
    --sec-y-end:   80px;
    --sec-x:       clamp(28px, 5vw, 64px);
  }

  .section--tight   { --sec-y-start: 48px; --sec-y-end: 48px; }
  .section--chapter { --sec-y-start: 104px; --sec-y-end: 116px; }
}

@media (min-width: 1100px) {
  :root {
    --sec-y-start: 88px;
    --sec-y-end:   96px;
    --sec-x:       clamp(56px, 8vw, 132px);
  }

  .section--tight   { --sec-y-start: 56px;  --sec-y-end: 56px; }
  .section--chapter { --sec-y-start: 128px; --sec-y-end: 144px; }
}

@media (max-width: 900px) {
  .nav-group-panel {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 719px) {
  .site-header {
    grid-template-columns: 1fr auto;
    gap: 12px;
    min-height: 68px;
    padding: 9px 14px;
  }

  .menu-toggle {
    grid-column: 2;
    grid-row: 1;
    width: 46px;
    height: 46px;
  }

  .nav-drawer {
    inset: var(--header-height) 0 0 20%;
    padding: 14px 14px 32px;
    box-shadow: -18px 0 38px rgba(0, 0, 0, 0.48);
  }

  .nav-group-panel,
  .nav-group-panel--compact {
    grid-template-columns: 1fr;
  }

  .nav-group-panel.is-open {
    max-height: 2600px;
  }

  .nav-home,
  .nav-group-toggle,
  .nav-cta {
    min-height: 48px;
    font-size: 20px;
  }

  .menu-column {
    padding: 14px;
  }

  .whatsapp-float {
    right: 14px;
    bottom: 14px;
    width: 56px;
    height: 56px;
    min-height: 56px;
  }

  .call-float {
    right: auto;
    bottom: 18px;
    left: 0;
    min-height: 48px;
    padding: 11px 16px;
    border-radius: 0 10px 10px 0;
    font-size: 20px;
  }

  .button-row .btn {
    width: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
    /* La regla original solo neutralizaba transiciones. Hoy el sitio no tiene
       ni un @keyframes, así que bastaba; en cuanto se añada la primera
       animación deja de proteger a quien ha pedido menos movimiento.
       Los retardos importan tanto como las duraciones: sin ellos, un
       escalonado seguiría haciendo esperar aunque la animación fuese
       instantánea. */
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
    transition-delay: 0s !important;
  }
}

@media (min-width: 1100px) {
  .site-header {
    grid-template-columns: max-content minmax(0, 1fr) max-content;
    gap: clamp(18px, 2vw, 36px);
    min-height: 92px;
    padding: 14px clamp(30px, 4vw, 72px);
  }

  .brand-link img {
    width: clamp(132px, 9vw, 164px);
  }

  .desktop-nav {
    display: flex;
    min-width: 0;
    align-items: center;
    justify-content: center;
    gap: clamp(2px, 0.5vw, 9px);
  }

  .desktop-nav-group {
    position: relative;
  }

  .desktop-nav-link,
  .desktop-nav-toggle {
    display: inline-flex;
    min-height: 44px;
    align-items: center;
    justify-content: center;
    padding: 10px clamp(7px, 0.75vw, 14px);
    color: var(--white);
    font-family: var(--aero-font);
    font-size: clamp(15px, 1.05vw, 19px);
    letter-spacing: 0.025em;
    line-height: 1;
    text-transform: uppercase;
    white-space: nowrap;
    background: transparent;
    border: 0;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    cursor: pointer;
    transition: color 180ms ease, border-color 180ms ease, background-color 180ms ease;
  }

  .desktop-nav-toggle {
    gap: 9px;
  }

  .desktop-nav-toggle::after {
    width: 7px;
    height: 7px;
    content: "";
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
    transition: transform 220ms ease;
  }

  .desktop-nav-toggle[aria-expanded="true"]::after {
    transform: translateY(2px) rotate(225deg);
  }

  .desktop-nav-link:hover,
  .desktop-nav-link.is-current,
  .desktop-nav-toggle:hover,
  .desktop-nav-toggle.is-current,
  .desktop-nav-toggle[aria-expanded="true"] {
    color: var(--estado);
    border-bottom-color: transparent;   /* el filete lo dibuja el ::before */
    background: rgba(245, 251, 255, 0.06);
  }

  /* El subrayado se traza desde la izquierda en lugar de encenderse de golpe.
     Un `transform: scaleX` es una operación de compositor; cambiar el color
     del borde es un repintado. Además el gesto significa algo: es el recorrido
     de la cuchilla del plóter, que es de lo que vive este cliente. */
  .desktop-nav-link,
  .desktop-nav-toggle { position: relative; }

  .desktop-nav-link::before,
  .desktop-nav-toggle::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    inset-block-end: -1px;
    block-size: 2px;
    background: var(--estado);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 220ms var(--ease-corte);
  }

  .desktop-nav-link:hover::before,
  .desktop-nav-link:focus-visible::before,
  .desktop-nav-link.is-current::before,
  .desktop-nav-toggle:hover::before,
  .desktop-nav-toggle:focus-visible::before,
  .desktop-nav-toggle.is-current::before,
  .desktop-nav-toggle[aria-expanded="true"]::before { transform: scaleX(1); }

  .desktop-nav-panel {
    position: fixed;
    z-index: 1002;
    top: var(--header-height);
    right: auto;
    left: var(--desktop-panel-left, 24px);
    display: grid;
    width: var(--desktop-panel-width, min(1000px, calc(100vw - 48px)));
    max-height: min(72vh, 720px);
    overflow-y: auto;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px;
    padding: 18px;
    background:
      linear-gradient(180deg, rgba(10, 10, 13, 0.99), rgba(15, 15, 18, 0.99)),
      url("./assets/fondo-web.svg") center top / cover fixed no-repeat;
    border: 1px solid var(--line-mid);
    border-top: 0;
    box-shadow: 0 24px 48px rgba(0, 0, 0, 0.42);
    opacity: 0;
    pointer-events: none;
    transform: translateY(-12px);
    transition: opacity 200ms ease, transform 280ms cubic-bezier(0.22, 1, 0.36, 1), visibility 280ms;
    visibility: hidden;
  }

  .desktop-nav-group:hover .desktop-nav-panel,
  .desktop-nav-group:focus-within .desktop-nav-panel,
  .desktop-nav-panel.is-open {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
    visibility: visible;
  }

  .desktop-nav-panel--compact {
    grid-template-columns: 1fr;
  }

  .desktop-nav-panel .menu-column {
    padding: 16px;
  }

  .menu-toggle {
    display: none;
  }

  .header-cta {
    min-height: 44px;
    max-width: 238px;
    padding: 10px 14px;
    font-size: clamp(15px, 1vw, 18px);
  }
}

/* ===========================================================================
   MEJORAS DE ACCESIBILIDAD Y UX — Xpandex, 2026-07-29
   Bloque añadido al final a propósito: al ir después en la cascada, corrige
   sin reescribir las reglas existentes y se puede retirar entero de una vez.
   Referencias: propuestas P3-P6 del informe de accesibilidad y rendimiento,
   y P4-P5 del informe de UX/UI.
   =========================================================================== */

/* --- P3 · Enlace de salto al contenido (WCAG 2.4.1, nivel A) ---------------
   Se desplaza fuera de la vista en lugar de ocultarse con display:none,
   porque un elemento oculto de ese modo no es enfocable y el enlace no
   funcionaría con teclado. */
.skip-link {
  position: absolute;
  z-index: 2000;
  inset-inline-start: 8px;
  inset-block-start: -200px;
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  padding: 12px 20px;
  background: var(--estado);
  color: var(--black);
  font-family: var(--ui-font);
  font-size: 18px;
  letter-spacing: 0.04em;
  text-decoration: none;
  border-radius: var(--radius);
  transition: inset-block-start 140ms ease;
}

.skip-link:focus-visible,
.skip-link:focus {
  inset-block-start: 8px;
  outline: 3px solid var(--white);
  outline-offset: 2px;
}

/* --- P4 · Indicador de foco propio y global -------------------------------
   17 de los 30 elementos del recorrido dependían del contorno por defecto
   del navegador: 1 px sobre fondo casi negro, invisible en la práctica.
   :where() mantiene la especificidad en 0 para no pisar estilos concretos. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--estado);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Contorno claro sobre los elementos que ya son cian */
:where(.btn-primary, .header-cta, .nav-cta, .skip-link):focus-visible {
  outline-color: var(--white);
}

/* --- P5 · Tamaño mínimo de objetivo (WCAG 2.5.8, nivel AA) ----------------
   El mínimo normativo es 24×24. La norma del estudio es 44×44 en móvil. */
:where(.site-footer, .footer-contact, .contact-social) a,
:where(.cookie-settings-link, .social-link, .menu-toggle, .nav-group-toggle) {
  min-block-size: 44px;
  min-inline-size: 44px;
  display: inline-flex;
  align-items: center;
}

/* Los enlaces de texto del pie no deben volverse cuadrados: basta el alto */
:where(.site-footer, .footer-contact) a {
  min-inline-size: 0;
}

.social-link {
  justify-content: center;
}

/* --- A3 · Región desplazable alcanzable con teclado ------------------------ */
.legal-table-wrap:focus-visible {
  outline: 3px solid var(--estado);
  outline-offset: 2px;
}

/* --- P6 · Botones flotantes como región con nombre ------------------------
   El <aside> es solo semántico: no debe introducir caja ni desplazamiento. */
.floating-actions {
  display: contents;
}

/* --- Los flotantes dejan de tapar contenido -------------------------------
   Medido a 375: los dos botones ocupan de forma permanente ~70 px del alto de
   pantalla y se superponen a lo que haya debajo — en la portada, al crédito
   del héroe; al final de las páginas, al pie.

   Esto reserva el hueco al final del documento, así que el último contenido
   siempre puede desplazarse por encima de ellos. NO decide cuántos flotantes
   hay: esa es la decisión D2, que es del cliente. Arregla el daño sin
   adelantarse a la decisión.

   `env(safe-area-inset-bottom)` cubre la barra de gestos de los iPhone. */
@media (max-width: 719px) {
  body {
    padding-block-end: calc(88px + env(safe-area-inset-bottom, 0px));
  }
}

/* --- Menú móvil como diálogo ----------------------------------------------
   Mientras está abierto, el fondo queda inerte. `inert` ya impide la
   interacción; esto solo lo refuerza visualmente en navegadores que aún no
   lo pintan de forma distinta. */
[inert] {
  cursor: default;
  user-select: none;
}

/* --- Respeto al movimiento reducido en lo añadido ------------------------- */
@media (prefers-reduced-motion: reduce) {
  .skip-link {
    transition: none;
  }
}

/* ===========================================================================
   IMÁGENES: recorte y tamaño — Xpandex, 2026-07-29
   ---------------------------------------------------------------------------
   Diagnóstico medido sobre las 45 fotos publicadas:
     · 51 % son verticales · 20 % cuadradas · 28 % apaisadas
     · proporción mediana: 0,89 (casi cuadrada)
   El CSS las metía en cajas de 16/10 y 4/3, es decir apaisadas. Consecuencia:
     · se descartaba de media el 37 % de la altura de cada foto
     · 29 de las 45 perdían más de un tercio de la imagen
     · el peor caso (revestimiento de columnas, proporción 0,30) perdía el 81 %
   Y a 1280 px cada foto se veía a 346×216, tres por fila.

   Dos decisiones:
     1. En galerías y muestras de trabajo NO se recorta. En un portfolio de
        rotulación la foto es el producto; se enseña entera.
     2. Donde hace falta rejilla uniforme, se pasa de 16/10 a cuadrado: la
        pérdida media baja del 38 % al 23 % y los recortes graves de 29 a 6.
   =========================================================================== */

.gallery-grid > figure {
  display: block;
  break-inside: avoid;     /* que una foto no se parta entre dos columnas */
  margin: 0 0 16px;
}

.gallery-grid .bevel-card img,
.gallery-grid .media-card img {
  width: 100%;
  height: auto;
  aspect-ratio: auto;      /* la del fichero: los width/height del HTML ya son los reales */
  object-fit: contain;
  /* Tope para los formatos extremos: hay una foto de proporción 0,30
     (revestimiento de columnas) que sin límite ocuparía tres pantallas.
     Con `contain` se ve entera, solo que sin llenar el ancho. */
  max-height: 74vh;
}

/* --- 2. Fuera de la galería: cuadrado en vez de apaisado ------------------ */
.bevel-card img,
.media-card img,
.service-card img {
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: var(--encuadre-x, 50%) 42%;   /* el motivo del rótulo suele estar algo por encima del centro */
}

/* --- 3. Más grandes: menos columnas en escritorio ------------------------- */
@media (min-width: 720px) {
  /* `.wide` era «una columna de imágenes apaisadas». Sin recorte, una sola
     columna deja fotos de 1073 px de ancho y verticales de más de 1400 px de
     alto: pasa a dos columnas, que sigue siendo un 53 % más grande que las
     346 px de antes. */

  /* Las rejillas uniformes pasan de 3 a 2 columnas: cada imagen casi duplica
     su ancho en un portátil de 1280 px. */
  .proof-grid,
  .priority-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 1440px) {

  .proof-grid,
  .priority-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* --- 4. Héroe: el peor recorte del sitio ----------------------------------
   El mosaico era 16/10 partido en DOS FILAS, así que cada celda quedaba en
   proporción 3,2 — una franja. Con las dos fotos cuadradas (1,00 y 1,05) eso
   descartaba el 69 % y el 67 % de cada imagen, y a 1280 px cada una se veía
   en 495×155.

   Pasa a dos celdas cuadradas, una al lado de la otra: las fotos entran
   enteras y ganan altura. Se retiran los `object-position` desplazados, que
   solo existían para elegir qué franja se salvaba del recorte. */
/* (El ajuste responsive del mosaico vivía aquí. Retirado con el resto de
   `.hero-mosaic`, que ya no existe en ninguna página.) */

/* ===========================================================================
   REJILLAS FLUIDAS — Xpandex, 2026-07-29
   ---------------------------------------------------------------------------
   El sitio fijaba el número de columnas por breakpoint. Medido, eso producía
   saltos absurdos:
     · 700 px de pantalla → tarjeta de 664 px
     · 720 px de pantalla → tarjeta de 316 px   (un píxel más, la mitad de ancho)
     · 1280 px → tarjeta de 530 px
     · 1440 px → tarjeta de 393 px              (pantalla más grande, tarjeta MÁS PEQUEÑA)

   Con `auto-fit` + `minmax` el número de columnas lo decide el espacio real:
   la tarjeta nunca baja de su ancho mínimo y crece de forma continua. Se
   acabaron los saltos y los breakpoints a mano para esto.

   El `min(100%, …)` del mínimo evita el desbordamiento clásico de `auto-fit`
   en pantallas más estrechas que el propio mínimo.

   El mínimo no es el mismo para todas: en las rejillas con fotografía manda
   el tamaño al que se ve el trabajo (26rem), y en las de texto manda la
   longitud de línea legible (20rem).
   =========================================================================== */

/* Rejillas con fotografía: la imagen manda.
   El máximo de 34rem evita el efecto contrario: en una sola columna a 900 px
   la tarjeta llegaba a 810 px de ancho y, con imagen cuadrada, a 810 px de
   alto. Con techo, la tarjeta crece hasta un tamaño sensato y se centra.

   --- 21rem, y por qué no 26 ni 24 (medido 2026-08-07) -----------------------
   Con 26rem la rejilla NO alcanzaba tres columnas hasta los 1600 px: las tres
   tarjetas de §priority caían a 2+1 y las cinco de §conversion-band a 2+2+1 en
   todos los portátiles. Esa era la asimetría que se percibía.

     minmax   1180 1280 1366 1440 1512 1600
     26rem      2    2    2    2    2    3
     24rem      2    2    2    3    3    3
     21rem      2    3    3    3    3    3     ← se elige este
     20rem      2    3    3    3    3    4     ← se fragmenta a cuatro

   Dirección de arte proponía 24rem, que solo arregla de 1440 en adelante y
   deja fuera 1280 y 1366 —los dos anchos de portátil más comunes—. A 1180 px
   ninguna configuración da tres columnas, y está bien: el contenedor mide
   1.075 px y tres tarjetas de 340 serían ilegibles. */
.priority-grid,
.proof-grid {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  justify-items: center;
}

/* El techo va en la tarjeta, no en la pista de la rejilla: si se pone en la
   pista con un valor fijo, el reparto se rompe y a 1280 px se queda en una
   sola columna. Con la pista en `1fr` y el tope en el hijo, las columnas
   siguen siendo fluidas y la tarjeta no se desborda.

   K11 pedía retirar este techo con el argumento de que «con tres columnas
   reales ya no protege de nada». **Se conserva, porque no es cierto.** Medido
   el ancho de la tarjeta con y sin él:

     ventana   contenedor  columnas   con techo   sin techo
       600 px     564 px       1        544 px      564 px   ← actúa
       720 px     648 px       1        544 px      648 px   ← actúa
       900 px     810 px       2        397 px      397 px
      1280 px    1075 px       3        348 px      348 px

   Entre 600 y 860 px la rejilla está en UNA columna y sin techo la tarjeta se
   iría a 648 px de ancho — con imagen cuadrada, 648 px de alto. Es exactamente
   el caso que el párrafo de arriba describe, y sigue vivo. */
.priority-grid > *,
.proof-grid > * {
  inline-size: 100%;
  max-inline-size: 34rem;
}

/* Rejillas de texto: la legibilidad manda */
.process-list,
.service-detail-grid {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}

.faq-list {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
}

/* Las rejillas fluidas ya no necesitan que el breakpoint les diga cuántas
   columnas caben: se anulan las reglas fijas anteriores. */
@media (min-width: 720px) {
  .priority-grid,
  .proof-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  }
  .process-list,
  .service-detail-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  }
}

@media (min-width: 1440px) {
  .priority-grid,
  .proof-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  }
}


/* ===========================================================================
   OLA P1 — ELEVACIÓN VISUAL · Xpandex, 2026-07-29
   ---------------------------------------------------------------------------
   Bloque al final de la hoja a propósito: corrige por cascada sin reescribir
   las reglas originales, y puede retirarse entero si el cliente no lo quiere.

   Diagnóstico que lo motiva (medido sobre esta misma hoja):
     · 9 clases compartían degradado azul + borde cian al 42 %: la interfaz
       teñía la fotografía, que en una web de rotulación ES el producto
     · 19 opacidades distintas del mismo cian, 34 usos literales
     · 4 tokens declarados y usados cero veces
     · 10 reglas :hover, todas en la navegación · 0 reglas :active
   =========================================================================== */

:root {
  /* --- Un solo filete, tres pesos con significado ------------------------
     El filete es NEUTRO, no cian. Los cinco tokens eran `rgba(101,198,238,α)`
     —cian disuelto en 45 selectores—: sutil de uno en uno, muy visible en
     conjunto. Era la razón de que la página «supiera» a azul aunque los
     fondos ya fueran neutros, y de que el cian significase ocho cosas a la
     vez. Ahora el cian solo señala estado de interacción (foco, filtro
     activo, navegación actual).

     El cambio de tinte NO cuesta contraste. Medido sobre --surface-2:
       hair   1,30 → 1,30      line   1,43 → 1,58      mid  2,28 → 2,27
       strong 3,97 → 4,23      field  7,18 → 7,27
     Los dos que la norma exige por encima de 3:1 (WCAG 1.4.11) suben. */
  --line-hair:   rgba(245, 251, 255, 0.10);  /* sombras suaves, separaciones internas   */
  --line:        rgba(245, 251, 255, 0.16);  /* borde por defecto de tarjeta            */
  --line-mid:    rgba(245, 251, 255, 0.26);  /* bordes estructurales, paneles           */
  --line-strong: rgba(245, 251, 255, 0.44);  /* énfasis, hover de tarjeta               */
  --line-field:  rgba(245, 251, 255, 0.62);  /* campos de formulario y estado actual:
                                                aquí el borde ES información y necesita
                                                contraste (WCAG 1.4.11)                 */

  /* --- Superficies por luminosidad, no por tinte -------------------------
     La profundidad en tema oscuro se construye subiendo el plano, no
     añadiendo color. Así el azul deja de estar untado bajo cada foto. */
  --surface-1: #141619;   /* banda de sección  */
  --surface-2: #1e2126;   /* tarjeta           */
  --surface-3: #2a2e34;   /* tarjeta elevada   */

  /* --- Elevación: dos sombras, neutras, solo para lo que flota ----------- */
  --elev-overlay: 0 24px 48px rgba(0, 0, 0, 0.42);
  --elev-sticky:  0 -8px 24px rgba(0, 0, 0, 0.34);

  /* --- Movimiento: dos duraciones y una curva, la que el sitio ya usaba -- */
  --dur-fast: 160ms;
  --dur-slow: 320ms;
  --dur-visor: 260ms;   /* apertura del visor de fotografía */

  /* La curva de salida acelera: lo que se va, se va. Usar la misma curva de
     entrada para salir hace que todo parezca dudar. */
  --ease-salida: cubic-bezier(0.4, 0, 1, 1);
  /* Simétrica, para barridos y cortes: entra y sale al mismo ritmo, como una
     cuchilla que recorre el vinilo. */
  --ease-corte:  cubic-bezier(0.65, 0, 0.35, 1);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* --- Radio: el canto vivo es la norma; la excepción se declara --------- */
  --radius-pill: 999px;
}

/* --- P1.1 · Superficies neutras ------------------------------------------ */
.bevel-card,
/* Esta regla aplanaba TODAS las superficies con `background: var(--surface-2)`
   y pisaba el degradado original, que vive más arriba. Es el sitio donde se
   decide el fondo de una tarjeta, así que el degradado va aquí — parchearlo al
   final de la hoja dejaba dos fuentes de verdad peleándose. */
.service-card,
.local-card,
.lead-form,
.proof-grid article,
.process-list li,
.faq-list details,
.service-detail-card,
.image-requirements,
.feature-card,
.solution-card,
.callout,
.info-panel,
.contact-card {
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.16), rgba(1, 10, 18, 0.78)),
    var(--panel);
  border-color: var(--line);
}

/* El formulario sí se eleva: es la acción. Se eleva con MÁS luz en el canto,
   no aplanándolo — que era lo que hacía esta regla. */
.lead-form,
.info-panel {
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.22), rgba(1, 10, 18, 0.74)),
    var(--panel);
}

/* --- P1.3 · Fuera los halos de color -------------------------------------
   Un halo alrededor del botón que gana dinero es el recurso más barato que
   existe. Se sustituye por contraste y por un estado de hover real.

   Actualizado el 05/08/2026: el botón de acción SALE de esta regla. Vuelve a
   llevar halo, pero con un motivo que en julio no existía —ROT3X fabrica
   rótulos de neón y el halo enseña el producto— y con el detalle técnico
   resuelto: no se anima. Ver `--halo` en `:root`.

   `.social-link` se queda: ahí sí era decoración. */
.social-link {
  box-shadow: none;
}

/* --- P1.2 · Estados ------------------------------------------------------
   :focus-visible recibe el mismo trato que :hover — quien navega con teclado
   merece la misma señal que quien navega con ratón. */
.btn,
.btn-primary,
.btn-ghost,
.header-cta,
.nav-cta {
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.btn-primary:hover,
.btn-primary:focus-visible { background: var(--accion-alta); }

/* El hover del CTA de cabecera se declara UNA vez, más abajo, junto al resto
   de sus estilos. Aquí había un verde `#22e57e` que ya no se ve porque una
   regla posterior lo pisa: funcionaba por orden de cascada, que es frágil.
   Se retira en lugar de dejar dos verdades en la hoja. */

.btn-ghost:hover,
.btn-ghost:focus-visible {
  background: rgba(245, 251, 255, 0.06);
  border-color: var(--line-field);
}

.btn:active,
.btn-primary:active,
.btn-ghost:active,
.header-cta:active,
.nav-cta:active {
  transform: translateY(1px);
  transition-duration: 60ms;   /* una pulsación se siente, no se acompaña */
}

.text-link:hover,
.text-link:focus-visible {
  color: var(--white);
  text-decoration: underline;
  text-underline-offset: 0.3em;
}

.faq-list summary { transition: color var(--dur-fast) var(--ease); }
.faq-list summary:hover,
.faq-list summary:focus-visible { color: var(--estado); }

.bevel-card,
.service-card,
.feature-card,
.solution-card,
.proof-grid article,
.process-list li,
.faq-list details,
.contact-card,
.callout,
.image-requirements,
.service-detail-card,
.local-card {
  transition: border-color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}

/* Al pasar por encima, la tarjeta se ENCIENDE un punto: sube la luz del canto
   y se aclara el filete. Antes esta regla ponía un color plano, y eso mataba
   el degradado — la tarjeta se volvía gris justo al mirarla, que es lo
   contrario de lo que tiene que pasar. */
.bevel-card:hover,
.service-card:hover,
.feature-card:hover,
.solution-card:hover,
.proof-grid article:hover,
.process-list li:hover,
.faq-list details:hover,
.contact-card:hover,
.callout:hover,
.image-requirements:hover,
.service-detail-card:hover,
.local-card:hover {
  border-color: var(--line-strong);
  /* Solo cambia el COLOR DE BASE, no el degradado. Un `background-image` no se
     puede interpolar: si el hover cambiase el degradado, la tarjeta daría un
     salto seco en vez de encenderse. Dejando la capa de luz fija y animando el
     color que tiene debajo, la transición de 160 ms funciona de verdad. */
  background-color: rgba(24, 42, 58, 0.86);
}

/* Nunca escala: escalar una foto de obra física la desenfoca, y aquí la foto
   ES el producto.

   Antes oscurecía con `filter: brightness(0.78)`. Dos problemas: oscurecer al
   pasar por encima se lee como «desactivado», no como «pulsable» — justo lo
   contrario de lo que queremos decir—; y un filtro sobre una foto de 869×869
   repinta la capa entera en cada frame de la transición.

   Ahora la foto no se toca. Sube un velo desde abajo, que es el gesto de una
   etiqueta que va a aparecer: `opacity` sobre un pseudoelemento, que va al
   compositor. */
.media-card,
.bevel-card { position: relative; }

.media-card::after,
.bevel-card::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top, rgba(2, 5, 11, 0.55), transparent 52%);
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease);
}

/* `focus-within` no es un adorno: sin él, quien navega con teclado nunca ve
   la respuesta que sí ve quien usa ratón. */
.media-card:hover::after,
.media-card:focus-within::after,
.bevel-card:hover::after,
.bevel-card:focus-within::after { opacity: 1; }

:where(.btn, a, button)[aria-disabled="true"],
:where(.btn, a, button):disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* --- P1.4 · Tracking -----------------------------------------------------
   Estaba del revés: positivo en el display, que lo abre y lo debilita, y
   ninguno en la micro-etiqueta en caja alta, que se apelmaza. */
h1 { letter-spacing: -0.015em; text-wrap: balance; }
h2 { letter-spacing: -0.01em;  text-wrap: balance; }
h3 { text-wrap: balance; }
p  { text-wrap: pretty; }        /* evita viudas sin partir palabras */

.eyebrow {
  letter-spacing: 0.12em;        /* la regla `p, li` pisaba la herencia */
}

/* Los índices dejan de bailar */
.proof-grid strong,
.process-list span,
.feature-index { font-variant-numeric: tabular-nums; }

/* --- P1.5 · Capa decorativa invisible ------------------------------------
   `body::before/::after` montaban dos siluetas fijas de hasta 330×680 px al
   34 % de opacidad… en z-index -1, por detrás del degradado casi opaco de
   `.site-shell`. Medido: cambian el 0,1 % de los píxeles, con una diferencia
   máxima de 3 sobre 255. Son invisibles y cuestan pintado.
   `background-attachment: fixed` se retira además porque iOS no lo honra:
   el fondo se comportaba distinto en iPhone que en escritorio. */
body::before,
body::after { content: none; }

/* La corrección original solo alcanzaba a `.site-shell`. Quedaban tres capas
   más con `background-attachment: fixed` —el cajón del menú, los héroes y
   secciones oscuras, y el panel del menú de escritorio— por la misma razón:
   iOS Safari no honra `fixed`, así que el diseño no era el mismo para media
   audiencia, y cada capa fija obliga al navegador a repintar el fondo en cada
   frame de desplazamiento. */
.site-shell,
.nav-drawer,
.page-hero,
.section-dark,
.desktop-nav-panel { background-attachment: scroll, scroll; }

/* --- P1.6 · El radio, declarado ------------------------------------------ */
/* K4 · Los dos flotantes eran los ÚNICOS objetos redondeados de un sistema
   con `--radius: 0`, y son los únicos que están en pantalla el 100 % del
   tiempo en las 19 páginas. La excepción más visible del sitio contradecía la
   norma a tiempo completo. El círculo de WhatsApp se conserva porque es la
   forma reconocible de la marca —cambiarlo lo vuelve irreconocible—; la
   píldora del botón de llamada no defiende nada. */
.whatsapp-float { border-radius: 50%; }
.call-float     { border-radius: 0; }

/* --- Acabados ------------------------------------------------------------ */
::selection { background: var(--estado); color: var(--black); }

html { scrollbar-color: var(--line-mid) transparent; }

/* --- CORRECCIÓN · los flotantes se solapaban con el menú abierto ---------
   Medido a 375 px con el panel desplegado: el panel ocupa y:89-812 y los dos
   flotantes caen dentro, a y:742. Quedaban por detrás del panel y asomando
   por su borde. Con el menú abierto no pintan nada: se retiran. */
body.menu-open .floating-actions {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease),
              visibility 0s linear var(--dur-fast);
}

/* --- Enlaces dentro de texto corrido -------------------------------------
   Un enlace en medio de un párrafo no puede distinguirse solo por el color:
   el cian contra el gris del cuerpo da 1,16:1, muy por debajo del 3:1 que
   exige WCAG, y quien no distingue ese matiz no lo ve. El subrayado lo
   resuelve sin tocar la paleta (WCAG 1.4.1, «uso del color»). */
main p a:not(.btn):not(.btn-primary):not(.btn-ghost),
.form-note a,
.faq-list p a {
  text-decoration: underline;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
}

main p a:not(.btn):hover,
main p a:not(.btn):focus-visible,
.form-note a:hover,
.form-note a:focus-visible {
  color: var(--white);
  text-decoration-thickness: 2px;
}

/* --- Tarjetas de servicio: la foto ilustra, no es el portafolio ----------
   Al pasar a cuadrado y a dos columnas, la sección «Tres formas…» creció de
   895 a 1.986 px (+122 %) y la página entera un 33 %: la imagen empujaba el
   título y el texto fuera de pantalla. Aquí la fotografía acompaña a un
   servicio, no se expone; un 3/2 la mantiene reconocible sin comerse la
   sección. El criterio de «no recortar» sigue intacto donde importa: en la
   galería y en las muestras de trabajo. */
.priority-grid .service-card img,
.priority-grid .bevel-card img {
  aspect-ratio: 3 / 2;
  max-block-size: 340px;
  object-fit: cover;
  object-position: var(--encuadre-x, 50%) 45%;
}

/* --- El flotante de teléfono, idéntico en todos los anchos ----------------
   Historia de este bloque, porque ha ido y vuelto:

   Aquí vivía `@media (min-width: 1024px) { .call-float { display: none } }`.
   El motivo era real y está medido: a 1280 px la pestaña «Llamar ahora» —fija
   abajo a la izquierda, 150 px de ancho— se superponía a los titulares de
   sección al desplazarse.

   Al pedir el cliente que volviera en ordenador se probó llevarla a la
   esquina derecha, y después convertirla en un círculo con icono del tamaño
   del flotante de WhatsApp. Medido sobre 46 posiciones de desplazamiento en
   6 páginas a 1280 px:

     pastilla de texto a la derecha  → 18 elementos de texto, 3 titulares
     círculo con icono               →  4 elementos de texto, 0 titulares
     flotante de WhatsApp (control)  →  3 elementos de texto, 0 titulares

   El usuario ha decidido (07/09/2026) que el botón se quede IGUAL en todos
   los anchos: la misma pastilla con el texto «Llamar ahora», en el mismo
   sitio, sin icono. Es una decisión de negocio —un botón que dice lo que hace
   se pulsa más que un símbolo— y se respeta.

   Así que no hay bloque de escritorio: rige la regla base para todos los
   anchos, y el único ajuste que queda es el de tamaño por debajo de 720 px.
   El solape con titulares vuelve a existir en ordenador; queda escrito aquí
   para que nadie lo redescubra dentro de seis meses y lo trate como un fallo
   nuevo. La mitigación, si algún día se quiere, es dar al flotante el mismo
   velo oscuro que ya tiene y desplazar el contenido, no esconder el botón. */

/* El de WhatsApp se mantiene en todos los anchos: es la conversión principal
   del negocio. Se le da holgura para que no roce el borde ni el contenido. */
.whatsapp-float {
  inset-block-end: clamp(16px, 3vh, 28px);
  inset-inline-end: clamp(16px, 2vw, 28px);
}

/* --- Vista previa de trabajos: piezas del mismo tamaño --------------------
   La galería completa de `galeria.html` no recorta a propósito: es el
   archivo, y allí cada foto manda su proporción. Pero en la vista previa de
   la portada y de las páginas de servicio son TRES piezas en fila, y las
   alturas desiguales se leen como descuido, no como decisión editorial.

   Aquí sí se recorta, pero a CUADRADO, no a 4/3. Con esta fototeca —51 %
   verticales, proporción mediana 0,89— el 4/3 volvería a descartar el 44 %
   de cada foto vertical. El cuadrado es la caja que menos destruye: pérdida
   media del 23 % y solo 6 fotos con recorte grave, frente a las 29 del
   16/10 original. Se gana la uniformidad sin volver al problema de partida. */
.work-preview .gallery-grid,
.work-preview .gallery-grid.wide {
  display: grid;
  columns: auto;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
  gap: 16px;
  align-items: stretch;
}

.work-preview .gallery-grid > figure {
  display: flex;
  flex-direction: column;
  margin: 0;
  block-size: 100%;
}

.work-preview .gallery-grid img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 1 / 1;
  max-block-size: none;
  object-fit: cover;
  object-position: var(--encuadre-x, 50%) 45%;
}

/* El pie se pega abajo para que todas las tarjetas midan igual aunque el
   texto ocupe una o dos líneas. */
.work-preview .gallery-grid figcaption {
  margin-block-start: auto;
}

/* ===========================================================================
   OLA P2 — MOVIMIENTO · Xpandex, 2026-07-29
   Todo dentro de `@media (prefers-reduced-motion: no-preference)` salvo lo
   que ya está cubierto por la regla global ampliada. Cero librerías, cero
   peticiones nuevas, sin tocar la CSP.
   =========================================================================== */

/* --- Transiciones entre páginas ------------------------------------------
   Las 19 páginas comparten cabecera y pie idénticos, y hoy cada navegación
   es un parpadeo blanco con recarga completa del mismo marco. Un fundido de
   200 ms lo elimina. Cuesta 0,3 KB, no lleva JavaScript, y va en la hoja que
   comparten las 19: ni un solo HTML tocado.

   No es una función universal —Firefox aún no la soporta entre documentos—
   pero el fallo no existe: quien no la soporta navega como hoy. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }

  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 200ms; }

  /* Sin nombrarlos, la cabecera y los flotantes —que son idénticos en las dos
     páginas— entran en el fundido y parpadean, que es peor que no hacer nada.
     Con nombre propio el navegador los trata como persistentes: el marco se
     queda quieto y solo cambia el contenido. */
  .site-header    { view-transition-name: cabecera; }
  .whatsapp-float { view-transition-name: flotante-wa; }
  .call-float     { view-transition-name: flotante-tel; }
}

/* --- Preguntas frecuentes: apertura animada ------------------------------
   Sin una línea de JavaScript y sin tocar el HTML. Envuelto en @supports:
   donde no exista, la apertura es instantánea, exactamente como hoy. */
@supports (interpolate-size: allow-keywords) and (selector(::details-content)) {
  @media (prefers-reduced-motion: no-preference) {
    :root { interpolate-size: allow-keywords; }

    .faq-list details::details-content {
      block-size: 0;
      overflow: clip;
      transition: block-size var(--dur-slow) var(--ease),
                  content-visibility var(--dur-slow) allow-discrete;
    }
    .faq-list details[open]::details-content { block-size: auto; }
  }
}

/* --- Apariciones al scroll -----------------------------------------------
   El estado inicial SOLO existe si el JavaScript ha puesto la clase en <html>.
   Es la protección crítica de este patrón: si el JS falla, no carga, lo
   bloquea una extensión o es un rastreador quien mira, el `opacity: 0` nunca
   llega a aplicarse y la página se ve entera. Es el fallo clásico que deja
   páginas en blanco.

   Hecho con `transition` y no con `@keyframes`, así queda cubierto también
   por la regla global de movimiento reducido. */
.con-reveal [data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-slow) var(--ease),
              transform var(--dur-slow) var(--ease);
}

.con-reveal [data-reveal].es-visible {
  opacity: 1;
  transform: none;
}

/* Escalonado cíclico de cuatro en cuatro. La versión anterior solo retardaba
   los hijos 2, 3 y 4: en `.proof-grid`, que tiene cinco, el quinto entraba a
   la vez que el primero y la fila aterrizaba como «1 y 5 juntos, luego 2,
   luego 3, luego 4» — peor que no escalonar.

   El ciclo `4n+1…4n+4` cubre cualquier número de hijos, mantiene la espera
   máxima en 180 ms y, sobre todo, se reinicia solo cuando la rejilla envuelve
   a dos filas: el elemento que abre una fila nueva no arrastra el retardo de
   la anterior. */
.con-reveal [data-reveal-grupo] > [data-reveal].es-visible:nth-child(4n + 1) { transition-delay: 0ms; }
.con-reveal [data-reveal-grupo] > [data-reveal].es-visible:nth-child(4n + 2) { transition-delay: 60ms; }
.con-reveal [data-reveal-grupo] > [data-reveal].es-visible:nth-child(4n + 3) { transition-delay: 120ms; }
.con-reveal [data-reveal-grupo] > [data-reveal].es-visible:nth-child(4n + 4) { transition-delay: 180ms; }

/* El recorrido de los pasos numerados sí narra: entran en su orden y algo
   más separados, porque el número lleva la información de secuencia. */
.con-reveal .process-list > [data-reveal].es-visible:nth-child(2) { transition-delay: 90ms; }
.con-reveal .process-list > [data-reveal].es-visible:nth-child(3) { transition-delay: 180ms; }

/* ===========================================================================
   OLA P3 — COMPOSICIÓN · Xpandex, 2026-07-29
   =========================================================================== */

/* --- P3.11 · El héroe compone ---------------------------------------------
   Medido a 1440 px antes del cambio: héroe de 810 px de alto con la columna
   visual de 359 px flotando en el centro y unos 225 px de vacío arriba y
   abajo. Con `align-items: start` el texto y la imagen arrancan a la misma
   altura y el bloque deja de levitar.

   NO se toca el titular: es una cadena de palabras clave con valor SEO y su
   sustitución por una frase de marca necesita copy del cliente. */
@media (min-width: 1024px) {

  .hero-visual { margin-block-start: 0; }
}

/* --- P3.14 · Pie de foto sobre la imagen ----------------------------------
   Sacar el pie fuera de la foto rompía el rectángulo y añadía una franja de
   interfaz por cada imagen. Va encima, con velo y en tono apagado: la
   etiqueta deja de gritar más que la fotografía. */
.work-preview .gallery-grid figure,
.gallery-grid .media-card { position: relative; }

.work-preview .gallery-grid figcaption {
  position: absolute;
  inset: auto 0 0 0;
  margin: 0;
  padding: 42px 14px 12px;
  color: rgba(245, 251, 255, 0.88);      /* NO cian: el cian competía */
  font-size: 14px;
  letter-spacing: 0.04em;
  background: linear-gradient(to top, rgba(2, 5, 11, 0.82), transparent);
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease);
}

/* `focus-within` es imprescindible: sin él, quien navega con teclado nunca
   llega a ver el pie de foto. */
.work-preview .gallery-grid figure:hover figcaption,
.work-preview .gallery-grid figure:focus-within figcaption { opacity: 1; }

/* En táctil no hay hover: el pie se muestra siempre. */
@media (hover: none) {
  .work-preview .gallery-grid figcaption { opacity: 1; }
}

/* --- P3.15 · Escala tipográfica: de 14 tamaños a 8 ------------------------
   Convivían 13, 14, 15, 16, 18, 19, 20, 21, 22, 24, 28, 31, 42 y 56 px.
   Cinco valores entre 18 y 22 px son indistinguibles a ojo. El trío de
   display ya tenía ratio —56/42/31 ≈ 1,333, una cuarta justa—: se extiende
   al resto en lugar de inventar una escala nueva. */
:root {
  --t-micro: 12px;
  --t-sm:    14px;
  --t-body:  16px;
  --t-lead:  clamp(17px, 1.1vw + 13px, 21px);
  --t-h3:    clamp(21px, 1.6vw + 15px, 28px);
  --t-h2:    clamp(28px, 3.2vw + 16px, 42px);
  /* El tope de 76 px que proponía dirección de arte presupone un titular
     CORTO de marca. Con el titular actual —nueve palabras de cadena SEO—
     pasaba de 6 a 7 líneas y el héroe crecía a 1.001 px: peor, no mejor.
     Se mantiene el tope original hasta que exista el copy nuevo. Las dos
     piezas van acopladas y no se puede aplicar solo una. */
  --t-h1:    clamp(30px, 5vw + 8px, 56px);
}

h1 { font-size: var(--t-h1); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); }

.hero-lead,
.section-heading p { font-size: var(--t-lead); }

.eyebrow { font-size: var(--t-micro); }

.work-preview .gallery-grid figcaption,
.media-card figcaption,
.bevel-card figcaption { font-size: var(--t-sm); }

/* --- P3.13 · Carrusel de trabajos ----------------------------------------
   scroll-snap nativo, sin librería. El gesto ya es nativo en móvil —con su
   inercia y su rebote—, es accesible por defecto y el sitio ya usa este mismo
   patrón en su navegación por anclas. El JavaScript solo añade los botones:
   si fallara, el carrusel sigue funcionando entero. */
.carrusel { position: relative; }

.carrusel-pista {
  display: flex;
  gap: 16px;
  margin: 0;
  padding: 0 0 14px;
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;     /* que no arrastre la página al final */
  scroll-snap-type: x mandatory;
  scroll-padding-inline-start: 0;
  scrollbar-width: thin;              /* la barra ES el indicador de progreso */
  scrollbar-color: var(--line-strong) transparent;
}

/* El asomo es deliberado: en móvil siempre se ve un trozo de la pieza
   siguiente. Es la única señal fiable de que hay más a la derecha; un
   carrusel que encaja exactamente N piezas parece una rejilla y nadie
   desliza. */
.carrusel-item {
  flex: 0 0 clamp(240px, 74vw, 330px);
  scroll-snap-align: start;
}

.carrusel-item a { display: block; block-size: 100%; }

.carrusel-item figure {
  position: relative;
  margin: 0;
  background: var(--surface-2);
  border: 1px solid var(--line);
}

.carrusel-item img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: var(--encuadre-x, 50%) 42%;
}

/* --- El oscurecido al pasar por encima ------------------------------------
   Era `filter: brightness(0.78)` sobre la `<img>`. Un filtro obliga a
   REPINTAR la capa entera de una fotografía de 869×869 en cada fotograma de
   los 320 ms de la transición. Es el mismo defecto que la ola M0 ya corrigió
   en las tarjetas y que aquí se quedó sin corregir.

   Un velo en `::after` hace lo mismo a la vista y solo compone: `opacity` es
   una de las dos propiedades que el navegador puede animar sin tocar pintado
   ni maquetación. */
.carrusel-item figure::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: #020509;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-slow) var(--ease);
}

.carrusel-item a:hover figure::after,
.carrusel-item a:focus-visible figure::after { opacity: 0.22; }

/* El pie tiene que quedar POR ENCIMA del velo, o se oscurece con la foto. */
.carrusel-item figcaption { z-index: 2; }

.carrusel-item figcaption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 40px 12px 10px;
  color: rgba(245, 251, 255, 0.9);
  font-size: var(--t-sm);
  letter-spacing: 0.04em;
  background: linear-gradient(to top, rgba(2, 5, 11, 0.84), transparent);
}

.carrusel-item a:hover figure,
.carrusel-item a:focus-visible figure { border-color: var(--line-strong); }

/* Estos botones llevaban `aria-hidden="true"` y `tabindex="-1"`, con el
   argumento de que «duplican una navegación que ya existe (tabulador, rueda,
   gesto)». El argumento no se sostiene: la pista es un contenedor de scroll y
   Chrome y Firefox la hacen enfocable, pero **Safari no**, y en ningún
   navegador tiene nombre accesible ni indicador de foco propio. Es decir: diez
   trabajos accesibles solo con el dedo o la rueda del ratón.

   Ahora son botones normales, con nombre. En táctil siguen sin aparecer —ahí
   el gesto de deslizar es nativo y no hace falta el duplicado—, y esa parte
   del razonamiento original sí era correcta. */
.carrusel-controles { display: none; }

@media (hover: hover) and (min-width: 720px) {
  .carrusel-controles {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-block-start: 12px;
  }

  .carrusel-btn {
    inline-size: 44px;
    block-size: 44px;
    display: grid;
    place-items: center;
    font-size: 24px;
    line-height: 1;
    color: var(--white);
    background: var(--surface-2);
    border: 1px solid var(--line);
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
  }

  .carrusel-btn:hover:not(:disabled) {
    background-color: rgba(24, 42, 58, 0.86);
    border-color: var(--line-strong);
  }

  .carrusel-btn:disabled { opacity: 0.35; cursor: default; }
}

/* --- Índice del catálogo (mas-soluciones.html) ---------------------------
   Los 44 servicios que vivían replicados en el menú de las 19 páginas. Aquí
   están en el cuerpo de una página hub, que es donde los buscadores sí los
   cuentan: un enlace en marcado repetido se descuenta. */
.catalogo-grid {
  display: grid;
  gap: clamp(16px, 2vw, 28px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.catalogo-familia {
  padding: 20px;
  background: var(--surface-2);
  border: 1px solid var(--line);
}

.catalogo-familia h3 {
  margin-block-end: 12px;
  padding-block-end: 10px;
  border-block-end: 1px solid var(--line-mid);
  font-size: var(--t-h3);
}

.catalogo-familia h3 a { color: var(--estado); }

.catalogo-familia ul {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.catalogo-familia li a {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;          /* objetivo táctil, WCAG 2.5.8 */
  color: var(--muted);
  font-family: var(--body-font);
  font-size: var(--t-body);
  transition: color var(--dur-fast) var(--ease);
}

.catalogo-familia li a:hover,
.catalogo-familia li a:focus-visible { color: var(--white); }

/* --- Navegación simplificada -------------------------------------------- */
.menu-column--servicios {
  display: grid;
  gap: 4px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}

.menu-index-link {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  margin-block-start: 12px;
  padding-block-start: 12px;
  border-block-start: 1px solid var(--line);
  color: var(--estado);
  font-size: var(--t-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* --- El verde se reserva para WhatsApp -----------------------------------
   El CTA de cabecera era verde cuando salía directo a `wa.me`: el color
   anunciaba el canal. Ahora lleva a `presupuesto.html`, así que el verde
   engaña — y además había DOS superficies verdes a la vez en pantalla, la
   del CTA y la del flotante, que no se leen como énfasis sino como
   duplicado.

   El CTA pasa al naranja de acción, el mismo del botón primario, y el verde
   queda como identificador exclusivo de WhatsApp. Un color, un significado. */
.header-cta,
.nav-cta {
  /* `background` vivía aquí en plano; lo sustituye el degradado del botón
     encendido, que se declara más abajo para `.btn-primary` y este a la vez. */
  color: var(--black);
  box-shadow: var(--halo);
}

.header-cta:hover,
.header-cta:focus-visible {
  background: var(--accion-alta);
}

/* --- El panel de Servicios, al tamaño de su contenido ---------------------
   El JavaScript dimensiona los paneles a 1000 px porque el menú original
   tenía cuatro columnas de sublistas. Con ocho enlaces, ese ancho deja el
   panel medio vacío: es el «mueble más grande que el contenido». Se ajusta a
   dos columnas y al ancho que realmente ocupa.

   El !important es necesario y deliberado: la anchura la fija el JS como
   propiedad personalizada en línea, y una regla normal no la alcanza. */
.desktop-nav-panel {
  grid-template-columns: 1fr;
  inline-size: min(560px, calc(100vw - 48px)) !important;
}

.menu-column--servicios {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px 20px;
}

.desktop-nav-panel .menu-index-link {
  justify-self: start;
}

/* --- Filtros de la página de trabajos ------------------------------------
   Mejora progresiva: la barra nace con `hidden` en el HTML y solo la muestra
   el JavaScript. Sin JS se ven las 17 piezas y no hay un control muerto en
   pantalla; el filtro nunca es la única vía de acceso al contenido. */
.filtros-trabajos {
  padding: 0 18px;
  margin-block: 8px 4px;
}

.filtros-lista {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.filtro-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-block-size: 44px;              /* objetivo táctil, WCAG 2.5.8 */
  padding: 0 16px;
  color: var(--white);
  background: var(--surface-2);
  border: 1px solid var(--line);
  font-family: var(--ui-font);
  font-size: 20px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.filtro-btn:hover,
.filtro-btn:focus-visible {
  background: var(--surface-3);
  border-color: var(--line-strong);
}

/* El estado activo NO se marca solo con color: lleva también borde e
   inversión de fondo, para que se distinga sin depender de percibir el
   matiz (WCAG 1.4.1). */
.filtro-btn.is-activo {
  color: var(--black);
  background: var(--estado);
  border-color: var(--estado);
}

.filtro-num {
  font-family: var(--body-font);
  font-size: 14px;
  letter-spacing: 0;
  opacity: 0.72;
}

.filtro-btn.is-activo .filtro-num { opacity: 0.62; }

.filtros-estado {
  margin-block: 10px 0;
  color: var(--muted);
  font-family: var(--body-font);
  font-size: var(--t-sm);
}

/* Las piezas ocultas salen del flujo y del recorrido de teclado */
.work-preview [data-categoria][hidden] { display: none !important; }

/* Una sección sin resultados se oculta entera: sin ella quedaría el titular
   flotando sobre una rejilla vacía. */
.work-preview.sin-resultados { display: none; }

/* --- Visor de fotografías ------------------------------------------------
   Complementa el recorte cuadrado de las miniaturas: la rejilla ordena, el
   visor enseña la foto entera. Es la pieza que faltaba para poder recortar
   sin perder información. */
.visor {
  opacity: 1;
  position: fixed;
  inset: 0;
  z-index: 4000;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: clamp(8px, 2vw, 24px);
  padding: clamp(16px, 4vw, 48px);
  background: rgba(2, 5, 11, 0.94);
  backdrop-filter: blur(6px);
}

/* --- El visor deja de aparecer de un corte -------------------------------
   Era el momento más teatral del sitio —una fotografía de obra a pantalla
   completa— y llegaba de golpe, con un `display: none` que se apaga.

   ⚠️ El `backdrop-filter: blur(6px)` NO se anima. Se aplica ya a su valor
   final y solo se desvanece la opacidad del conjunto. Animar el radio de un
   desenfoque a pantalla completa es de las formas más rápidas de bajar a 20
   fps en un Android de gama media. */
.visor[hidden] {
  display: none;
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .visor {
    transition: opacity var(--dur-visor) var(--ease),
                display  var(--dur-visor) allow-discrete;
  }

  .visor[hidden] {
    transition: opacity 160ms var(--ease-salida),
                display  160ms allow-discrete;
  }

  /* Sin esto, un elemento que pasa de `display: none` a visible no tiene
     desde dónde interpolar y aparecería de golpe: exactamente lo que
     queremos quitar. Donde no exista, ocurre eso — degradación aceptable. */
  @starting-style {
    .visor:not([hidden]) { opacity: 0; }
  }

  .visor:not([hidden]) .visor-figura {
    animation: visorEntra var(--dur-visor) var(--ease) both;
  }

  @keyframes visorEntra {
    from { opacity: 0; transform: scale(0.96); }
    to   { opacity: 1; transform: none; }
  }

  .visor:not([hidden]) .visor-cerrar,
  .visor:not([hidden]) .visor-nav,
  .visor:not([hidden]) .visor-contador {
    animation: visorEntra 200ms var(--ease) 80ms backwards;
  }
}

body.visor-abierto { overflow: hidden; }   /* el fondo no se desplaza detrás */

.visor-figura {
  grid-column: 2;
  display: grid;
  gap: 12px;
  justify-items: center;
  margin: 0;
  min-inline-size: 0;
}

/* La foto se ve ENTERA: `contain` y no `cover`. Es todo el sentido del visor. */
.visor-img {
  max-inline-size: 100%;
  max-block-size: 78vh;
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
  border: 1px solid var(--line);
}

.visor-pie {
  color: rgba(245, 251, 255, 0.9);
  font-family: var(--body-font);
  font-size: var(--t-body);
  text-align: center;
  max-inline-size: 60ch;
}

.visor-contador {
  position: absolute;
  inset-block-end: clamp(12px, 3vw, 28px);
  inset-inline: 0;
  margin: 0;
  color: var(--muted);
  font-family: var(--body-font);
  font-size: var(--t-sm);
  text-align: center;
}

.visor-cerrar,
.visor-nav {
  display: grid;
  place-items: center;
  inline-size: 48px;
  block-size: 48px;
  color: var(--white);
  background: var(--surface-2);
  border: 1px solid var(--line);
  font-size: 28px;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}

.visor-cerrar:hover,
.visor-nav:hover,
.visor-cerrar:focus-visible,
.visor-nav:focus-visible {
  background: var(--surface-3);
  border-color: var(--line-strong);
}

.visor-cerrar {
  position: absolute;
  inset-block-start: clamp(12px, 3vw, 28px);
  inset-inline-end: clamp(12px, 3vw, 28px);
  font-size: 32px;
}

.visor-prev { grid-column: 1; }
.visor-next { grid-column: 3; }
.visor-nav[hidden] { display: none; }

/* En móvil los controles laterales quitarían ancho a la foto: se apilan
   debajo, donde el pulgar llega. */
@media (max-width: 640px) {
  .visor { grid-template-columns: 1fr; }
  .visor-figura { grid-column: 1; }
  .visor-prev,
  .visor-next {
    position: absolute;
    inset-block-end: clamp(48px, 12vw, 72px);
  }
  .visor-prev { inset-inline-start: 16px; }
  .visor-next { inset-inline-end: 16px; }
}

/* El disparador cubre la miniatura sin añadir un nodo visible. La figura ya
   es `position: relative` por el pie con velo. */
.visor-abrir {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  padding: 0;
  background: none;
  border: 0;
  cursor: zoom-in;
}

.visor-abrir:focus-visible {
  outline: 3px solid var(--estado);
  outline-offset: -3px;
}

.work-preview figure[data-categoria] { position: relative; }

/* --- P17 · Encuadrar por legibilidad del rótulo --------------------------
   ROT3X no vende fotografías de locales: vende letras que se leen desde la
   acera. Un rótulo cortado a media palabra no demuestra el oficio, lo
   desmiente. La métrica correcta aquí no es «cuántos píxeles conservo» —esa
   era la de P15, que hizo bien su trabajo— sino «¿se lee el nombre?».

   El eje horizontal de cada pieza se declara aparte, en `--encuadre-x`, y el
   vertical lo sigue poniendo el contexto (rejilla, carrusel, tarjeta). Así una
   misma foto se encuadra bien en todos los sitios sin duplicar reglas.

   Los valores NO son estéticos: salen de medir dónde empieza y acaba el texto
   en el archivo original.

   Bar Malandrín · 890×846 en ventana cuadrada de 846 → sobran 44 px de ancho.
   El rótulo va de x≈45 («MALANDRÍN») a x≈886 («…NATURALEZA»), es decir 841 px:
   cabe entero en los 846, pero solo si la ventana se corre a la derecha. Al
   50 % se perdían los 22 px finales y «NATURALEZA» quedaba en «NATURALE».
   Al 95 % la ventana va de 41,8 a 887,8 y entran las dos puntas. */
img[src$="letras-corporeas-bar-malandrin.webp"] { --encuadre-x: 95%; }

/* Clínica Sanve · 931×787 en ventana cuadrada de 787 → sobran 144 px.
   Aquí NO caben las dos cosas: el rótulo ocupa de x≈55 a 690 y el logotipo
   «SV» de 745 a 880; juntos son 825 px y la ventana mide 787. Se prioriza el
   nombre del cliente, que es el criterio de aceptación de P17. Alineando a la
   izquierda el rótulo entra completo; el logotipo queda fuera casi entero, que
   es mejor que la mitad de un logotipo partida por el borde. */
img[src$="rotulos-fachadas-clinica-sanve-sevilla.webp"] { --encuadre-x: 0%; }


/* ===========================================================================
   ENTRADA DEL HÉROE — Xpandex, 2026-08-05  (ola M1 de context/motion-plan.md)
   ---------------------------------------------------------------------------
   Hasta hoy el héroe estaba inmóvil en las 19 páginas: la web aparecía de
   golpe, completa y quieta. Era el motivo nº 1 de que se sintiera plana, y los
   primeros 600 ms son toda la primera impresión.

   Se resuelve SOLO con CSS, sin una línea de JavaScript. Y no es casualidad:
   `styles.css` bloquea el pintado, así que el estado inicial ya está aplicado
   en el primer frame. Hacerlo con JS provocaría el parpadeo clásico —contenido
   visible que desaparece al aplicarse la clase— porque `script.js` va con
   `defer` y llega cuando la página ya está pintada.

   ⚠️ LA RESTRICCIÓN QUE NO SE PUEDE TOCAR
   El elemento LCP de la portada es el `<h1>`, y Chrome EXCLUYE del cálculo de
   LCP los elementos con opacidad 0. Un desvanecido con retardo sobre el
   titular retrasaría la métrica exactamente ese retardo — es la forma habitual
   de tirar un Lighthouse de 100 «haciendo la web premium».
   Por eso el `h1` y las imágenes del héroe entran SOLO con `transform`, sin
   `opacity` y sin retardo. Si alguien añade `opacity` ahí, el LCP se degrada
   al instante. Medido antes y después: sin cambio.

   `backwards` en el fill-mode es obligatorio en todo lo que lleva retardo: sin
   él, el elemento se pinta visible y salta a su estado inicial al arrancar.

   Todo vive dentro de `prefers-reduced-motion: no-preference`, así que para
   quien pide menos movimiento la animación no existe — no es «dura 0 ms».
   =========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  /* --- Partituras ------------------------------------------------------- */

  /* Lo normal: sube y aparece. */
  @keyframes heroEntra {
    from { opacity: 0; transform: translateY(var(--entra-y, 10px)); }
    to   { opacity: 1; transform: none; }
  }

  /* Para candidatos a LCP: se mueve, NO se desvanece. */
  @keyframes heroMueve {
    from { transform: translateY(var(--entra-y, 14px)); }
    to   { transform: none; }
  }

  /* La fotografía se asienta: un punto de escala, nada más. Escalar más
     desenfoca una foto de obra física, que es justo lo que aquí se vende. */
  @keyframes heroAsienta {
    from { transform: translateY(10px) scale(1.025); }
    to   { transform: none; }
  }

  /* --- Portada ---------------------------------------------------------- */

  .hero .eyebrow {
    --entra-y: 8px;
    animation: heroEntra 320ms var(--ease) var(--hero-espera) both;
  }

  .hero h1 {                                   /* 🔒 LCP: sin opacity, sin retardo */
    --entra-y: 14px;
    animation: heroMueve 420ms var(--ease) var(--hero-espera) both;
  }

  /* 🔒 Sin `opacity`. En móvil, con el titular más pequeño y las fotos del
     collage repartidas, la ENTRADILLA pasa a ser el elemento más grande de la
     pantalla — y por tanto el LCP. Al entrar con desvanecido, Chrome no la
     contaba hasta terminar el fundido y el LCP se iba a ~500 ms. Solo
     `transform`: medido, vuelve a 40-90 ms.
     Es la misma trampa que ya estaba documentada para el `h1`; lo que cambió
     es cuál de los dos es el elemento más grande. */
  .hero .hero-lead {
    --entra-y: 10px;
    animation: heroMueve 320ms var(--ease) calc(var(--hero-espera) + 90ms) backwards;
  }

  .hero .cta-row .btn {
    animation: heroEntra 300ms var(--ease) calc(var(--hero-espera) + 160ms) backwards;
  }
  .hero .cta-row .btn:nth-child(2) { animation-delay: calc(var(--hero-espera) + 220ms); }

  .hero .trust-strip li {
    --entra-y: 6px;
    animation: heroEntra 260ms var(--ease) calc(var(--hero-espera) + 280ms) backwards;
  }
  .hero .trust-strip li:nth-child(2) { animation-delay: calc(var(--hero-espera) + 330ms); }
  .hero .trust-strip li:nth-child(3) { animation-delay: calc(var(--hero-espera) + 380ms); }

  .hero .hero-visual {                         /* 🔒 contiene la imagen fetchpriority=high */
    animation: heroAsienta 560ms var(--ease) calc(var(--hero-espera) + 60ms) both;
  }

  .hero .hero-visual figcaption {
    --entra-y: 8px;
    animation: heroEntra 340ms var(--ease) calc(var(--hero-espera) + 420ms) backwards;
  }

  /* --- Páginas de servicio (12) ----------------------------------------- */
  /* Misma partitura, comprimida. La plantilla de servicio es la mejor pieza
     del sitio según la auditoría UX/UI: no necesita adorno. */

  .service-hero .breadcrumbs {
    --entra-y: 0px;
    animation: heroEntra 240ms var(--ease) both;
  }

  .service-hero .eyebrow {
    --entra-y: 8px;
    animation: heroEntra 300ms var(--ease) 60ms backwards;
  }

  .service-hero h1 {                           /* 🔒 candidato a LCP */
    --entra-y: 12px;
    animation: heroMueve 400ms var(--ease) both;
  }

  .service-hero .hero-lead {
    --entra-y: 8px;
    animation: heroEntra 300ms var(--ease) 80ms backwards;
  }

  .service-hero .cta-row .btn {
    --entra-y: 8px;
    animation: heroEntra 300ms var(--ease) 140ms backwards;
  }
  .service-hero .cta-row .btn:nth-child(2) { animation-delay: 200ms; }

  .service-hero > figure {                     /* 🔒 imagen principal */
    animation: heroAsienta 520ms var(--ease) 40ms both;
  }

  /* --- Resto de páginas (6): empresa, soluciones, presupuesto, legales --- */
  /* Entrada mínima. En `presupuesto` el formulario está sobre el pliegue y no
     debe hacerse esperar: 120 ms y dentro. */

  .page-hero h1 {                              /* 🔒 candidato a LCP */
    --entra-y: 12px;
    animation: heroMueve 400ms var(--ease) both;
  }

  .page-hero .eyebrow,
  .page-hero .hero-lead,
  .page-hero > p {
    --entra-y: 8px;
    animation: heroEntra 300ms var(--ease) 80ms backwards;
  }

  .page-hero .budget-form {
    --entra-y: 12px;
    animation: heroEntra 320ms var(--ease) 120ms backwards;
  }
}

/* Alto de la cabecera por punto de ruptura. Medido: cambia a 440, 720 y 1100. */
@media (min-width: 440px)  { :root { --header-height:  97px; } }
@media (min-width: 720px)  { :root { --header-height: 103px; } }
@media (min-width: 1100px) { :root { --header-height: 118px; } }

/* ===========================================================================
   ESTADOS DEL FORMULARIO — Xpandex, 2026-08-05  (ola M2)
   ---------------------------------------------------------------------------
   El formulario respondía, pero sin decirlo: el botón cambiaba de texto y
   aparecía un mensaje, correcto y accesible, pero mudo.

   NO hay sacudida en el error. Un temblor horizontal es un tic nervioso, no
   una señal premium, y es un disparador vestibular gratuito. El mensaje ya
   lleva `role="status"` y `aria-live="polite"` en el marcado: lo que faltaba
   era que se viera llegar.
   =========================================================================== */

/* `:user-invalid` avisa al SALIR del campo, no mientras se escribe. Es la
   diferencia entre corregir a alguien cuando ha terminado la frase y hacerlo
   a media palabra. Sin una línea de JavaScript. */
.budget-form input:user-invalid,
.budget-form select:user-invalid,
.budget-form textarea:user-invalid {
  border-color: #ff835f;   /* 7,06:1 sobre el panel */
}

@media (prefers-reduced-motion: no-preference) {
  .budget-form input,
  .budget-form select,
  .budget-form textarea {
    transition: border-color var(--dur-fast) var(--ease);
  }

  /* El mensaje entra en lugar de aparecer. `@starting-style` da el estado de
     partida sin necesidad de una clase que poner y quitar desde JS. */
  .budget-form-status.is-success,
  .budget-form-status.is-error {
    transition: opacity var(--dur-slow) var(--ease),
                transform var(--dur-slow) var(--ease);
  }

  @starting-style {
    .budget-form-status.is-success,
    .budget-form-status.is-error {
      opacity: 0;
      transform: translateY(-6px);
    }
  }

  /* Mientras se envía, una barra indeterminada recorre el botón. Es
     `transform` sobre un pseudoelemento: compositor puro, sin repintado.
     Solo aparece con el botón deshabilitado, que es exactamente el rato que
     dura el envío. */
  .budget-form [data-budget-submit] {
    position: relative;
    overflow: hidden;
  }

  .budget-form [data-budget-submit]:disabled::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    block-size: 2px;
    background: var(--black);
    opacity: 0.45;
    animation: barraEnvio 1100ms linear infinite;
  }

  @keyframes barraEnvio {
    from { transform: translateX(-100%); }
    to   { transform: translateX(100%); }
  }
}

/* --- El morfismo del visor ------------------------------------------------
   `script.js` mueve `view-transition-name: foto-visor` entre la miniatura y la
   foto grande. Aquí solo se le pone tiempo y curva.

   Va bajo `@supports` porque `::view-transition-group` no existe en todos los
   navegadores; donde no exista, queda la capa de arriba, que ya es decente. */
@supports selector(::view-transition-group(*)) {
  @media (prefers-reduced-motion: no-preference) {
    ::view-transition-group(foto-visor) {
      animation-duration: 300ms;
      animation-timing-function: var(--ease);
    }

    /* La raíz no se anima: solo se mueve la fotografía. Un fundido de toda la
       página al abrir una foto es ruido, y además cuesta un fotograma
       completo de composición. */
    ::view-transition-old(root),
    ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
  }
}

/* ===========================================================================
   ENCENDIDO DE MARCA — Xpandex, 2026-08-05
   ---------------------------------------------------------------------------
   NO es una pantalla de carga. Un splash que retiene el contenido mientras
   «carga» retrasa el LCP, entra en la definición de interstitial que Google
   penaliza, y en una web cuyo único trabajo es llevar a WhatsApp cuesta
   conversión. Nada de eso se hace aquí.

   Lo que se hace: el logotipo se enciende como un tubo de neón sobre una web
   que YA está pintada debajo. Es el mismo argumento que justificó el halo del
   botón — ROT3X fabrica rótulos de neón, así que encender es enseñar el
   producto, no adornar.

   Las cinco condiciones que lo hacen aceptable:
     1. Lo crea el JavaScript. Sin JS no existe: es imposible que un fallo deje
        la web tapada.
     2. Una vez por sesión. Quien vuelve, o quien llega desde Google a una
        página interna, no lo ve nunca.
     3. No aparece si la URL trae un ancla: esa persona ha pedido un sitio
        concreto y no se la entretiene.
     4. Se salta con un clic o con cualquier tecla.
     5. Con `prefers-reduced-motion` no se monta siquiera.

   Red de seguridad: el fotograma final del overlay es `opacity: 0` con
   `pointer-events: none` y `forwards`. Aunque el JavaScript que lo retira
   fallara, la web queda utilizable igual.

   No lleva foco ni texto: `aria-hidden` y sin nada enfocable dentro, así que
   ni atrapa el tabulador ni le habla a un lector de pantalla. El contenido
   real está debajo desde el primer byte, así que para un rastreador esto no
   existe.
   =========================================================================== */

.encendido {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: grid;
  place-items: center;
  background: var(--black);
  pointer-events: auto;
  animation: encendidoSale 320ms var(--ease-salida) 700ms forwards;
}

.encendido-marca {
  position: relative;
  inline-size: min(52vw, 320px);
  aspect-ratio: 646.72 / 436;
}

.encendido-marca img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;
}

/* La capa base parpadea, como un tubo que arranca. */
.encendido-base { animation: tuboArranca 620ms steps(1, end) both; }

/* El resplandor es una SEGUNDA copia con el filtro ya aplicado a su valor
   final: se anima su `opacity`, nunca el radio del desenfoque. Animar el
   radio de un `drop-shadow` es repintar el logotipo en cada fotograma. */
.encendido-halo {
  filter: drop-shadow(0 0 6px rgba(34, 211, 255, 0.9))
          drop-shadow(0 0 22px rgba(0, 200, 255, 0.65))
          drop-shadow(0 0 54px rgba(0, 160, 255, 0.4));
  opacity: 0;
  animation: haloSube 620ms var(--ease) both;
}

/* El parpadeo no es aleatorio: es la secuencia de un tubo de neón real —
   un destello corto, se apaga, dos golpes y estabiliza. `steps(1, end)`
   mantiene cada valor hasta el siguiente, que es como conmuta un tubo: no
   hay desvanecido entre estados. */
@keyframes tuboArranca {
    0% { opacity: 0; }
   14% { opacity: 0.85; }
   20% { opacity: 0.05; }
   30% { opacity: 0.95; }
   36% { opacity: 0.15; }
   44% { opacity: 1; }
   50% { opacity: 0.4; }
   58% { opacity: 1; }
  100% { opacity: 1; }
}

@keyframes haloSube {
    0% { opacity: 0; }
   44% { opacity: 0.35; }
   58% { opacity: 1; }
  100% { opacity: 1; }
}

/* Al salir, el resplandor florece un punto: el tubo se queda encendido y lo
   que se va es el telón, no la luz. */
@keyframes encendidoSale {
  from { opacity: 1; }
  to   { opacity: 0; pointer-events: none; visibility: hidden; }
}

/* Saltado a mano: fuera de inmediato, sin hacer esperar a nadie. */
.encendido.se-va {
  animation: encendidoSale 180ms var(--ease-salida) forwards;
}

/* Si alguien pide menos movimiento nunca se monta —lo decide el JavaScript—,
   pero si llegara a montarse, desaparece sin animación. */
@media (prefers-reduced-motion: reduce) {
  .encendido { display: none; }
}

/* ===========================================================================
   LIGADAS AL SCROLL — Xpandex, 2026-08-05  (ola M4 de context/motion-plan.md)
   ---------------------------------------------------------------------------
   Las tres se ejecutan EN EL COMPOSITOR: coste en el hilo principal, 0 ms. No
   hay un solo listener de scroll en ninguna.

   Doble puerta obligatoria. `@supports` porque `animation-timeline` no existe
   en todos los navegadores, y `prefers-reduced-motion` porque —ojo con esto—
   la regla global de movimiento reducido pone `animation-duration: 0.01ms`, y
   eso NO neutraliza una animación ligada al scroll: su duración la marca el
   recorrido, no el reloj. Sin este `@media` seguirían corriendo.
   =========================================================================== */

@supports ((animation-timeline: scroll()) and (animation-timeline: view())) {
  @media (prefers-reduced-motion: no-preference) {

    /* --- Progreso de lectura ------------------------------------------
       No es decorativo. Las páginas de servicio miden hasta 16.900 px en un
       teléfono; esta barra dice cuánto queda. Va colgada de la barra de
       anclas, que ya es pegajosa, así que no añade ni un elemento al
       documento. */
    .anchor-nav::after {
      animation: leerProgreso linear;
      animation-timeline: scroll(root block);
    }

    @keyframes leerProgreso {
      to { transform: scaleX(1); }
    }

    /* El parallax del mosaico del héroe vivía aquí. Se retira: `.hero-mosaic`
       no existe en ninguna de las 19 páginas —el héroe se rehízo como muro y
       el mosaico se quedó por el camino—, así que era una regla que el
       navegador parseaba en cada carga sin poder aplicarla nunca. */
  }
}

/* --- La barra de progreso, siempre presente ------------------------------
   V6 · Antes vivía ENTERA dentro del `@supports` de scroll-driven, así que en
   Firefox —que no lo soporta en estable— las tres páginas de 11 a 21 pantallas
   se recorrían sin ninguna señal de cuánto quedaba. En una página decorativa
   daría igual; en un documento de 16.791 px la barra es funcional.

   Ahora la barra se declara aquí, fuera de toda condición, y se mueve con la
   variable `--progreso`. Donde hay scroll-driven, el bloque de arriba la
   sustituye por la animación nativa y `script.js` no llega a tocarla. */
.anchor-nav::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: -1px;
  block-size: 2px;
  background: var(--accion);
  transform-origin: left;
  transform: scaleX(var(--progreso, 0));
}

.anchor-nav { position: sticky; }   /* refuerzo: el ::after se ancla a ella */

/* ===========================================================================
   TARJETAS — marca de corte
   ---------------------------------------------------------------------------
   El acento de esquina es una MARCA DE CORTE: las dos líneas en ángulo que un
   rotulista traza en el vinilo antes de pasar la cuchilla. No es un adorno
   genérico, es el oficio del cliente puesto en la interfaz.

   Neutro, no de color: el azul es la acción y el blanco pleno es el estado.
   Una tarjeta no es ninguna de las dos cosas.
   =========================================================================== */
.service-card,
.feature-card,
.solution-card,
.proof-grid article {
  position: relative;
}

.service-card::before,
.feature-card::before,
.solution-card::before,
.proof-grid article::before {
  content: "";
  position: absolute;
  inset-block-start: 10px;
  inset-inline-end: 10px;
  inline-size: 14px;
  block-size: 14px;
  border-block-start: 2px solid var(--line-strong);
  border-inline-end: 2px solid var(--line-strong);
  opacity: 0;
  transform: scale(0.7);
  transform-origin: top right;
  transition: opacity var(--dur-fast) var(--ease-corte),
              transform var(--dur-fast) var(--ease-corte);
  pointer-events: none;
}

.service-card:hover::before,
.service-card:focus-within::before,
.feature-card:hover::before,
.feature-card:focus-within::before,
.solution-card:hover::before,
.solution-card:focus-within::before,
.proof-grid article:hover::before,
.proof-grid article:focus-within::before {
  opacity: 1;
  transform: scale(1);
}

/* ===========================================================================
   RÓTULO LED DE MENSAJE VARIABLE — Xpandex, 2026-08-05
   ---------------------------------------------------------------------------
   Un marquee es, casi siempre, decoración barata. Aquí no, por una razón
   concreta: ROT3X fabrica rótulos LED de mensaje variable. La tira es una
   demostración del producto, igual que el halo del botón y el encendido del
   logotipo. Y el contenido es honesto —los nombres de sus propios servicios,
   ya publicados en la web—, no una lista de clientes que la web no declara.

   Tres condiciones que lo hacen aceptable:
     1. `prefers-reduced-motion` lo deja QUIETO y legible, no oculto. El texto
        es contenido; lo que sobra es el movimiento.
     2. Se para al pasar por encima y al recibir el foco, para poder leerlo.
     3. Solo se anima `transform`: compositor puro, sin repintado. El texto
        duplicado lleva `aria-hidden`, así que un lector de pantalla lo lee
        una vez, no dos.
   =========================================================================== */

.rotulo-led {
  overflow: hidden;
  padding-block: 18px;
  border-block: 1px solid var(--line);
  background:
    linear-gradient(180deg, rgba(54, 164, 222, 0.10), rgba(1, 10, 18, 0.6)),
    var(--surface-1);
  /* Los bordes se desvanecen: la tira no "empieza", entra en cuadro. */
  mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}

.rotulo-led-pista {
  display: flex;
  inline-size: max-content;
}

.rotulo-led-tira {
  display: flex;
  align-items: center;
  gap: 44px;
  padding-inline-end: 44px;
}

.rotulo-led-tira span {
  font-family: var(--ui-font);
  font-size: 21px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(245, 251, 255, 0.62);
  white-space: nowrap;
}

/* El separador entre servicios es un punto, no un icono: la tira tiene que
   leerse como una secuencia de mensajes, no como una lista con viñetas. */
.rotulo-led-tira span::after {
  content: "·";
  margin-inline-start: 44px;
  color: var(--accion);
}

@media (prefers-reduced-motion: no-preference) {
  .rotulo-led-pista {
    animation: rotuloDesfila 46s linear infinite;
  }

  /* Se detiene para poder leerlo. `focus-within` incluido: quien tabula por la
     página también necesita que pare. */
  .rotulo-led:hover .rotulo-led-pista,
  .rotulo-led:focus-within .rotulo-led-pista {
    animation-play-state: paused;
  }

  /* --- WCAG 2.2.2 (Nivel A) -----------------------------------------------
     Lo de arriba NO cumplía el criterio, y estaba publicado. La tira desfila
     46 s en bucle infinito, arranca sola y se presenta junto a otro contenido;
     2.2.2 exige un mecanismo para pausarla, detenerla u ocultarla. El único
     declarado era `:hover` —que en táctil no existe— y `:focus-within`, que
     es INALCANZABLE: la tira contiene 28 `<span>` y ni un solo elemento
     enfocable. El comentario de arriba describía una intención que el marcado
     no cumplía. `axe` no puede detectarlo, por eso las 19 páginas seguían
     dando 0 violaciones.

     El interruptor lo pone `script.js` y escribe esta bandera en la raíz.
     Se declara aquí, dentro del bloque de `no-preference`, porque solo tiene
     sentido donde hay movimiento que detener. */
  :root[data-movimiento="off"] .rotulo-led-pista {
    animation-play-state: paused;
  }

  /* -50 % es exactamente una copia de la tira: el bucle no tiene costura. */
  @keyframes rotuloDesfila {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
  }
}

/* --- El interruptor de movimiento ------------------------------------------
   Va adosado a la tira, que es la pieza que lo necesita. No es un interruptor
   de accesibilidad general: no toca reveals, ni hover, ni la entrada del
   héroe. Solo el movimiento AUTOMÁTICO, que es lo que el criterio exige.

   Lo inyecta `script.js`: sin JavaScript no queda un botón que no hace nada,
   y sin JavaScript tampoco hay nada que apagar porque la animación es CSS y
   la tira sigue siendo legible en cualquier caso. */
.rotulo-led-control {
  display: flex;
  justify-content: flex-end;
  padding-block-start: 12px;
  padding-inline: var(--sec-x, 18px);
}

.rotulo-led-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* 44×44 de objetivo táctil (WCAG 2.5.8) */
  min-block-size: 44px;
  padding-inline: 14px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--ui-font);
  font-size: 14px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.rotulo-led-btn:hover { border-color: var(--line-strong); color: var(--white); }
.rotulo-led-btn:focus-visible { outline: 3px solid var(--estado); outline-offset: 3px; }

/* El icono es decorativo: el estado lo lleva el texto y `aria-pressed`. */
.rotulo-led-btn svg { inline-size: 13px; block-size: 13px; fill: currentColor; }

/* Sin movimiento, la tira se queda quieta y se lee. No se oculta: el texto es
   contenido. La segunda copia sí sobra, y se retira. */
@media (prefers-reduced-motion: reduce) {
  .rotulo-led { mask-image: none; }
  /* No hay movimiento que detener: el control sobra y se retira. */
  .rotulo-led-control { display: none; }
  .rotulo-led-pista { inline-size: 100%; flex-wrap: wrap; }
  .rotulo-led-tira[aria-hidden="true"] { display: none; }
  .rotulo-led-tira { flex-wrap: wrap; gap: 14px 32px; }
  .rotulo-led-tira span::after { margin-inline-start: 32px; }
}

/* ===========================================================================
   HÉROE — COMPOSICIÓN Y PROFUNDIDAD — Xpandex, 2026-08-05
   Recetas R9 y R1 de 04-templates/motion/recetario-parallax-y-scroll.md
   ---------------------------------------------------------------------------
   Regla nº 6 de la lista de rechazo del recetario: NADA de parallax sobre el
   texto del héroe. Daña el LCP y la legibilidad. El `h1` sigue entrando solo
   con `transform`, sin desplazamiento ligado al scroll. Verificado: el
   elemento LCP de la portada es el `h1`, y ninguna regla de aquí lo toca.

   `overflow: clip` y no `hidden`: `hidden` convierte el contenedor en
   contenedor de scroll y rompe todo lo pegajoso que haya dentro.
   =========================================================================== */

/* --- R9 · Marca de agua tipográfica ---------------------------------------
   Llena el vacío con marca en vez de con relleno. Sangra por la izquierda,
   va por detrás de todo y se desplaza más despacio que el contenido: eso es
   lo que produce la sensación de profundidad, no una sombra.

   Al 5 % de blanco es textura, no texto: no compite con el titular ni entra
   en el cálculo de contraste de nada legible. `aria-hidden` porque decir
   «Rotulación» dos veces a un lector de pantalla es ruido. */
.hero-marca-agua {
  /* El texto va en un pseudoelemento para que esta palabra decorativa no
     duplique «Rotulación» en el DOM, donde ensuciaría la extracción de
     contenido.

     Lo que NO consigue, y conviene saberlo: a 1.440 px esta palabra sigue
     siendo el elemento LCP de la portada, por delante del titular — Chrome
     cuenta el texto generado igual que el real. Se probó y se descartó
     forzarlo: para que ganase el `h1` habría que encoger la marca de agua
     hasta dejarla sin efecto.

     Se acepta porque es inocuo AQUÍ, y el porqué importa: la marca de agua y
     el titular usan la MISMA familia (`--aero-font`), así que pintan a la
     vez. La métrica no puede estar tapando un titular lento. Medido: 56-64 ms.
     Si algún día la marca de agua cambia de tipografía, esto deja de ser
     cierto y hay que volver a mirarlo. */
  position: absolute;
  z-index: -1;
  /* Anclada al PIE del héroe, no detrás del titular. Cruzando el `h1` la
     marca de agua deja de ser textura y empieza a competir con lo único que
     tiene que leerse en los primeros dos segundos. Aquí ocupa la banda vacía
     de abajo, que es lo que sobraba en la composición. */
  inset-block-end: -4%;
  inset-inline-start: -5%;
  font-family: var(--aero-font);
  font-size: clamp(6rem, 21vw, 19rem);
  line-height: 0.78;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: rgba(245, 251, 255, 0.035);
  white-space: nowrap;
  user-select: none;
  pointer-events: none;
}

.hero-marca-agua::before { content: "Sevilla"; }

@media (max-width: 719px) {
  /* En móvil el titular ya ocupa el ancho: la marca de agua estorbaría. */
  .hero-marca-agua { display: none; }
}

/* --- Grano ---------------------------------------------------------------
   Una capa plana de color se lee como plantilla. Un grano al 3 % le da
   materia sin que nadie sepa decir por qué. Es un SVG en línea, así que no
   añade ni una petición. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.03;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='r'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23r)'/%3E%3C/svg%3E");
}

@supports ((animation-timeline: view()) and (animation-range: entry)) {
  @media (prefers-reduced-motion: no-preference) {

    .hero { view-timeline: --heroe; }

    /* La marca de agua deriva en sentido contrario al scroll: se aleja. */
    .hero-marca-agua {
      animation: marcaDeriva linear both;
      animation-timeline: --heroe;   /* SIEMPRE tras el atajo `animation` */
    }

    @keyframes marcaDeriva {
      from { transform: translateY(5%); }
      to   { transform: translateY(-7%); }
    }
  }
}

/* --- El bloque de arriba se quedaba SIN CERRAR --------------------------
   Hallado el 07/08 contando llaves: 697 aperturas contra 696 cierres, y el
   desbalance venía de antes de esta tanda de trabajo. Faltaba el cierre del
   `@supports`, y además el bloque de `prefers-reduced-motion: reduce` estaba
   anidado DENTRO del de `no-preference`, así que no podía aplicarse nunca.

   Las dos consecuencias, y la segunda es grave:

   1 · La regla que congela la marca de agua con movimiento reducido no
       existía en la práctica.
   2 · Con el `@supports` abierto, **todo lo que va detrás de este punto
       —unas 600 líneas: la receta de marca de agua, el muro del héroe, las
       fichas, la simetría de rejilla y los ajustes de paleta— quedaba dentro
       de la condición**. En Chromium se cumple y no se nota nada. En Firefox,
       que no soporta scroll-driven animations en estable, la condición falla
       y el navegador descarta ese tramo entero: media hoja de estilo.

   Chromium no podía revelarlo. Salió de contar llaves, no de mirar la web. */

/* Sin movimiento: todo quieto y en su sitio. El grano y la marca de agua se
   quedan —son textura, no animación— pero no derivan. */
@media (prefers-reduced-motion: reduce) {
  .hero-marca-agua { animation: none; transform: none; }
  /* El muro se mueve por JS, y `setupParallaxProfundidad()` ya comprueba
     `prefers-reduced-motion` y limpia el `transform`. Aquí se refuerza por si
     el script no llega a correr. */
  .hero-muro img { animation: none; transform: none; }
}

/* ===========================================================================
   MARCA DE AGUA REUTILIZABLE — receta R9 · revisada en el Lote 1 (2026-08-07)
   ---------------------------------------------------------------------------
   Qué cambia respecto a la versión de agosto, y por qué:

   1 · DOSIS. Se contaron **62 marcas** repartidas en 14 páginas: hasta 9 en
       una sola, y «TALLER» 19 veces en el sitio —cuatro en la misma página—.
       El comentario de esta misma receta pedía «solo las tres secciones que
       abren capítulo» y se acabó aplicando a casi todas. Ahora es **una por
       página**, y la palabra la elige la PÁGINA, no el bloque:
       `rotulacion-vehiculos` dice VEHÍCULOS una vez, no TALLER cuatro.

   2 · ACCESIBILIDAD. El atributo vivía en la `<section>` y el texto en su
       `::before`. El contenido generado SÍ se expone a las tecnologías de
       apoyo, así que la palabra decorativa era anunciable y no había dónde
       poner el `aria-hidden` sin ocultar la sección entera. Ahora hay un
       `<span class="marca-agua" aria-hidden="true">` vacío dentro de la
       sección: el texto sigue sin entrar en el DOM y el `aria-hidden` sí
       alcanza al pseudoelemento.

   3 · RECORTE POR UN SOLO BORDE. Era `inset-inline-start: -4%` con
       `white-space: nowrap`: sangraba por los dos lados a la vez y producía
       recortes del tipo «TRARA.INS» —una palabra partida por delante y por
       detrás se lee como error, no como decisión—. Ahora arranca en 0 y solo
       puede salirse por la derecha.

   4 · ESCALA POR PALABRA. Una de 11 letras no cabe donde una de 4. El tope lo
       trae cada instancia en `--ma-escala`; 17vw sigue siendo el techo
       relativo para que nunca desborde en pantallas estrechas.
   =========================================================================== */

.marca-agua-host {
  position: relative;
  overflow: clip;
  isolation: isolate;
}

/* La marca ocupa toda la sección: el registro grande se ancla al pie y el
   vertical al canalón izquierdo. Dos presencias, una sola pieza de marcado. */
/* --- Por qué NO va en z-index negativo ------------------------------------
   Estaba en `z-index: -1`, es decir, por DEBAJO del fondo de su propia
   sección. Y esos fondos son semiopacos —`rgba(0,6,12,0.64)` en las secciones
   de servicio—, así que se comían el 64 % de la marca: pedirle presencia
   subiendo la opacidad era pelear contra una capa que siempre iba a ganar.
   Con el registro vertical al 38 % el resultado efectivo era ~14 % y no se
   veía en absoluto.

   Ahora la marca va por ENCIMA del fondo de la sección y el contenido por
   encima de la marca. El `z-index: 1` de los hijos es lo que garantiza el
   orden; `position: relative` no los mueve ni un píxel. */
.marca-agua {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.marca-agua-host > *:not(.marca-agua) {
  position: relative;
  z-index: 1;
}

/* --- Registro 1 · la grande, sangrando por el pie ----------------------- */
.marca-agua::before {
  content: attr(data-marca-agua);
  position: absolute;
  inset-block-end: -3%;
  inset-inline-start: 0;
  font-family: var(--aero-font);
  font-size: min(var(--ma-escala, 15rem), 22vw);
  line-height: 0.78;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: rgba(245, 251, 255, 0.03);
  white-space: nowrap;
  user-select: none;
}

/* --- Registro 2 · el filete vertical del canalón ------------------------
   Responde a «más presencia» sin sumar más masa: ocupa un canalón que hoy
   está vacío y no compite con nada. Y trae algo que la grande no tiene —
   LEGIBILIDAD. Al 30 % es texto de verdad, no textura, y le da al conjunto un
   registro técnico que encaja con un taller.

   Solo a partir de 1.240 px, que es donde el contenedor deja canalón libre a
   los lados. Por debajo no existe: no se aprieta contra el texto.

   CONTRASTE, dicho con precisión. La especificación de dirección de arte daba
   4,9:1 al 28 % sobre `--surface-1`. Es incorrecto: medido, el 28 % da 2,5:1 y
   haría falta subir al 45 % para pasar el 4,5:1 — a ese nivel deja de ser
   marca de agua y compite con el cuerpo de texto.

     opacidad   0,22   0,30   0,34   0,38   0,45   0,55
     contraste  1,98   2,64   3,04   3,48   4,37   5,92

   Se elige 0,38 → 3,48:1, y NO cumple 4,5:1. Se declara en lugar de
   disimularlo: es texto **decorativo** bajo la excepción de «incidental» de
   WCAG 1.4.3, va con `aria-hidden` y repite literalmente el eyebrow que está
   visible a un centímetro. Nadie pierde información si no llega a leerlo. Si
   algún día pasa a decir algo que no esté ya en pantalla, esta excepción
   dejaría de aplicar y habría que subirlo al 45 %. */
/* Antes en 1240 px, un punto de ruptura que introduje solo para esto. Se
   alinea con el par 1199,98/1200 que el sitio ya tenía. Medido el canalón:
   96 px de relleno lateral a 1200 px de ventana, y el filete vive a 14 px del
   borde con 13 px de cuerpo. Entra sin rozar nada. */
@media (min-width: 1200px) {
  .marca-agua::after {
    content: attr(data-marca-agua);
    position: absolute;
    /* Propiedades FÍSICAS a propósito. Con `writing-mode: vertical-rl` los ejes
       lógicos rotan con el elemento: `inset-inline-start` deja de ser «a la
       izquierda» y pasa a ser «arriba», y `inset-block-start` se convierte en
       el eje horizontal. Con lógicas, este filete aparecía en mitad de la
       página en lugar de en el canalón. */
    left: 14px;
    top: 50%;
    transform: translateY(-50%);
    writing-mode: vertical-rl;
    font-family: var(--ui-font);
    font-size: 13px;
    letter-spacing: 0.32em;
    text-transform: uppercase;
    color: rgba(245, 251, 255, 0.38);
    white-space: nowrap;
    user-select: none;
  }
}

/* En móvil el texto ya ocupa el ancho: la marca de agua solo estorbaría. */
@media (max-width: 719px) {
  .marca-agua { display: none; }
}

@supports ((animation-timeline: view()) and (animation-range: entry)) {
  @media (prefers-reduced-motion: no-preference) and (min-width: 720px) {
    .marca-agua-host { view-timeline: --seccion; }

    .marca-agua {
      animation: marcaDeriva linear both;
      animation-timeline: --seccion;   /* SIEMPRE tras el atajo `animation` */
      animation-range: cover 0% cover 100%;
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  .marca-agua { animation: none; transform: none; }
}

/* ===========================================================================
   HÉROE · MURO DE TRABAJOS — Xpandex, 2026-08-05
   Referencia vía Refero: Agence K72 — «midnight graphic stage»: lienzo negro,
   fotografía a sangre con velo pesado, titular sobredimensionado encima.
   ---------------------------------------------------------------------------
   Se acaba el «columna de texto contra columna de imagen». Los trabajos pasan
   a ser el FONDO, a sangre y sin huecos, y el titular va encima.

   Tres problemas se resuelven de una vez, y no por ajuste sino por
   construcción:

   · El hueco entre columnas que llevamos tres intentos cuadrando ya no existe,
     porque ya no hay dos columnas.
   · Las cuatro familias que promete el titular se ven todas, así que la imagen
     dice lo mismo que las palabras.
   · El parallax por fin tiene superficie: el muro ocupa el ancho completo, y
     cada pieza se mueve a su velocidad detrás del texto.

   El velo NO es decoración: es lo que sostiene el contraste del titular sobre
   una fotografía. Va más denso por la izquierda, donde vive el texto, y se
   abre por la derecha para que las fotos respiren.
   =========================================================================== */

@font-face {
  font-family: "Archivo Black";
  src: url("./tipografias/ArchivoBlack.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

/* Métricas de reserva: sin esto, el intercambio de fuente recompone el titular
   y devuelve el desplazamiento de maquetación que costó arreglar. */
@font-face {
  font-family: "Archivo Black Fallback";
  src: local("Impact"), local("Haettenschweiler"), local("Arial Black");
  size-adjust: 92%;
  ascent-override: 88%;
  descent-override: 22%;
  line-gap-override: 0%;
}

/* ===========================================================================
   HÉROE «PLACA Y RAÍL» — Lote 2 · Xpandex, 2026-08-08
   ---------------------------------------------------------------------------
   Sustituye al muro de cuatro fotografías. La decisión la impone el MATERIAL,
   no el gusto:

   Las cuatro imágenes del muro medían 640 px de ancho y se interpolaban 2,4×
   en 1440 con DPR 2. El velo del 96 % que las oscurecía se justificaba por el
   contraste del titular —y ese argumento era correcto— pero hacía un segundo
   trabajo no declarado: **tapar esa interpolación**. Por eso el héroe se leía
   como atmósfera y no como trabajo.

   Del catálogo, solo seis fotos llegan a 1254 px y son obra entregada. Con un
   techo de 1,3× de interpolación, la caja máxima de fotografía es 760×507 CSS.
   Una banda a sangre en escritorio pediría 2880 px: **no existe en el
   material**. En móvil sí (375×250 a DPR 3 son 1125×750).

   De ahí la composición: titular a la izquierda sobre negro, placa a plena luz
   —sin velo— a la derecha, cartela de obra debajo y raíl de cinco miniaturas.
   A sangre en móvil, acotado en escritorio.

   El raíl NO es un carrusel: es un `tablist`. El usuario elige trabajo, no
   pasa páginas. Con *roving tabindex* añade UNA sola parada de tabulación
   antes del CTA, y activar una pestaña detiene el avance automático para
   siempre — que es lo que WCAG 2.2.2 exige de verdad.
   =========================================================================== */

.hero {
  position: relative;
  isolation: isolate;
  overflow: clip;
  padding-block: clamp(28px, 4vw, 72px);
}

/* El titular se dimensiona contra SU columna, no contra la ventana.
   Con `8.4vw` heredado del muro —donde ocupaba el ancho entero— «ROTULACIÓN»
   medía 788 px dentro de una columna de 508 y `overflow: clip` se lo comía:
   se leía «ROTULAC». Con unidades de contenedor el titular ocupa siempre el
   ancho de su columna, en cualquier ventana, sin tabla de puntos de ruptura. */
.hero-copy {
  min-inline-size: 0;
  container-type: inline-size;
}

@media (min-width: 1000px) {
  .hero {
    display: grid;
    /* 44 / 56 · el titular necesita menos ancho del que cree; la placa
       necesita todo el que pueda pagar. */
    grid-template-columns: 44fr 56fr;
    gap: clamp(28px, 4vw, 56px);
    align-items: center;
  }
}

/* --- La placa ------------------------------------------------------------ */
.placa-bloque { min-inline-size: 0; }

.placa-marco {
  position: relative;
  overflow: hidden;
  /* Reserva el hueco en el PRIMER frame: es lo que mantiene el CLS en 0. */
  aspect-ratio: 3 / 2;
  max-inline-size: 760px;
  margin-inline-start: auto;
  border: 1px solid var(--line-mid);
  background: var(--surface-1);
}

/* Solo se anima `transform`. Nada de `clip-path`: eso repinta la capa entera
   de una fotografía de 1.520 px en cada fotograma. */
.placa-pista {
  display: flex;
  block-size: 100%;
  transition: transform 360ms var(--ease-corte);
  will-change: transform;
}

.placa { flex: 0 0 100%; block-size: 100%; }

.placa img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
  /* SIN velo. El texto no se apoya en la foto, así que no hay nada que
     compensar: la foto se ve porque el texto se ha apartado. */
  filter: brightness(1);
}

/* --- La cartela de obra --------------------------------------------------- */
.cartela {
  display: flex;
  align-items: center;
  gap: 16px;
  max-inline-size: 760px;
  margin: 0 0 0 auto;
  /* Alto FIJO: la cartela cambia de texto al pasar de placa y no puede
     empujar nada al hacerlo. */
  block-size: 52px;
  padding-inline: 14px;
  border: 1px solid var(--line);
  border-block-start: 0;
  background: var(--surface-2);
  font-family: var(--ui-font);
  font-size: 16px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  overflow: hidden;
}

.cartela-num   { color: var(--white); font-variant-numeric: tabular-nums; }
.cartela-fam   { color: var(--accion); }
.cartela-lugar { color: var(--muted); margin-inline-start: auto; letter-spacing: 0.06em; }

/* --- El raíl · `tablist` -------------------------------------------------- */
.rail {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  max-inline-size: 760px;
  margin: 10px 0 0 auto;
  padding: 0;
}

.rail-btn {
  position: relative;
  display: block;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 0;
  background: var(--surface-1);
  cursor: pointer;
  overflow: hidden;
  aspect-ratio: 3 / 2;
  transition: border-color var(--dur-fast) var(--ease);
}

.rail-btn img {
  inline-size: 100%; block-size: 100%; object-fit: cover; display: block;
  /* 0,72 y no 0,55. El raíl no es una fila de botones: es el ÍNDICE del
     trabajo, y a 0,55 las fotos oscuras —un neón de noche, un local en
     sombra— se volvían rectángulos negros indistinguibles. Si no se ve qué
     hay en cada miniatura, el raíl no está indexando nada. */
  filter: brightness(0.72) saturate(0.85);
  transition: filter var(--dur-fast) var(--ease);
}

.rail-btn:hover img { filter: brightness(0.9); }
.rail-btn[aria-selected="true"] img { filter: brightness(1); }
.rail-btn[aria-selected="true"] { border-color: var(--line-strong); }

/* `--estado` significa «estás aquí»; el filete es lo que lo dice. */
.rail-btn[aria-selected="true"]::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  block-size: 3px;
  background: var(--estado);
}

.rail-btn:focus-visible { outline: 3px solid var(--estado); outline-offset: 3px; }

.rail-ord {
  position: absolute;
  inset-block-start: 4px; inset-inline-start: 6px;
  z-index: 1;
  font-family: var(--ui-font);
  font-size: 13px;
  color: rgba(245, 251, 255, 0.62);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95);
}
.rail-btn[aria-selected="true"] .rail-ord { color: #fff; }

/* --- Móvil: la foto a sangre y ARRIBA -------------------------------------
   El primer pliegue de un iPhone son ~643 px útiles. Con la columna de texto
   delante, el trabajo no se ve sin scroll y el héroe deja de enseñar nada. La
   placa se adelanta con `order`, no reordenando el HTML: el orden del DOM
   sigue siendo el correcto para lector de pantalla y para el `h1`.

   Medido a 375×812: placa 0-250 · cartela 265-309 · raíl 324-364 ·
   titular 404-463 · CTA 565-613. Todo dentro del pliegue. */
@media (max-width: 999px) {
  /* `align-items: stretch` es obligatorio, no cosmético: `.hero-copy` es un
     contenedor de consulta (`container-type: inline-size`) y su ancho deja de
     depender del contenido. Con `align-items: start` heredado de una regla
     anterior, el elemento se encogía a contenido, el contenido no podía
     medirlo, y la anchura resolvía a CERO — con ella, el titular a 0 px. */
  .hero {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    padding-block-start: 0;
    gap: 14px;
  }
  .hero.section-pad { padding-block-start: 0; }
  .placa-bloque { order: -1; margin-inline: calc(var(--sec-x, 18px) * -1); }
  /* 16:9 en vez de 3:2. A 375 px son 211 px de alto en lugar de 250, y esos
     39 px son exactamente los que faltaban para que el botón de presupuesto
     entrara entero en el pliegue. El rótulo de la fotografía se sigue viendo:
     lo que se recorta es cielo y suelo. */
  .placa-marco { border-inline: 0; max-inline-size: none; aspect-ratio: 16 / 9; }
  .cartela { max-inline-size: none; block-size: 44px; font-size: 15px; margin-inline: 0; }
  .rail { max-inline-size: none; gap: 6px; margin: 8px 0 0; }
  /* --- El pliegue de un iPhone manda ------------------------------------
     Medido en el sitio real: 812 − 89 de cabecera − ~80 de chrome del
     navegador = ~643 px útiles. Con el párrafo delante del CTA, el botón caía
     en 758 px, fuera. La regla del estudio pide propuesta de valor + CTA +
     una señal de confianza SIN scroll, así que el párrafo pasa detrás del
     botón: en un teléfono el visitante necesita la promesa y la acción; la
     explicación puede esperar dos dedos de scroll.

     Se hace con `order`, no reordenando el HTML: el orden del DOM —y por
     tanto el de lectura de un lector de pantalla— sigue siendo el correcto. */
  .hero-copy { display: flex; flex-direction: column; }
  .hero-copy .eyebrow { order: 1; }
  .hero-copy h1       { order: 2; }
  .hero-copy .cta-row { order: 3; margin-block: 4px 14px; }
  .hero-copy .hero-lead { order: 4; margin-block-end: 14px; }
  .hero-copy .trust-strip { order: 5; }
}

/* Con movimiento reducido el temporizador ni se crea (ver `script.js`). Aquí
   solo se quita la interpolación: la función se conserva entera. */
@media (prefers-reduced-motion: reduce) {
  .placa-pista { transition: none; }
}
:root[data-movimiento="off"] .placa-pista { transition: none; }

.hero-copy {
  position: relative;
  /* Ancho suficiente para que la palabra entre ENTERA. Cortada a media
     palabra se lee como un fallo de maquetación, no como una decisión. */
  max-inline-size: min(100%, 1120px);
  padding-block: clamp(28px, 4vw, 56px);
}

/* --- El titular ----------------------------------------------------------
   Todo en la pesada. La cadena no cambia ni una palabra: lo que cambia es que
   «Rotulación» manda y el resto acompaña. */
/* --- El titular del héroe, en la voz del sitio ---------------------------
   Iba en Archivo Black mientras TODOS los demás titulares y todos los eyebrow
   van en Aero. No era una cursiva contra una redonda —medido, ninguna de las
   dos lleva `font-style: italic`—: eran dos familias distintas. La inclinación
   de los `h2` está dibujada dentro de la propia Aero, que es una display
   oblicua de fábrica.

   Lo delataba el propio héroe: su eyebrow «ROTULACIÓN EN SEVILLA» ya iba en
   Aero, así que dentro del mismo bloque convivían las dos.

   Se unifica en Aero. Archivo Black se queda declarada en la hoja por si el
   cliente decide lo contrario en D4 — es un cambio de una línea. */
.hero h1 {
  display: block;
  margin: 0 0 18px;
  font-family: var(--aero-font);
  /* 700, igual que los `h2` de sección. Aero se sirve en UN solo fichero, así
     que ese 700 lo sintetiza el navegador engordando el trazo. Da igual si
     está bien o mal: si los titulares de sección lo llevan y el del héroe no,
     el héroe se ve más fino que todo lo que hay debajo — que era la mitad de
     la sensación de «no es la misma fuente». */
  font-weight: 700;
  text-transform: uppercase;
}

.hero .h1-fuerte {
  display: block;
  /* El titular se dimensiona contra SU columna, no contra la ventana.
     Con `8.4vw` —heredado del muro, donde ocupaba el ancho entero—
     «ROTULACIÓN» medía 788 px dentro de una columna de 508 y el
     `overflow: clip` del héroe se lo comía: se leía «ROTULAC».

     Medido en Archivo Black, el texto ocupa 7,04 veces el cuerpo, así que
     100/7,04 ≈ 14cqw llena la columna exacta. Se deja en 13 para que quede
     aire por la derecha, y el `min()` conserva el tope de 7rem. */
  /* 16,5cqw y no 13. Aero es bastante más condensada que Archivo Black: con el
     valor anterior el titular ocupaba 388 px de una columna de 508 y el héroe
     perdía la presencia que se le pide. Medido con la familia nueva para que
     siga cabiendo en los seis anchos probados. */
  font-size: min(7rem, 16.5cqw);
  line-height: 0.84;
  letter-spacing: -0.035em;
}

.hero .h1-resto {
  display: block;
  margin-block-start: 12px;
  font-size: clamp(0.95rem, 1.75vw, 1.5rem);
  line-height: 1.1;
  letter-spacing: 0.005em;
  color: rgba(245, 251, 255, 0.82);
  max-inline-size: 24ch;
}

/* El titular necesita los 1120 px; el texto que se LEE, no. Se le devuelve una
   medida de lectura sana y a los botones un ancho de botón, no de banda. */
.hero .hero-lead { max-inline-size: 52ch; }
.hero .cta-row   { max-inline-size: 560px; }
.hero .trust-strip { max-inline-size: 560px; }

/* ===========================================================================
   PROFUNDIDAD DE SUPERFICIE — Xpandex, 2026-08-05
   ---------------------------------------------------------------------------
   Corrección de algo que rompí. El original le daba a cada superficie un
   degradado de 135° —un canto iluminado en diagonal— y eso era lo que hacía
   que las tarjetas parecieran objetos y no rectángulos de color. Al quitar el
   tinte cian me llevé el degradado con él, y las superficies se quedaron
   planas.

   El tinte era el problema; el degradado no. Aquí vuelve el degradado con
   LUZ NEUTRA en lugar de cian: se recupera la profundidad sin devolver el
   «todo es azul» que costó arreglar.

   La luz entra por arriba a la izquierda, como en el original. Es una
   convención, no un capricho: si cada superficie se ilumina desde un sitio
   distinto, el conjunto deja de leerse como un plano físico.
   =========================================================================== */

:root {
  /* Luz diagonal para tarjetas y paneles. */
  --sup-luz:
    linear-gradient(135deg,
      rgba(245, 251, 255, 0.075) 0%,
      rgba(245, 251, 255, 0.022) 38%,
      rgba(0, 0, 0, 0.10) 100%);

  /* Versión más marcada, para lo que debe destacar del resto. */
  --sup-luz-alta:
    linear-gradient(135deg,
      rgba(245, 251, 255, 0.11) 0%,
      rgba(245, 251, 255, 0.03) 42%,
      rgba(0, 0, 0, 0.14) 100%);
}

/* Las superficies que ya llevaban degradado: se les cambia el tinte, no la
   estructura. Las que quedaron planas: se les devuelve. */
/* El azul VUELVE. La luz neutra recuperaba el relieve pero perdía el carácter
   del original: ese tinte frío en el canto iluminado es parte de la identidad
   de ROT3X, no un accidente de la paleta vieja. Se restauran los valores
   exactos del original.

   Nota de sistema, para quien venga después: esto convive con la regla de
   «un color, un significado» porque aquí el azul está al 16 % sobre una
   superficie, no saturado sobre un botón. Un tinte de superficie y un color de
   acción no compiten: uno es el material, el otro es la orden. */
.bevel-card {
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.16), rgba(1, 10, 18, 0.78)),
    var(--panel);
}

.menu-column {
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.15), rgba(1, 10, 18, 0.82)),
    var(--panel);
}

/* El formulario, un punto más marcado: es la superficie que tiene que
   destacar sobre el resto. */
.budget-form {
  background:
    linear-gradient(135deg, rgba(54, 164, 222, 0.20), rgba(1, 10, 18, 0.80)),
    var(--panel);
}

/* El campo va HUNDIDO: la luz entra al revés, desde abajo a la derecha, que
   es lo que distingue un hueco de un bulto. Los dos formularios del sitio, no
   solo el largo. */
.budget-form input,
.budget-form select,
.budget-form textarea,
.lead-form input,
.lead-form select,
.lead-form textarea {
  background:
    linear-gradient(135deg, rgba(0, 0, 0, 0.34), rgba(54, 164, 222, 0.07)),
    var(--surface-1);
}

/* El botón de acción, encendido por arriba como un tubo. El degradado es
   vertical y muy corto: no es el degradado diagonal de 2012 que el director
   de arte retiró con razón, es la caída de luz de algo que emite. */
.btn-primary,
.header-cta,
.nav-cta {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 58%),
    var(--accion);
}

.btn-primary:hover,
.btn-primary:focus-visible,
.header-cta:hover,
.header-cta:focus-visible,
.nav-cta:hover,
.nav-cta:focus-visible {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0) 58%),
    var(--accion-alta);
}

/* El secundario deja de ser un rectángulo vacío y recupera cuerpo, sin llegar
   a competir con el primario. */
.btn-ghost {
  background: linear-gradient(135deg, rgba(54, 164, 222, 0.18), rgba(1, 10, 18, 0.55));
}

.btn-ghost:hover,
.btn-ghost:focus-visible {
  background: linear-gradient(135deg, rgba(54, 164, 222, 0.28), rgba(1, 10, 18, 0.5));
}

/* Barra de anclas y pie: profundidad vertical, que es la que corresponde a una
   banda que cruza la página. */
.anchor-nav a {
  background: linear-gradient(135deg, rgba(54, 164, 222, 0.14), rgba(1, 10, 18, 0.7));
}

.site-footer {
  background: linear-gradient(180deg, rgba(9, 36, 55, 0.92), rgba(1, 4, 12, 0.98));
}

/* K3 · El ámbar que sobrevivió a la limpieza de paleta.
   `rgba(229,139,8,.15)` es el naranja de la paleta anterior, retirado del
   resto del sitio en julio y vivo aquí en las 9 páginas que llevan el bloque.
   Sobre una interfaz cuyo único color de acción es el cian, un panel con
   fondo ámbar se lee como un aviso — y no lo es: es un consejo.
   Pasa a la escala de superficies, como el resto de paneles. */
.lead-advice {
  background: var(--sup-luz-alta), var(--surface-3);
}

.quote {
  background: linear-gradient(180deg, rgba(4, 29, 45, 0.88), rgba(0, 6, 12, 0.86));
}

/* ===========================================================================
   MARCAS DE AGUA, MÁS PRESENTES
   ---------------------------------------------------------------------------
   Del 3 % al 6,5 %. Sigue siendo textura —no compite con ningún texto— pero
   ahora se ve que está ahí, que era el sentido de ponerla.
   =========================================================================== */
/* Del 3 % al 6,5 % y ahora al 13 %. El encargo pide presencia de marca, y al
   6,5 % la palabra estaba pero no se leía como decisión. Medido el peor caso
   —cuerpo al 78 % de blanco sobre el trozo de sección que la marca aclara—
   el contraste se mantiene muy por encima del 4,5:1 exigido: la marca ACLARA
   el fondo y el texto es claro, así que la única forma de romper contraste
   sería subirla hasta competir en luminosidad con el propio texto. */
.marca-agua::before { color: rgba(245, 251, 255, 0.13); }
.hero-marca-agua    { color: rgba(245, 251, 255, 0.12); }

/* ===========================================================================
   TIPO «FICHA» — Lote 1.2 · Xpandex, 2026-08-07
   ---------------------------------------------------------------------------
   El sitio tenía DIEZ clases de tarjeta para diecinueve páginas, y seis de
   ellas compartían literalmente la misma declaración de fondo y filete
   (`styles.css:504-518`). No eran diez tipos: era un tipo con diez nombres.

   La regla nueva, que decide el envase por el CONTENIDO y no por la posición:

     · con fotografía            → PLACA   (la foto ES la tarjeta)
     · < 40 palabras y sin foto  → FICHA   (y entonces NO lleva caja)
     · con controles o formulario→ PANEL   (el único con derecho a fondo)

   Este bloque implementa la Ficha. El diagnóstico de por qué estas piezas se
   veían pobres no era falta de adorno: **un párrafo de doce palabras dentro
   de un marco de 20 px promete un objeto y entrega una frase**. Quitar la caja
   no es quitar diseño, es dejar de prometer lo que no hay.

   Lo que se gana, además del tono: el relleno pasa de 20 px por los cuatro
   lados a 18 px arriba y 20 abajo, sin bordes laterales. Y donde había rejilla
   con hueco impar —cinco motivos en 2+2+1— deja de haber huérfanas, porque
   donde no hay rejilla no puede haberlas.
   =========================================================================== */

.proof-grid article,
.process-list li,
.image-requirements,
.service-detail-card {
  display: grid;
  /* El ordinal vive en un canalón propio: la columna de texto queda alineada
     y la lectura vertical se ordena sola. */
  grid-template-columns: 3.5rem 1fr;
  align-items: start;

  background: none;
  border: 0;
  border-block-start: 1px solid var(--line);
  border-radius: 0;
  padding: 18px 0 20px;
}

/* Todo el contenido a la segunda columna; el ordinal se queda en la primera. */
.proof-grid article > *,
.process-list li > *,
.image-requirements > *,
.service-detail-card > * { grid-column: 2; }

.proof-grid article > strong,
.process-list li > span {
  grid-column: 1;
  grid-row: 1;
  margin: 0;
  font-family: var(--ui-font);
  font-size: 28px;
  line-height: 1.05;
  color: rgba(245, 251, 255, 0.5);   /* medido 5,00:1 — cumple el 3:1 de texto grande */
  font-variant-numeric: tabular-nums;
}

/* `.image-requirements` y `.service-detail-card` no traen ordinal en el HTML.
   Se genera con contador en lugar de tocar 7 ficheros: el resultado es el
   mismo y el marcado no engorda. */
.service-detail-grid { counter-reset: ficha; }

.image-requirements,
.service-detail-card { counter-increment: ficha; }

.image-requirements::before,
.service-detail-card::before {
  content: counter(ficha, decimal-leading-zero);
  grid-column: 1;
  grid-row: 1;
  font-family: var(--ui-font);
  font-size: 28px;
  line-height: 1.05;
  color: rgba(245, 251, 255, 0.5);
  font-variant-numeric: tabular-nums;
}

.proof-grid article h3,
.process-list li h3,
.image-requirements h3,
.service-detail-card h3 {
  margin: 0 0 6px;
  font-size: var(--t-h3);
}

.proof-grid article p,
.process-list li p,
.image-requirements p,
.service-detail-card p {
  margin: 0;
  max-inline-size: 62ch;
  color: var(--muted);
}

/* La marca de corte se reserva para la Placa. Sobre una ficha sin caja no hay
   esquina que marcar, y repetida en todo pierde el significado de oficio. */
.proof-grid article::before { content: none; }

/* --- Las pistas -----------------------------------------------------------
   MEDIDO antes de decidir, y el primer intento estaba mal. Pasar `.proof-grid`
   y `.service-detail-grid` a una y dos columnas parecía lo coherente con
   «filas continuas», pero las páginas de servicio CRECÍAN en escritorio:

     rotulacion-vehiculos  12.135 → 12.253 px   (+118)
     vinilos-negocios      10.739 → 10.925 px   (+186)

   La razón es aritmética y no de diseño: 22 fichas en dos columnas ocupan más
   que 22 tarjetas en tres, por muy baja que sea cada ficha. Así que las pistas
   se quedan como estaban —`auto-fit` fluido— y lo que cambia es la ANATOMÍA de
   la pieza, que es donde estaba el problema. En móvil todas caen a una columna
   solas, y ahí la ficha gana casi 500 px por página.

   Con el `21rem` del Lote 1.1, los cinco motivos de `conversion-band` pasan de
   2+2+1 —una fila huérfana de uno— a 3+2. La huérfana desaparece sin forzar
   una sola columna. */

/* El centrado y el techo de 34rem eran para tarjetas con foto. Una ficha ocupa
   su celda entera y se alinea a la izquierda. */
.proof-grid { justify-items: stretch; }
.proof-grid > * { max-inline-size: none; }

/* ===========================================================================
   SIMETRÍA DE REJILLA — Lote 1.8 · Xpandex, 2026-08-07
   ---------------------------------------------------------------------------
   Ninguna fila puede terminar con una sola pieza.

   Medido antes de escribir una línea: **38 de las 53 rejillas del sitio**
   acababan con una huérfana a 1024 px, 12 a 1440 y 11 a 1280. No era un
   descuido puntual, era estructural: `auto-fit` decide el número de columnas
   por el ANCHO disponible y no mira nunca cuántas piezas tiene que repartir.
   Tres piezas en dos columnas dan 2+1; siete en tres dan 3+3+1.

   La corrección va en dos capas:

   1 · CONTEO. Con `:has()` se lee cuántos hijos hay y se fija el número de
       columnas que reparte bien. Tres piezas van a tres columnas o a una,
       nunca a dos. Cuatro van a dos o a una, nunca a tres.

   2 · RED DE SEGURIDAD. Para los conteos que no reparten exacto —siete en
       tres columnas— la última pieza ocupa la fila entera. Deja de leerse
       como un hueco y pasa a leerse como una decisión.

   `:has()` es Baseline desde diciembre de 2023 y el sitio no tiene build, así
   que entra sin coste. Donde no esté soportado, la rejilla se comporta como
   hasta ahora: la degradación es la situación anterior, no una rota.
   =========================================================================== */

@media (min-width: 720px) {

  /* --- 1 · Conteo ------------------------------------------------------- */

  /* Tres piezas: o tres columnas o una. Dos dan 2+1, que es el caso más
     frecuente del sitio. */
  .priority-grid:has(> :last-child:nth-child(3)),
  .proof-grid:has(> :last-child:nth-child(3)),
  .service-detail-grid:has(> :last-child:nth-child(3)),
  .gallery-grid:has(> :last-child:nth-child(3)),
  .process-list:has(> :last-child:nth-child(3)),
  .faq-list:has(> :last-child:nth-child(3)) {
    grid-template-columns: 1fr;
  }

  /* Cuatro piezas: dos columnas (2+2). Tres dan 3+1. */
  .priority-grid:has(> :last-child:nth-child(4)),
  .proof-grid:has(> :last-child:nth-child(4)),
  .service-detail-grid:has(> :last-child:nth-child(4)),
  .process-list:has(> :last-child:nth-child(4)),
  .faq-list:has(> :last-child:nth-child(4)) {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Las rejillas con FOTOGRAFÍA aguantan tres columnas antes que las de texto:
   una placa de 295 px sigue enseñando el rótulo, mientras que un párrafo o una
   pregunta de FAQ a ese ancho se parte en demasiadas líneas. Por eso el umbral
   es distinto para unas y otras, y no un 1100 para todo. */
@media (min-width: 1000px) {
  .priority-grid:has(> :last-child:nth-child(3)),
  .proof-grid:has(> :last-child:nth-child(3)),
  .service-detail-grid:has(> :last-child:nth-child(3)),
  .gallery-grid:has(> :last-child:nth-child(3)) {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* --- Las rejillas de FOTOGRAFÍA piden otra aritmética ----------------------
   Medido: pasar una galería de cuatro fotos de 3+1 a 2+2 es simétrico, sí,
   pero cada foto dobla de ancho y la sección crece. `galeria.html` subía
   **+414 px** y `rotulacion-vehiculos` perdía casi todo lo ganado con las
   fichas. Simetría y densidad tiraban en direcciones opuestas.

   La salida es una fila de cuatro: sigue siendo simétrica —una sola fila,
   ninguna huérfana— y además es la disposición MÁS CORTA de las tres. A
   1.200 px el contenedor da columnas de ~290 px, de sobra para que se lea el
   rótulo de la fotografía, que es lo único que esa foto tiene que enseñar. */
@media (min-width: 1200px) {
  .gallery-grid:has(> :last-child:nth-child(4)) {
    grid-template-columns: repeat(4, 1fr);
  }
}

@media (min-width: 720px) and (max-width: 1199.98px) {
  .gallery-grid:has(> :last-child:nth-child(4)) {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1100px) {
  .process-list:has(> :last-child:nth-child(3)),
  .faq-list:has(> :last-child:nth-child(3)) {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* --- 2 · Red de seguridad ------------------------------------------------
   Para todo lo demás. Si la última pieza es la primera de su fila, es que
   está sola: ocupa la fila entera.

   Los `nth-child` van por número de columnas, no por conteo, y por eso hay
   que declararlos por punto de ruptura: es la única forma de que CSS sepa
   cuántas columnas hay sin preguntárselo al navegador. */

@media (min-width: 720px) and (max-width: 1099.98px) {
  /* dos columnas: sobra cuando el conteo es impar */
  .proof-grid:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(odd),
  .service-detail-grid:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(odd),
  .gallery-grid:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(odd),
  .process-list:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(odd),
  .faq-list:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }
}

@media (min-width: 1100px) {
  /* tres columnas: sobra cuando el conteo deja resto 1 */
  .proof-grid:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(3n + 1),
  .service-detail-grid:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(3n + 1),
  .gallery-grid:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(3n + 1),
  .process-list:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(3n + 1),
  .faq-list:not(:has(> :last-child:nth-child(-n + 4))) > :last-child:nth-child(3n + 1) {
    grid-column: 1 / -1;
  }
}


/* ===========================================================================
   TIPO «PLACA» — Lote 1.3 · Xpandex, 2026-08-07
   ---------------------------------------------------------------------------
   La foto ES la tarjeta y el texto va encima, no debajo. Eso recupera entre 90
   y 130 px de alto por pieza y hace que el elemento se lea como una obra
   etiquetada, que es literalmente lo que un rotulista entrega.

   El pie tenía toda su información al mismo peso —«Trabajo realizado:
   furgoneta comercial»—, así que la distinción que más importa a quien está
   decidiendo (obra entregada vs. render de referencia) quedaba enterrada
   delante de los dos puntos. Ahora son dos registros: el título grande y el
   tipo de pieza en pequeño. No se ha inventado texto: es el mismo, partido.
   =========================================================================== */

/* El sello: ordinal sobre la fotografía, arriba a la izquierda. El contador
   evita tener que numerar a mano 102 figuras en 13 ficheros. */
.gallery-grid,
.carrusel-pista { counter-reset: placa; }

.gallery-grid > figure,
.carrusel-item { counter-increment: placa; }

.gallery-grid > figure > img,
.carrusel-item figure > img { display: block; }

.gallery-grid > figure::before,
.carrusel-item figure::before {
  content: counter(placa, decimal-leading-zero);
  position: absolute;
  z-index: 2;
  inset-block-start: 8px;
  inset-inline-start: 10px;
  font-family: var(--ui-font);
  font-size: 14px;
  letter-spacing: 0.08em;
  color: rgba(245, 251, 255, 0.72);
  /* La sombra es lo que garantiza el contraste: el sello cae sobre fotografía,
     y la fotografía no se puede medir. Con ella el ordinal se lee sobre cielo
     claro y sobre asfalto por igual. */
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.95), 0 0 2px rgba(0, 0, 0, 0.9);
  pointer-events: none;
}

/* --- El pie, en dos registros -------------------------------------------- */
.gallery-grid > figure figcaption,
.carrusel-item figcaption {
  display: grid;
  gap: 1px;
}

.placa-tit {
  font-family: var(--ui-font);
  font-size: clamp(17px, 1.1vw + 13px, 21px);
  line-height: 1.05;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: #fff;
}

.placa-meta {
  font-family: var(--body-font);
  font-size: 12.5px;
  line-height: 1.3;
  letter-spacing: 0.06em;
  /* 78 % de blanco sobre el velo de 0,88 da holgura sobre el 4,5:1. */
  color: rgba(245, 251, 255, 0.78);
}

/* El velo solo bajo el texto, nunca sobre el sujeto de la foto. */
.gallery-grid > figure figcaption,
.carrusel-item figcaption {
  background: linear-gradient(to top, rgba(2, 5, 11, 0.90) 0%, rgba(2, 5, 11, 0.72) 62%, transparent 100%);
}

/* --- Galerías de servicio convertidas a carrusel --------------------------
   El carrusel de portada usa piezas CUADRADAS, y con razón: allí cada foto es
   la portada de un servicio y el cuadrado le da presencia. En una galería de
   servicio el cuadrado sale caro — medido, la banda pasaba de 783 px como
   rejilla a 893 como carrusel, es decir, la conversión ALARGABA la página, que
   es lo contrario de lo que se buscaba.

   A 3:2 la misma fila mide lo que medía la rejilla y enseña más fotos. */
.carrusel--galeria .carrusel-item img { aspect-ratio: 3 / 2; }

/* H4 · enlaces de contexto hacia el catálogo y la página de empresa.
   `mas-soluciones` y `empresa` no recibían NI UN enlace desde el cuerpo de
   ninguna página: se llegaba solo por el menú, que va en la cabecera y no
   transmite la misma señal de relevancia.

   Van al CIERRE del bloque, no dentro del encabezado. Colocados arriba
   heredaban el registro del `.section-heading` —mayúsculas y cuerpo de
   titular—, competían con el `h2` y, peor, invitaban a salir de la página
   antes de haber leído nada. */
.cierre-enlace {
  margin: 28px 0 0;
  padding-block-start: 18px;
  border-block-start: 1px solid var(--line);
  font-size: var(--t-body);
  text-transform: none;
  letter-spacing: normal;
}

/* ===========================================================================
   P3 · El formulario, por encima de los textos de apoyo — Xpandex, 2026-08-10
   ---------------------------------------------------------------------------
   Medido en `presupuesto.html` a 375 px: el primer campo estaba a **1.823 px**,
   casi tres pantallas de scroll. La propuesta del informe planteaba invertir el
   orden dentro de `.page-hero-grid`, pero se escribió sobre una estructura que
   no es esta: el formulario no vive en el héroe, vive en su propia sección, y
   por delante tiene **731 px** de textos de apoyo —cómo enviar archivos, qué
   fotografías recomendamos— dentro del mismo contenedor.

   En un teléfono ese orden está invertido respecto a la intención: quien entra
   en «pedir presupuesto» ya ha decidido; los consejos ayudan MIENTRAS rellena,
   no antes de empezar.

   Se hace con `order`, no reordenando el HTML: el orden del DOM —y por tanto el
   de lectura de un lector de pantalla y el del tabulador— no cambia.
   Solo en móvil: en escritorio las dos columnas van a la par y no hay problema.
   =========================================================================== */
@media (max-width: 719px) {
  .container:has(> .budget-form) {
    display: flex;
    flex-direction: column;
  }

  .container:has(> .budget-form) > .budget-form { order: -1; }
}
