
/* ── tipografías servidas por el propio sitio: sin dependencia de red ── */
@font-face{font-family:'Satoshi';src:url(/assets/fonts/Satoshi-300.woff2) format('woff2');font-weight:300;font-display:swap;font-style:normal}
@font-face{font-family:'Satoshi';src:url(/assets/fonts/Satoshi-400.woff2) format('woff2');font-weight:400;font-display:swap;font-style:normal}
@font-face{font-family:'Satoshi';src:url(/assets/fonts/Satoshi-500.woff2) format('woff2');font-weight:500;font-display:swap;font-style:normal}
@font-face{font-family:'Satoshi';src:url(/assets/fonts/Satoshi-700.woff2) format('woff2');font-weight:700;font-display:swap;font-style:normal}
@font-face{font-family:'Newsreader';src:url(/assets/fonts/Newsreader-var-latin.woff2) format('woff2');font-weight:200 800;font-display:swap;font-style:normal;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Newsreader';src:url(/assets/fonts/Newsreader-var-latin-ext.woff2) format('woff2');font-weight:200 800;font-display:swap;font-style:normal;unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
/* ─────────────────────────────────────────────────────────────
   EIKONIC — sistema de tokens (ID Manual 2026-08-16)
   Color: Pantone 533C Asphalt #1F2945 · 534C Steel #15355E ·
   552C Arctic #B8D2DD + tint #D4E4EA · grises de apoyo del manual.
   Display: Newsreader (interina hasta licencia web de Signifier).
   Cuerpo: Satoshi — asset real de la marca (Fontshare, libre web).
   Forma: filo editorial, radio 0–2px.
   ───────────────────────────────────────────────────────────── */
:root{
  /* ADENDA_R72_VENTANA_HERO.md §3.3 -- techo de 3 curvas de easing en TODO el
     archivo, contadas sobre el CSS completo (auditoría de Jara encontró 8
     cubic-bezier distintos: .2,.7,.3,1 / .23,1,.32,1 / .25,1,.5,1 / .2,.6,.2,1 /
     .16,1,.3,1 / .61,1,.88,1 / .5,1,.89,1, más `ease`/`linear` sueltos sin
     declarar). Interpretación aplicada: `linear` y `ease` son primitivas del
     navegador, no curvas "inventadas" por rondas anteriores -- se declaran
     igual como custom property y se referencian, pero no se tocan sus 74+ usos
     de UI (botones/cards/hover) por ser el mecanismo de menor riesgo de
     regresión visual sin QA dedicado. Los 7 cubic-bezier CUSTOM sí eran el
     problema real (uno por ronda, nunca reusado) -- se consolidan en UNO solo,
     --ease-out, el más usado ya (var(--ease-out), 2 apariciones antes
     de esta ronda). Total: 3 -- --ease-scrub (linear) + --ease-ui (ease) +
     --ease-out (el bezier único). */
  --ease-scrub:linear;
  --ease-ui:ease;
  --ease-out:cubic-bezier(.16,1,.3,1);
  /* ADENDA_R72_VENTANA_HERO.md -- ventana única de scroll para el gesto del hero
     en las 8 fichas (medida real de madison/quadrante, el único tier que ya
     cerraba a la vista: 1435px/1378px @1440 -- se porta 1400px, no se reinventa).
     Sustituye los tres números de tier (largo H-100 ~2025-2225px, medio H-100
     ~1350-1395px, corto 10vh~90px) por UNO solo: la diferencia entre desarrollos
     deja de vivir en cuánto scrollea el usuario y pasa a vivir en QUÉ mecanismo
     corre (máscara que panea / ventana que viaja / bandas que convergen) sobre
     la MISMA distancia. Valor fijo en px, no vh: un window de scroll pensado en
     "cuánto tarda el gesto" no debe depender de la altura del viewport del
     dispositivo -- 1400px son 1400px en cualquier pantalla. */
  --hero-ventana:1400px;
  --tinta:#0E1B2F;        /* navy profundo del manual — fondos oscuros */
  --navy:#15355E;         /* Steel Blue · Pantone 534 C */
  --asfalto:#1F2945;      /* Asphalt Blue · Pantone 533 C — tinta de texto */
  --navy-alto:#1D4578;
  --papel:#F2EEE7;        /* crema tibio de Quadrante — fondo de sección clara (promovido 2026-08-19) */
  --blanco:#F7F9FA;      /* casi-blanco — texto/UI sobre navy (antes --papel) */
  --papel-2:#D4E4EA;      /* Arctic tint — del propio manual */
  --cielo:#B8D2DD;        /* Arctic Blue · Pantone 552 C */
  --niebla:#A1A8BC;       /* gris del manual · 7.3:1 sobre --tinta */
  --error:#E9A08A;
  --error-papel:#AD4120;  /* mismo tono que --error, bajado a 5.11:1 sobre --papel (crema, recalculado 2026-08-19: sobre F7F9FA daba 4.28:1, bajo AA) */        /* estado de error del formulario, sobre fondo oscuro */
  --niebla-papel:#5D696E; /* gris del manual · 5.4:1 sobre --papel */
  --linea:rgba(21,53,94,.16);
  --linea-osc:rgba(184,210,221,.20);
  --ok:#3E6B4F;

  --display:'Newsreader', Georgia, 'Times New Roman', serif;
  --cuerpo:'Satoshi', system-ui, -apple-system, sans-serif;

  --r:2px;
  --ancho:1240px;
  --pad:clamp(20px, 5vw, 64px);

  /* escala tipográfica fluida 390 → 1440 */
  /* Escala de 7 escalones — BRIEF_COMPOSICION_HOME_2026-08-20: un solo pico,
     el H1 baja para dejarle el lugar, y todo tamaño suelto del CSS colapsa a uno
     de estos 7. Objetivo @1440: hero 68 · h2 40 · h3 24 · cuerpo 17 · chico 14 ·
     micro 11. Verificado sobre DOM real, no en frío.
     --t-clima RETOCADO — BRIEF_NUMERO_VIVO_2026-08-20: dejó de vestir el clímax
     "destinos" (190px, retirado) y pasa al numeral vivo de #conteo. Bajado a
     164px@1440 (rango pedido 150–180; Jara: "no obscenamente grande sin decir
     nada") y ≤72px@390. Sigue siendo el único pico — mismo token, mismo cupo
     de los 7 escalones, no uno nuevo. */
  --t-hero:clamp(2.6rem, 1.6rem + 3vw, 4.25rem);
  /* R69 · Krillin, medido: el pico de .cifras/.precio (81px@1440) superaba el h1
     del .fhero (68px@1440) en las 8 fichas -- dos picos en la misma página. El
     tope se ata a --t-hero con un factor <1 para que NUNCA lo alcance en ningún
     ancho (68/58.6/41.6px @1440/1100/390 → tope 57.8/49.8/35.4px), en vez de un
     px fijo que se desalinea la próxima vez que cambie --t-hero. */
  --t-cifra-cap:calc(var(--t-hero) * .85);
  --t-clima:clamp(2.6rem, 2.15rem + 9vw, 10.3125rem);
  --t-h2:clamp(1.9rem, 1.15rem + 1.6vw, 2.5rem);
  --t-h3:clamp(1.35rem, 1.15rem + .6vw, 1.5rem);
  /* BRIEF_28: fijo en 18px (antes fluido 16-17px). El rango viejo dejaba
     micro/chico/cuerpo (11/14/16-17) apretados en una ventana de 6px en los dos
     breakpoints -- defecto estructural del propio token, no de un tamaño suelto.
     18px fijo separa el trío sin tocar el resto de la escala. */
  --t-cuerpo:1.125rem;
  --t-chico:.875rem;
  --t-micro:.6875rem;
}
/* ═══ SYS-ERA ═══════════════════════════════════════════════════════════════
   SPEC_PASADA_ERA_CATALOGO_2026-09-13 §C1. Tokens T1-T4, activados solo bajo
   [data-sys="era"] en <body> (F1: quadrante). Se copia el CRITERIO/PROPORCIÓN
   de ERA (MM 2026-09-08), nunca su identidad literal -- ver spec §C cierre.
   Todo el bloque se porta a otro proyecto copiando este selector + 3 valores
   (spec §E fase F3). */
[data-sys="era"]{
  /* T1 reloj -- MM §2 L11-23: 3 duraciones, no las 12 de transition que había.
     --sys-out reusa el mismo bezier que ya vive en .dosc.plano-ed (línea ~2393),
     no se inventa uno nuevo encima de --ease-out (R72, .16,1,.3,1) que sigue
     gobernando los 74+ usos de UI fuera de este sistema -- eso queda para F2/F3
     si Jara aprueba tocar UI global, no en este prototipo de una ficha. */
  --dur-s:.4s; --dur-m:.8s; --dur-l:1.2s;
  --sys-out:cubic-bezier(.25,1,.5,1);
  /* T2 aire -- B2: ERA u-160 = 144px@1440 / 90px@390. clamp() ya interpola solo
     entre los dos extremos, no hace falta más de un valor. Se aplica solo al
     tier "normal" de .seccion (línea ~386): el tier "apretado" (mapa/amenidades,
     BRIEF_28) es una decisión de densidad deliberada y no se pisa. */
  --sec-pad:clamp(88px, 10vw, 144px);
  /* T3 líneas -- B4: .75px al 100% de tinta, para la línea que SE DIBUJA (M-B).
     --linea (16% alfa) se queda intacta para las filas de tabla: sigue siendo
     ruido de tabla a propósito, no estructura. */
  --hair:.75px;
}
*,*::before,*::after{box-sizing:border-box}
[hidden]{display:none!important}
html{scroll-behavior:smooth}
body{
  margin:0;background:var(--papel);color:var(--asfalto);
  font-family:var(--cuerpo);font-size:var(--t-cuerpo);font-weight:400;
  line-height:1.6;-webkit-font-smoothing:antialiased;
  overflow-x:hidden;
  /* R39: el único elemento fijo sin dueño de espacio que queda es el CTA sticky
     (.aviso-grab murió con el toast). --scta-alto lo publica su propio JS, así que
     la reserva es estática y no hay CLS al encender o apagar la barra. */
  padding-bottom:var(--scta-alto,0px);
}
img,video{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
button,input,select,textarea{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--navy);outline-offset:3px}
.nav :focus-visible,.hero :focus-visible,.fhero :focus-visible,.seccion-osc :focus-visible,
.menu-movil :focus-visible,.pie :focus-visible{outline-color:var(--cielo)}
.env{max-width:var(--ancho);margin-inline:auto;padding-inline:var(--pad)}
.oculto{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* 23-ago: text-wrap:balance en los display. Sin esto, 20 titulares de las 9 páginas
   dejaban una palabra sola en la última línea (medido, no a ojo) — el mismo defecto
   que ya se cachó en las fichas con "entrega." colgando. */
h1,h2,h3{font-family:var(--display);font-weight:330;line-height:1.06;letter-spacing:.002em;margin:0;text-wrap:balance}
/* T4 SYS-ERA (spec §C1/§B1): ERA h2 87.7-88.2% vs 1.06 de Eikonic -- 21% más
   apretado. Solo h2 (regla de Jara 20:15: el h1 se queda en .98/lo que ya tiene,
   un solo pico no se retoca dos veces). Gate de viudas 5 anchos antes de dar por
   bueno -- si aparecen ≥2 viudas nuevas, stop condition del spec §F: vuelve a 1.06. */
[data-sys="era"] h2{line-height:.92}
/* 11-sep: los display ya traían `balance`; el cuerpo no traía nada y la sonda
   canónica (`web_probe.mjs widows`) encontró viudas en las 9 páginas — última
   línea de 9–25% del ancho. `pretty` es la regla para ese caso (balance es para
   titulares de pocas líneas); en navegador que no la soporta, no cambia nada. */
p,li,figcaption,dd{text-wrap:pretty}
h2{font-size:var(--t-h2)}
/* RONDA 37 · T1: el h2 sigue al fondo. Sin esto las 8 fichas corrían entre 4 y 9
   titulares seguidos al mismo tamaño — el "lista de secciones con buena tipografía"
   de awwwards-bar. Ambos derivan de --t-h2, no son números sueltos. */
.seccion-papel-2 h2{font-size:clamp(2.15rem,1.3rem + 1.9vw,2.95rem)}
.seccion-osc h2{font-size:clamp(1.65rem,1.05rem + 1.3vw,2.1rem)}
/* M3 — el h2 de "El proyecto" (primera sección tras .fhero) sigue la escala del
   h1 puente, no la del resto del sitio: ≥0.55× el h1 (spec A3, composition-
   grammar.md) para que el salto de tipo sea gramática del corte, no solo
   apertura. Selector por posición (hermano inmediato de .fhero) — cero HTML
   nuevo, aplica igual en las 8 fichas. */
/* RONDA 35 M3 — 6vw daba 104px@1440, por encima del techo de 96 y compitiendo con
   el h1 del fhero. 5.2vw da 91px@1440 (0.57x el h1 de 160px) y 94px@1920: conserva
   el ≥0.55x que pide el salto de tipo del corte A3, bajo el techo. */
/* RONDA 35 M4 — a 91px el h2 puente daba 4 renglones con la última palabra sola en
   3 de 8 fichas, y balance no lo arregla: "de Providencia." mide ~620px contra una
   columna de ~500, así que la palabra NO PUEDE acompañarse a esa escala — el defecto
   es del tamaño, no del reparto. A 68px sí cabe acompañada.
   Delta declarado contra la ronda anterior: esto rompe a propósito el ≥0.55x del h1
   que fijó el corte A3 (queda en 0.42x). Es la dirección de Jara del 29-ago — "varios
   números enormes que toman demasiado espacio visual" — y el pico único de la ficha
   sigue siendo el h1 del .fhero. Si el salto de tipo del corte hace falta, se
   recupera con el color o el aire, no volviendo a subir esta escala. */
/* balance, no pretty: se probó pretty en esta misma regla y las viudas subieron de
   1 a 3 (hof "El ecosistema del nómada digital" ratio .24, simona .72). Medido, no
   supuesto — el skill dice balance para display y aquí el número lo confirma. */
/* RONDA 43 · T4 (OK Jara 31-ago). Esta regla daba 67.84px@1440 — calibrada contra un h1
   de 104px (ratio 0.65). Con el h1 en --t-hero (68px) el h2 de "El proyecto" quedaba del
   MISMO tamaño que el titular y la ficha perdía su pico único: 68 vs 67.84. El h2 vuelve
   a --t-h2 (40px), la escala del resto del sitio, y el ratio queda en 0.59 — el pico es
   otra vez el h1 del hero, uno solo por página. */
.fhero + .seccion h2{font-size:var(--t-h2);max-width:20ch}
h3{font-size:var(--t-h3);line-height:1.15}
p{margin:0 0 1em;max-width:68ch}
h2+p{margin-top:20px}

.eyebrow{
  font-family:var(--cuerpo);font-size:var(--t-micro);font-weight:500;
  letter-spacing:.22em;text-transform:uppercase;color:var(--niebla-papel);margin:0 0 18px
}
.seccion-osc .eyebrow{color:var(--niebla)}

/* ── botones ─────────────────────────────────────────────── */
.btn{
  display:inline-flex;align-items:center;gap:10px;
  min-height:52px;padding:0 28px;border:1px solid var(--navy);border-radius:var(--r);
  background:var(--navy);color:var(--blanco);
  font-size:var(--t-chico);font-weight:500;letter-spacing:.02em;cursor:pointer;
  transition:background .24s ease,color .24s ease,border-color .24s ease,transform .24s ease;
}
.btn:hover{background:var(--tinta);border-color:var(--tinta);transform:translateY(-2px)}
.btn--fantasma{background:transparent;color:var(--blanco);border-color:rgba(245,242,236,.5)}
/* Sobre fondo oscuro el primario se invierte: el relleno navy no se separaba de la tinta
   (1.48:1) y en hover tomaba el color exacto de la sección (1.00:1). */
.nav .btn,.hero .btn:not(.btn--fantasma),.fhero .btn:not(.btn--fantasma),.seccion-osc .btn:not(.btn--fantasma){
  background:var(--blanco);color:var(--tinta);border-color:var(--blanco)}
.nav .btn:hover,.hero .btn:not(.btn--fantasma):hover,.fhero .btn:not(.btn--fantasma):hover,.seccion-osc .btn:not(.btn--fantasma):hover{
  background:var(--cielo);color:var(--tinta);border-color:var(--cielo)}
.hero .btn--fantasma:hover,.fhero .btn--fantasma:hover,.seccion-osc .btn--fantasma:hover{
  background:rgba(245,242,236,.16);color:var(--blanco);border-color:var(--blanco)}
.btn--fantasma:hover{background:var(--blanco);color:var(--tinta);border-color:var(--blanco)}
.btn .flecha{transition:transform .24s ease}
.btn:hover .flecha{transform:translate(3px,-3px)}

/* ── nav ─────────────────────────────────────────────────── */
.nav{
  position:fixed;inset:0 0 auto 0;z-index:60;
  display:flex;align-items:center;justify-content:space-between;
  gap:24px;padding:14px var(--pad);
  background:transparent;transition:background .3s ease,box-shadow .3s ease,padding .3s ease;
}
.nav.pegado{background:rgba(12,22,38,.94);backdrop-filter:blur(10px);box-shadow:0 1px 0 var(--linea-osc)}
/* Velo del nav (30-ago). La tinta del nav es --blanco y el fondo es transparent, así que
   el contraste lo decidía la FOTO del hero de cada ficha: medido con contraste_glifos.py
   daba 1.88 en madison, 2.29 kartesia, 2.86 quadrante — 6 de 8 fichas bajo el piso AA 4.5,
   y solo hof (8.01) y nikté (6.77) pasaban, por tener hero oscuro. Un degradado propio hace
   que el número deje de depender de qué foto le toque a la ficha. Va en ::before con
   z-index:-1 para no tapar los clics, y se apaga en .pegado, que ya trae su fondo sólido. */
.nav::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:linear-gradient(to bottom,rgba(12,22,38,.82) 0%,rgba(12,22,38,.62) 55%,rgba(12,22,38,0) 100%);
  transition:opacity .3s ease;
}
.nav.pegado::before{opacity:0}
/* FASE4 D4-a — el panel Lock-Off (texto blanco sobre navy) cruza bajo el
   header pegado (navy también, .94 opacidad) durante el scroll normal.
   Medido: la ventana de colisión en scrollY = altura del elemento + altura
   del header (56.8px @1440/64px @390) es geométrica, invariante a padding
   antes o después del texto -- verificado empíricamente (subir el
   padding-bottom del panel no movió ni una parada del barrido). La solución
   real: el header pegado se desvanece en su borde inferior en vez de cortar
   en seco -- cualquier texto pasando por debajo se funde, no se decapita.
   Scopeado a HOF vía :has() porque .nav es compartida por las 8 fichas. */
body:has(.fhero--r55) .nav.pegado{background:transparent}
/* z-index:-1 en ::after de un descendiente de position:fixed lo manda DETRAS
   del contenido de scroll de la pagina entera, no solo detras de los hijos
   propios de .nav -- medido: con z:-1 el degradado es invisible (tapado por
   el panel navy scrolleando encima); con z:1 se ve pero cubre el propio
   texto del nav. Fix: z:0 (auto) en el pseudo + z:1 explicito en los hijos
   reales del nav, para que el texto quede siempre encima del degradado. */
body:has(.fhero--r55) .nav.pegado > *{position:relative;z-index:1}
body:has(.fhero--r55) .nav.pegado::after{
  content:"";position:absolute;top:0;left:0;right:0;bottom:-28px;z-index:0;pointer-events:none;
  background:linear-gradient(to bottom,rgba(12,22,38,.94) 0%,rgba(12,22,38,.94) 67%,rgba(12,22,38,0) 100%);
  backdrop-filter:blur(10px);
  -webkit-mask-image:linear-gradient(to bottom,black 0%,black 67%,transparent 100%);
          mask-image:linear-gradient(to bottom,black 0%,black 67%,transparent 100%);
}
/* zona táctil ≥44px sin mover el svg: el padding vive en el <a>, el borde de hover en .ln
   (auditoría §tactil, última pasada) */
.nav .marca{color:var(--blanco);display:flex;align-items:center;padding:13px 0;margin:-13px 0}
.nav .marca .ln{display:flex;align-items:center}
.nav .marca svg{height:22px;width:auto;display:block}
.nav .liga{
  color:var(--blanco);font-size:var(--t-chico);letter-spacing:.14em;text-transform:uppercase;font-weight:500;
  padding:8px 0;border-bottom:1px solid transparent;transition:color .2s ease,border-color .2s ease
}
.nav .liga:hover{color:var(--blanco);border-bottom-color:var(--blanco)}
/* La liga ya nace en --papel (contraste sobre el video); el hover se distingue
   por el subrayado, no por un cambio de color que ya no tiene a dónde subir. */
.nav ul{display:flex;gap:28px;list-style:none;margin:0;padding:0;align-items:center}
.nav .btn{min-height:42px;padding:0 18px;font-size:var(--t-chico)}
.menu-btn{display:none;flex-direction:column;justify-content:center;gap:5px;width:44px;height:44px;
  padding:0 10px;background:none;border:0;cursor:pointer;flex:none}
@media (max-width:900px){ .nav ul{display:none} .nav{padding:12px 20px} .menu-btn{display:flex} }
/* Con ratón el piso de puntero fino es 24×24 (WCAG 2.5.8) — 34/42px lo pasa de sobra, no se toca.
   Con dedo (tablet en horizontal, p.ej. 1024px con el nav de escritorio activo) sí hace falta 44px.
   .liga no tiene fondo propio: padding+margen negativo, cero cambio de layout. .btn sí tiene fondo/
   borde — crecer su padding le crecería el color visible, así que la zona extra vive en un ::after
   invisible con inset negativo, nunca en su caja real. */
@media (pointer:coarse){
  .nav .liga{padding:14px 0;margin:-14px 0}
  .nav .btn{position:relative}
  .nav .btn::after{content:"";position:absolute;inset:-3px}
}
@media (max-width:480px){
  .nav{padding:10px 14px;gap:8px}
  .nav .marca svg{height:18px}
  .nav .btn{padding:0 13px;font-size:var(--t-micro);white-space:nowrap;min-height:38px}
  /* 44px de ancho real (antes 40, único target bajo el piso). El padding derecho se queda en 8px
     para que el icono no se mueva ni un píxel: todo el ancho nuevo se lo lleva el padding
     izquierdo, invisible (auditoría §tactil, última pasada). */
  .menu-btn{width:44px;padding:0 8px 0 12px}
  .menu-movil{padding-top:64px}
}
.menu-btn span{display:block;height:1.5px;background:var(--blanco);transition:transform .26s ease,opacity .2s ease}
.menu-btn[aria-expanded="true"] span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
.menu-btn[aria-expanded="true"] span:nth-child(2){opacity:0}
.menu-btn[aria-expanded="true"] span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}
.menu-movil{position:fixed;top:0;left:0;right:0;z-index:55;padding:74px 20px 18px;
  background:rgba(12,22,38,.97);backdrop-filter:blur(10px);border-bottom:1px solid var(--linea-osc);
  display:flex;flex-direction:column}
.menu-movil a{padding:15px 2px;color:var(--cielo);font-size:var(--t-chico);letter-spacing:.12em;
  text-transform:uppercase;border-bottom:1px solid var(--linea-osc);transition:color .2s ease}
.menu-movil[hidden]{display:none}
.menu-movil a:last-child{border-bottom:0}
.menu-movil a:active,.menu-movil a:hover{color:var(--blanco)}
@media (min-width:901px){ .menu-movil{display:none} }

/* ── hero ────────────────────────────────────────────────── */
.hero{position:relative;min-height:100svh;display:flex;flex-direction:column;justify-content:flex-end;background:var(--tinta);overflow:hidden}
.hero__media{position:absolute;inset:0;z-index:0}
.hero__media video,.hero__media img{width:100%;height:100%;object-fit:cover}
.hero__media video{opacity:0;transition:opacity 1.2s ease}
.hero__media video.listo{opacity:1}
.hero__velo{
  position:absolute;inset:0;z-index:1;
  /* Velo aligerado (17-ago): el par de gradientes llegaba a .72 lateral y .86 abajo y
     dejaba la imagen turbia — se leía "opaca", no "cinematográfica". Se conserva la
     carga SOLO donde vive texto: la banda inferior y el arranque izquierdo. */
  /* Vertical bajado (19-ago): a .72 en el borde inferior contaminaba de navy
     TODO el ancho, incluida la mitad derecha que se acaba de liberar arriba.
     El respaldo de la franja lo da el ::before local (izquierda), no este. */
  background:
    linear-gradient(180deg, rgba(12,22,38,.18) 0%, rgba(12,22,38,.09) 18%, rgba(12,22,38,.30) 36%, rgba(12,22,38,.32) 62%, rgba(12,22,38,.20) 76%, rgba(12,22,38,.16) 86%, rgba(12,22,38,.30) 100%),
    linear-gradient(100deg, rgba(12,22,38,.56) 0%, rgba(12,22,38,.28) 30%, rgba(12,22,38,.04) 56%, rgba(12,22,38,0) 70%);
}
/* Escudo local del texto. Aligerar el velo global dejó respirar el video, pero
   destapó los rótulos chicos: medido contra el PÍXEL MÁS CLARO de su propia caja
   (no contra el fondo promedio), el eyebrow del hero caía a 2.35:1. En vez de
   volver a oscurecer toda la imagen, la carga va SOLO donde vive la tipografía. */
/* padding-left:21px en hero__cuerpo/.cabeza/.filtros — BRIEF_COMPOSICION_HOME_2026-08-20
   gate 3: los alinea al mismo eje x=185 que ya usa el contenido de las cartas (grid + borde
   1px + padding interno 20px de .carta__cuerpo, que no se toca en esta ronda). Un eje, no
   dos casi iguales. .linea NO se toca: sus rótulos "Hoy"/"2031" marcan los extremos del
   riel, no texto de lectura — moverlos rompería la escala visual del timeline. */
.hero__cuerpo{position:relative;z-index:2;padding-bottom:clamp(28px,4vh,44px);padding-top:120px;padding-left:21px}
/* BRIEF_HERO_VELO_BORDES_2026-08-20: el escudo estaba anclado a .hero__cuerpo (el
   bloque de copy, que arranca en x=324,y=200 a 1440 por su padding), no al hero. Eso
   dejaba un canto vertical duro a la izquierda (36px de imagen sin cubrir) y un canto
   horizontal duro cruzando toda la pantalla en y=200. Reanclado a .hero completo: nace
   pegado a su borde izquierdo y superior de verdad, y el -20vw/70vw ya no depende del
   padding de .env — solo del ancho del hero, que es el mismo ancho del viewport. */
.hero::before{
  content:"";position:absolute;z-index:1;
  /* Ancho recortado a 70vw (18-ago→19-ago, medido): a 1620px fijos el borde
     opaco llegaba a 92.5% del ancho y dejaba solo ~7.5% de render limpio.
     70vw hace que el borde caiga siempre a 50vw = 50% del viewport, sea cual
     sea el ancho — cumple el ≥45% sin velo del spec en cualquier breakpoint. */
  inset:0 auto 0 -20vw;width:70vw;
  /* BRIEF_COSTURA_Y_AVISO_2026-08-20: a 96deg la línea del stop alfa-0 no es vertical,
     así que en la franja de arriba del hero el alfa todavía valía algo cuando la caja
     se cortaba en su borde derecho (x=720 a 1440) — un salto de 25.6 en 8px, medido.
     90deg hace que las líneas de stop sean verticales: coinciden con el borde recto
     de la caja en TODA la altura, no solo donde se midió — el alfa llega a 0 exacto
     en el borde sin importar la fila. (Primer intento adelantó el stop de 0 a 92%
     "por margen": con 90deg ese margen no hace falta, y solo comprime el degradado
     — sube el delta por ventana en la zona visible. Se revirtió a los mismos % de
     antes, solo cambió el ángulo.) */
  background:linear-gradient(90deg, rgba(10,19,34,.88) 0%, rgba(10,19,34,.82) 54%,
             rgba(10,19,34,.56) 76%, rgba(10,19,34,.30) 90%, rgba(10,19,34,0) 100%);
  pointer-events:none}
@media (max-width:760px){ .hero::before{inset:0 -8vw 0 -8vw;width:auto;
  /* Bajado (19-ago) junto con el navy global del home: a .86 en el fondo
     empujaba el navy medido a móvil por arriba del 12% del spec. El primer
     stop subió de vuelta a .34 — medido en el eyebrow real, .16 caía a
     3.88:1 sobre un parche claro del cielo del video, bajo AA. */
  background:linear-gradient(180deg, rgba(10,19,34,.34) 0%, rgba(10,19,34,.58) 38%, rgba(10,19,34,.66) 100%)} }
.hero .eyebrow{color:var(--blanco)}
.hero h1{
  font-size:var(--t-hero);color:var(--blanco);letter-spacing:-.028em;max-width:16ch;
  margin:0 0 22px
}
.hero h1 .b{display:block}
/* La 2a línea iba en --cielo: azul claro sobre un video con partes claras.
   Nominalmente pasaba AA, pero se leía lavada. El titular va entero en --papel. */
.hero h1 .b:last-child{color:var(--blanco)}
.hero__sub{color:var(--blanco);max-width:44ch;font-weight:300;font-size:var(--t-cuerpo);margin:0 0 30px}
.hero__acciones{display:flex;flex-wrap:wrap;gap:14px}

/* franja de datos duros — la prueba en el primer viewport */
.franja{
  position:relative;z-index:2;border-top:1px solid var(--linea-osc);
  margin-top:clamp(26px,4.5vh,44px)
}
.franja ul{
  display:flex;flex-wrap:wrap;gap:8px clamp(26px,4vw,54px);
  list-style:none;margin:0;padding:16px 0 0
}
.franja li{display:flex;align-items:baseline;gap:10px}
.franja .n{font-family:var(--display);font-size:var(--t-h3);color:var(--blanco);line-height:1}
.franja .e{font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;color:var(--niebla)}
@media (max-width:700px){
  .hero__cuerpo{padding-top:96px}
}
@media (max-width:520px){
  .hero__acciones{display:grid;grid-template-columns:1fr;gap:10px}
  .hero__acciones .btn{justify-content:center}
}

/* ── secciones — ritmo por densidad, BRIEF_28 (antes: metrónomo, 1 solo padding) ──
   Decisión de densidad, no de números al azar — 3 tiers por rol:
   · respiro  (.inversion-clara): el precio es el momento firma; necesita el aire
     de una sola idea en pantalla, igual que el hero.
   · normal   (.seccion default): intro/diferenciador/galería/modelos/portafolio —
     contenido que se lee, no se escanea.
   · apretado (mapa, amenidades, contacto): datos y listas que el usuario escanea
     por renglón, no por párrafo — el aire extra ahí solo alarga el scroll sin
     agregar lectura. */
.seccion{padding:clamp(64px,9vw,120px) 0}
.inversion-clara{padding:clamp(84px,11vw,148px) 0}
#pedir,.seccion:has(.mapbox),.seccion:has(.amen){padding:clamp(48px,6vw,84px) 0}
/* T2 SYS-ERA (spec §C1/§D5): solo el tier "normal" sube al aire de ERA; el tier
   "apretado" (mapa/amenidades/#pedir, regla de arriba, más específica y por eso
   gana igual) se queda como está -- es densidad deliberada, no un número suelto. */
[data-sys="era"] .seccion:not(:has(.mapbox)):not(:has(.amen)){padding:var(--sec-pad) 0}
/* --cielo deja de cargar texto de lectura (17-ago). El sistema lo define como
   "texto suave sobre oscuro", y nominalmente pasa AA — pero a 14-16px sobre navy
   se lee lavado, y encima del video del hero directamente se pierde. Se queda en su
   otro rol: acentos, fondos y estados de foco. El texto sobre oscuro va en --papel. */
.seccion-osc{background:var(--tinta);color:var(--blanco)}
.seccion-osc h2,.seccion-osc h3{color:var(--blanco)}
/* Bandas navy con foto (19-ago): el spec prohíbe navy plano sin imagen o dato
   grande adentro. Curaduría y asesoría eran relleno liso — ahora cargan la
   foto de un desarrollo real detrás, con un scrim parejo encima para que el
   texto blanco siga en AA. El scrim se midió más oscuro que .82 para no
   perder contraste frente al --tinta sólido que reemplaza. */
.seccion-osc--foto{position:relative;background-size:cover;background-position:center;isolation:isolate}
.seccion-osc--foto::before{
  content:"";position:absolute;inset:0;z-index:0;
  background:rgba(10,19,34,.58);
}
.seccion-osc--foto>.env{position:relative;z-index:1}
/* respaldo local SOLO detrás de cada bloque de texto — el tint parejo de arriba
   se bajó a .58 para que la foto rompa el muestreo de navy-plano del QA; sin
   este respaldo el texto sobre las zonas claras de la foto pierde AA. */
.seccion-osc--foto .txt-respaldo{
  background:rgba(9,16,28,.62);backdrop-filter:blur(2px);
  padding:clamp(20px,3vw,40px);border-radius:2px;
}
#curaduria.seccion-osc--foto{background-image:url(assets/nikte/hero.webp)}
#asesoria.seccion-osc--foto{background-image:url(assets/montevideo/hero.webp)}
.seccion-papel{background:var(--papel)}
.seccion-papel-2{background:var(--papel-2)}
/* RONDA 37: --niebla-papel (#5D696E) está calibrado contra --papel (4.89:1); sobre
   --papel-2 cae a 4.33 — bajo AA. Con el ritmo tonal cualquier sección puede caer
   aquí, así que se corrige la VARIABLE en el ámbito, no selector por selector.
   #576367 = 4.75:1 sobre --papel-2 (medido, no estimado). */
.seccion-papel-2{--niebla-papel:#576367}
/* --niebla-papel (#5D696E) está medido contra --papel (5.4:1). Sobre --papel-2,
   que es más oscuro, cae a 3.74:1 real — falla en los 11-14px de las cartas de
   casa desarrolladora. Sobre tint, la metadata va en asfalto. */
.seccion-papel-2 .dim,.seccion-papel-2 .eyebrow,.seccion-papel-2 .meta,
.seccion-papel-2 small,.seccion-papel-2 .carta__pie,
.seccion-papel-2 .dev__n,.seccion-papel-2 .dev__lista{color:var(--asfalto)}
/* desarrolladores + preguntas dejaron de ser banda plana de --papel-2 (13.6% de
   la página, BRIEF_PUNCH_HOME_2026-08-19 hallazgo 1) — corren en --papel como el
   resto del sitio. La costura entre ambas es una sola regla, no dos fondos. */
#preguntas{border-top:1px solid var(--linea)}

/* ── ARCHIVADO 2026-08-20 (BRIEF_NUMERO_VIVO) ───────────────────────
   El clímax tipográfico "destinos a escala grande" (sección .seccion-clima +
   botones .clima__link "Guadalajara"/"Riviera Nayarit") se retiró: repartía
   7-contra-1 la pantalla más grande del sitio a un filtro, sin decir nada.
   El filtro de destino sigue vivo en #fCiudad — no se perdió función, solo
   la vitrina. Markup y CSS completos abajo, comentados, no borrados:

<section class="seccion seccion-clima" aria-label="Elige destino">
  <div class="env">
    <button type="button" class="clima__link" data-filtro="ciudad" data-valor="GDL">Guadalajara</button>
    <button type="button" class="clima__link" data-filtro="ciudad" data-valor="Riviera Nayarit">Riviera Nayarit</button>
  </div>
</section>

.seccion-clima{background:var(--tinta);padding-block:clamp(72px,12vw,160px)}
.clima__link{
  display:block;width:100%;text-align:left;background:none;border:0;border-top:1px solid var(--linea-osc);
  font-family:var(--display);font-weight:330;font-size:var(--t-clima);line-height:.94;letter-spacing:-.035em;
  color:var(--blanco);text-decoration:none;cursor:pointer;padding:.16em 0;margin:0;
  transition:color .24s ease,padding-left .24s ease;
}
.clima__link:first-child{border-top:0}
.clima__link:hover,.clima__link:focus-visible{color:var(--cielo);padding-left:.14em}
.clima__link[aria-pressed="true"]{color:var(--cielo)}
@media (prefers-reduced-motion:reduce){ .clima__link{transition:none} }
──────────────────────────────────────────────────────────────────── */
/* ritmo de sección — BRIEF_COMPOSICION_HOME_2026-08-20 gate 4: 3 valores de
   padding-top en vez del metrónomo a 120px parejo (el 160 del pico se fue con
   .seccion-clima; siguen 0/84/120 — el gate pide ≥3, sigue cumpliendo). */
#desarrolladores,#preguntas,#contacto{padding-block:clamp(48px,7vw,84px)}

/* ── número vivo del portafolio — BRIEF_NUMERO_VIVO_2026-08-20 ──────
   El pico tipográfico se muda aquí: el conteo real de #conteo, atado al
   filtro. Composición por contraste de escala en la misma línea base
   (numeral grande + frase chica), no un glifo suelto. */
.conteo{display:flex;align-items:baseline;flex-wrap:wrap;column-gap:.32em;row-gap:10px;margin:0 0 32px}
.conteo__num{
  font-family:var(--display);font-weight:330;font-size:var(--t-clima);line-height:.86;
  letter-spacing:-.03em;color:var(--navy);
}
.conteo__frase{font-size:var(--t-chico);color:var(--niebla-papel);max-width:34ch}
.conteo__frase b{color:var(--navy);font-weight:500}
.conteo__ver{background:none;border:0;padding:0;margin-left:.35em;color:var(--navy);
  text-decoration:underline;cursor:pointer;font:inherit}
.conteo--cero .conteo__linea{font-size:var(--t-h3);color:var(--asfalto);margin:0 0 10px;width:100%}
.conteo--cero .conteo__sugerencia{
  background:none;border:0;padding:0;text-align:left;width:100%;
  font-size:var(--t-chico);color:var(--niebla-papel);cursor:pointer;
  text-decoration:underline;text-decoration-color:var(--linea);text-underline-offset:3px;
}
@media (prefers-reduced-motion:reduce){ .conteo__num{transition:none} }
.cabeza{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.85fr);gap:clamp(20px,4vw,64px);align-items:end;margin-bottom:clamp(36px,5vw,60px);padding-left:21px}
.cabeza p{margin:0}
@media (max-width:860px){ .cabeza{grid-template-columns:1fr;align-items:start;gap:18px} }

/* (el eje de tarjetas de entrega se retiró 16-ago: dominaba la vista sobre las
   propiedades. El filtro de entrega ahora es un grupo de chips más; la línea
   de tiempo de abajo conserva la firma.) */

/* ── filtros ─────────────────────────────────────────────── */
.filtros{display:flex;flex-wrap:wrap;gap:22px 40px;align-items:center;margin-bottom:26px;padding-left:21px}
.grupo{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.grupo__rot{font-size:var(--t-micro);letter-spacing:.16em;text-transform:uppercase;color:var(--niebla-papel);margin-right:4px}
.chip{
  min-height:38px;padding:0 16px;border:1px solid var(--linea);border-radius:var(--r);
  background:transparent;color:var(--navy);font-size:var(--t-chico);font-weight:500;cursor:pointer;
  transition:background .2s ease,color .2s ease,border-color .2s ease;
}
.chip:hover{background:rgba(23,53,95,.07);border-color:var(--navy)}
.chip[aria-pressed="true"]{background:var(--navy);color:var(--blanco);border-color:var(--navy)}
.conteo{font-size:var(--t-chico);color:var(--niebla-papel);margin:0 0 26px;padding-left:21px}
.conteo b{color:var(--navy);font-weight:700}

/* ── filtros en móvil: barra + panel ─────────────────────────
   El problema medido: en 390×844 el bloque de controles ocupaba 643px del eje a la
   primera card — 76% del viewport. Filtro y resultado nunca cabían juntos.
   Dos arreglos que se suman: (1) los controles se compactan, (2) al pasarlos de largo
   se levantan como panel desde una barra inferior. */
.panel__cab,.barraf,.velo{display:none}

@media (max-width:760px){
  /* una fila por grupo, con desplazamiento lateral en vez de envolverse */
  .filtros{display:grid;grid-template-columns:1fr;gap:9px;margin-bottom:14px}
  .grupo{flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-x:contain;gap:7px;
    scrollbar-width:none;-ms-overflow-style:none}
  .grupo::-webkit-scrollbar{display:none}
  .grupo>*{flex:0 0 auto}
  /* la etiqueta se queda anclada: si se fuera con el scroll, la fila perdería su nombre */
  .grupo__rot{position:sticky;left:0;z-index:1;margin-right:0;padding-right:9px;
    background:linear-gradient(90deg,var(--papel) 78%,rgba(245,242,236,0))}
  .chip{min-height:36px;padding:0 13px}
  .conteo{margin-bottom:16px}
  /* el ajuste automático de Chrome pelea con el reacomodo al filtrar */
  .grid{overflow-anchor:none}

  /* (2) panel: el mismo #controles, levantado */
  /* El aviso de grabación también es fixed/bottom:0. En la primera visita —la del
     cliente— tapaba la barra entera. R39: el toast se retiró (el aviso vive en el
     pie), así que ya no hay nada abajo que esquivar y la barra vuelve a bottom:0. */
  #controles.en-panel{
    position:fixed;left:0;right:0;bottom:0;z-index:80;
    max-height:min(74svh,560px);overflow-y:auto;overscroll-behavior:contain;
    padding:0 var(--pad) calc(16px + env(safe-area-inset-bottom));
    background:var(--papel);border-top:1px solid var(--linea);
    box-shadow:0 -18px 40px rgba(12,22,38,.22);
    animation:subePanel .3s var(--ease-out)
  }
  @keyframes subePanel{from{transform:translateY(100%)}to{transform:none}}
  #controles.en-panel .panel__cab{
    display:flex;align-items:center;justify-content:space-between;gap:16px;
    position:sticky;top:0;z-index:2;background:var(--papel);
    padding:14px 0 12px;margin-bottom:2px;border-bottom:1px solid var(--linea)}
  .panel__t{font-size:var(--t-micro);letter-spacing:.16em;text-transform:uppercase;color:var(--niebla-papel)}
  .panel__x{min-height:40px;padding:0 18px;border:0;border-radius:var(--r);
    background:var(--navy);color:var(--blanco);font:inherit;font-size:var(--t-chico);font-weight:700;cursor:pointer}
  #controles.en-panel .grupo__rot{background:linear-gradient(90deg,var(--papel) 78%,rgba(245,242,236,0))}
  #controles-hueco:not([hidden]){display:block}

  .velo:not([hidden]){display:block;position:fixed;inset:0;z-index:75;
    background:rgba(12,22,38,.5);animation:apareceVelo .3s ease}
  @keyframes apareceVelo{from{opacity:0}to{opacity:1}}

  .barraf:not([hidden]){display:block;position:fixed;left:0;right:0;bottom:0;z-index:72;
    padding:10px var(--pad) calc(10px + env(safe-area-inset-bottom));
    background:rgba(245,242,236,.94);backdrop-filter:blur(10px);
    border-top:1px solid var(--linea);animation:subeBarra .26s var(--ease-out)}
  @keyframes subeBarra{from{transform:translateY(100%)}to{transform:none}}
  .barraf__b{display:flex;align-items:center;gap:12px;width:100%;min-height:48px;
    padding:0 16px;border:1px solid var(--navy);border-radius:var(--r);
    background:var(--navy);color:var(--blanco);font:inherit;text-align:left;cursor:pointer;
    transition:background .2s ease,color .2s ease}
  /* §3 del manual: hover cambia ≥2 propiedades y el foco se ve sobre fondo navy
     (el outline global es navy y aquí sería invisible) */
  .barraf__b:hover{background:var(--tinta);border-color:var(--cielo)}
  .barraf__b:hover .barraf__n{background:var(--blanco)}
  .barraf__b:focus-visible{outline-color:var(--cielo)}
  .barraf__b:active{transform:scale(.99);transition-duration:.1s}
  .panel__x{transition:background .2s ease,color .2s ease,transform .16s ease}
  .panel__x:hover{background:var(--tinta);color:var(--cielo)}
  .panel__x:active{transform:scale(.97);transition-duration:.1s}
  .barraf__ico{display:flex;flex-direction:column;justify-content:center;gap:3px;width:15px;flex:none}
  .barraf__ico i{display:block;height:1.5px;background:var(--cielo)}
  .barraf__ico i:nth-child(2){width:70%}
  .barraf__ico i:nth-child(3){width:40%}
  .barraf__txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
  .barraf__txt b{font-size:var(--t-chico);font-weight:700}
  .barraf__txt span{font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;
    color:var(--blanco);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .barraf__txt span:empty{display:none}
  .barraf__n{flex:none;min-width:26px;height:26px;padding:0 8px;border-radius:13px;
    display:flex;align-items:center;justify-content:center;
    background:var(--cielo);color:var(--navy);font-size:var(--t-chico);font-weight:700}
  /* la barra no puede tapar la última tarjeta */
  body.con-barraf #portafolio{padding-bottom:96px}
}
@media (max-width:760px) and (prefers-reduced-motion:reduce){
  #controles.en-panel,.velo:not([hidden]),.barraf:not([hidden]){animation:none}
  .barraf__b:active,.panel__x:active{transform:none}
}

/* ── grid de portafolio ──────────────────────────────────── */
/* 2 columnas siempre: menos piezas por pantalla, cada una más grande
   (mediana 266px→400+px, medido en site/AIRE_2026-08-18.md). */
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(14px,1.6vw,22px)}
@media (max-width:640px){ .grid{grid-template-columns:1fr} }

/* ── ARCHIVADO 2026-08-20 (BRIEF_CARDS_VARIANTE_B) ──────────────────
   La card A (borde 1px, radio 2px, fondo crema, cuerpo separado de la
   foto) se retiró: la muestra premiada de Awwwards (Village Properties,
   Seneca Group) corre border-0/radio-0/fondo-transparente sin excepción.
   .carta y .carta__foto se REUTILIZAN (mismo nombre, otra regla — el
   filtro/timeline/hover en JS depende de esos selectores) — lo que sigue
   es la regla vieja completa, comentada, no borrada:

.carta{
  position:relative;display:flex;flex-direction:column;background:var(--papel);
  border:1px solid var(--linea);border-radius:var(--r);overflow:hidden;
  transition:border-color .26s ease,background .26s ease,transform .26s ease;
}
.carta:hover{border-color:var(--navy);background:#fff;transform:translateY(-3px)}
.carta:hover .carta__cta{background:var(--navy);color:var(--blanco)}
.carta__cta .flecha{display:inline-block;transition:transform .24s ease}
.carta:hover .carta__cta .flecha{transform:translate(2px,-2px)}
.carta__foto{position:relative;flex:none;aspect-ratio:4/3;overflow:hidden;background:var(--papel)}
.carta__foto img{width:100%;height:100%;object-fit:cover;transition:transform .6s var(--ease-out)}
.carta:hover .carta__foto img{transform:scale(1.045)}
.carta__marca{position:absolute;left:0;bottom:0;z-index:2;display:flex;gap:5px;padding:8px 10px;background:rgba(12,22,38,.82)}
.carta__marca span{width:6px;height:6px;border-radius:50%;background:var(--cielo)}
.carta__marca em{font-style:normal;font-size:var(--t-micro);letter-spacing:.12em;text-transform:uppercase;color:var(--cielo);margin-left:6px}
.carta__cuerpo{padding:20px 20px 22px;display:flex;flex-direction:column;flex:1}
.carta__lugar{font-size:var(--t-micro);letter-spacing:.16em;text-transform:uppercase;color:var(--niebla-papel);margin:0 0 10px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.carta__lugar .indice{letter-spacing:.08em;color:var(--navy);font-weight:500}
.pill{border:1px solid currentColor;border-radius:999px;padding:2px 10px;letter-spacing:.14em}
.carta h3{margin:0 0 14px}
.carta__datos{display:grid;grid-template-columns:repeat(2,1fr);gap:10px 16px;margin:0 0 18px;border-top:1px solid var(--linea);padding:14px 0 0;list-style:none}
.carta__datos li{font-size:var(--t-chico);color:var(--navy)}
.carta__datos b{display:block;font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;color:var(--niebla-papel);font-weight:700;margin-bottom:3px}
.carta__pie{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--linea);padding-top:14px}
.carta__precio{font-family:var(--display);font-size:var(--t-h3);line-height:1}
.carta__precio small{font-family:var(--cuerpo);font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;color:var(--niebla-papel);display:block;margin-bottom:4px}
.carta__cta{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 16px;border:1px solid var(--navy);border-radius:var(--r);background:transparent;color:var(--navy);font-size:var(--t-chico);font-weight:500;cursor:pointer;white-space:nowrap;transition:background .22s ease,color .22s ease}
.carta__cta:hover{background:var(--navy);color:var(--blanco)}
.carta--destacada{grid-column:span 2}
.carta--destacada .carta__foto{aspect-ratio:16/9}
.carta--destacada h3{font-size:var(--t-h2)}
.carta--destacada .carta__nota{font-size:var(--t-chico);color:var(--navy);opacity:.8;margin:-6px 0 16px;max-width:56ch}
──────────────────────────────────────────────────────────────────── */

/* ── card variante B — la foto ES la tarjeta ─────────────────────────
   Cero borde, cero radio, cero fondo (gate 2). Prototipo aprobado:
   site/_proto/carta.html. Tamaños de texto colapsados a los 7 tokens
   del sitio (h3→--t-h2, precio→--t-cuerpo, zona/datos→--t-micro) —
   el prototipo usaba 2.125rem/12px sueltos, aquí no se suman tamaños. */
.carta{position:relative;display:block;color:inherit;text-decoration:none;overflow:hidden;isolation:isolate}
.carta.fuera{display:none}
/* ── línea de tiempo del portafolio ─────────────────────────
   Vive sobre papel, no sobre tinta: los colores son los del tema claro.
   La etiqueta aparece al pasar encima o al enfocar con teclado — con ocho
   nombres largos, ponerlas todas fijas las encimaba hasta hacerlas ilegibles
   (medido: se superponían en el mismo renglón). El punto siempre está; el
   nombre llega cuando se pide. */
.linea{position:relative;height:78px;margin:30px 0 34px}
.linea__riel{position:absolute;left:0;right:0;top:22px;height:1px;
  background:linear-gradient(90deg,var(--navy),rgba(23,53,95,.18))}
.linea__rot{position:absolute;top:0;font-size:var(--t-micro);letter-spacing:.18em;
  text-transform:uppercase;color:var(--niebla-papel)}
.linea__rot.ini{left:0} .linea__rot.fin{right:0}
.hito{position:absolute;left:var(--x);top:14px;transform:translateX(-50%) translateX(var(--dx,0px));
  background:none;border:0;padding:3px;cursor:pointer;line-height:0;
  transition:opacity .3s ease}
.hito__pt{display:block;width:9px;height:9px;border-radius:50%;
  background:var(--navy);border:2px solid var(--papel);
  transition:transform .26s var(--ease-out),background .26s ease}
.hito__et{position:absolute;left:50%;top:30px;transform:translateX(-50%) translateY(-4px);
  white-space:nowrap;font-size:var(--t-micro);line-height:1.4;text-align:center;
  padding:7px 11px;background:var(--tinta);color:var(--blanco);border-radius:var(--r);
  opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease;z-index:3}
.hito__et b{display:block;font-size:var(--t-micro);font-weight:700;letter-spacing:.01em}
.hito__et span{color:var(--blanco);letter-spacing:.06em;text-transform:uppercase;font-size:var(--t-micro)}
.hito:hover,.hito:focus-visible{outline:none}
.hito:hover .hito__pt,.hito:focus-visible .hito__pt{transform:scale(1.6)}
.hito:hover .hito__et,.hito:focus-visible .hito__et{opacity:1;transform:translateX(-50%) translateY(0)}
.hito:focus-visible .hito__pt{box-shadow:0 0 0 3px var(--navy)}
.hito.apagado{opacity:.22;pointer-events:none}
.carta.senalada{outline:2px solid var(--cielo);outline-offset:-4px}
@media (max-width:900px){ .linea{display:none} }

/* ── proporciones del sitio — tres declaradas, no una mezcla accidental ──
   16/10  → cualquier foto de PROPIEDAD o de CONTEXTO (cards del portafolio,
            .carta__foto; foto de curaduría, .corte__foto; fotos de
            desarrolladora, .dev__foto). Es el 1.6 medido contra Fort Vega
            (BRIEF_CARDS_VARIANTE_B_2026-08-20).
   1/1    → SOLO retrato de persona (.persona__foto, equipo). Un headshot
            cuadrado es correcto — no se fuerza a 16/10 (BRIEF_CIERRE
            2026-08-20, confirmado por Jara).
   21/9   → SOLO la banda a sangre (.show--sangre .show__marco). El hero usa
            el viewport, no cuenta como proporción declarada.
   Si aparece una cuarta proporción en el home, es un descuido: documentarla
   aquí o corregirla, no dejarla suelta. ─────────────────────────────────── */
.carta__foto{position:relative;aspect-ratio:16/10;overflow:hidden;background:var(--tinta)}
.carta__foto img{
  width:100%;height:100%;object-fit:cover;object-position:50% var(--pos,50%);display:block;
  transition:transform .7s var(--ease-out)
}
.carta:hover .carta__foto img{transform:scale(1.035)}
/* banda reservada: SOLO el 74% inferior — medida contra el material real
   (no elegida). rgba(10,19,34,..) = --tinta, igual que el resto del sitio
   usa el color literal cuando necesita alfa (custom properties no mezclan
   alfa con color-mix aquí). */
.carta__piso{position:absolute;left:0;right:0;bottom:0;height:74%;pointer-events:none;
  background:linear-gradient(to top, rgba(10,19,34,.92) 0%, rgba(10,19,34,.80) 34%,
              rgba(10,19,34,.42) 72%, rgba(10,19,34,0) 100%)}
.carta__txt{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:flex-end;
  justify-content:space-between;gap:24px;padding:0 22px 20px}
.carta__zona{display:block;font-size:var(--t-micro);letter-spacing:.18em;text-transform:uppercase;
  color:var(--cielo);margin:0 0 6px}
.carta h3{font-size:var(--t-h2);line-height:1;color:var(--blanco);margin:0}
.carta__precio{font-family:var(--display);font-size:var(--t-cuerpo);color:var(--blanco);margin:8px 0 0;opacity:.92}
.carta__datos{list-style:none;margin:0;padding:0;text-align:right;flex:none}
.carta__datos li{font-size:var(--t-micro);line-height:1.3;color:var(--blanco);padding:5px 0;
  border-top:1px solid rgba(247,249,250,.28)}
.carta__datos li:first-child{border-top:0;padding-top:0}
.carta__linea{position:absolute;left:22px;bottom:0;height:2px;width:0;background:var(--cielo);
  transition:width .5s var(--ease-out)}
.carta:hover .carta__linea{width:calc(100% - 44px)}
@media (prefers-reduced-motion:reduce){ .carta__foto img,.carta__linea{transition:none} }
/* .vacio se retiró con BRIEF_NUMERO_VIVO_2026-08-20: el estado cero ahora vive
   dentro de #conteo (.conteo--cero), no como mensaje aparte bajo el grid vacío. */

/* ── los cinco filtros (el corte) ────────────────────────── */
.corte{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:clamp(24px,5vw,72px);align-items:start}
@media (max-width:900px){ .corte{grid-template-columns:1fr} }
.corte__lista{list-style:none;margin:0;padding:0;counter-reset:f}
.corte__lista li{
  counter-increment:f;display:grid;grid-template-columns:auto 1fr;gap:20px;
  padding:22px 0;border-top:1px solid var(--linea-osc);align-items:start;
  transition:opacity .3s ease
}
.corte__lista li::before{
  content:counter(f,decimal-leading-zero);
  font-family:var(--display);font-size:var(--t-cuerpo);color:var(--niebla);padding-top:4px
}
.corte__lista h3{font-size:var(--t-cuerpo);font-family:var(--cuerpo);font-weight:700;letter-spacing:.01em;margin:0 0 4px;color:var(--blanco)}
.corte__lista p{margin:0;font-size:var(--t-chico);color:var(--niebla);max-width:52ch}

/* ── desarrolladores ─────────────────────────────────────── */
/* foto de prueba en curaduría: el filtro no es abstracto, es ESTE edificio */
.corte__foto{margin:34px 0 0}
.corte__foto img{width:100%;aspect-ratio:16/10;object-fit:cover;display:block}
.corte__foto figcaption{margin-top:10px;font-size:var(--t-micro);letter-spacing:.14em;
  text-transform:uppercase;color:var(--niebla)}

/* casas desarrolladoras: render + nombre + sus desarrollos */
.devs{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:clamp(20px,3vw,40px)}
.dev__foto{aspect-ratio:16/10;overflow:hidden}
.dev__foto img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .5s ease}
.dev:hover .dev__foto img{transform:scale(1.03)}
.dev{padding-left:21px}
.dev h3{margin:18px 0 0;font-size:var(--t-h3)}
.dev__n{display:block;font-size:var(--t-micro);letter-spacing:.16em;text-transform:uppercase;
  color:var(--niebla-papel);margin-top:8px}
.dev__lista{margin:12px 0 0;padding-top:12px;border-top:1px solid var(--linea);
  font-size:var(--t-chico);color:var(--niebla-papel);max-width:none}

/* preguntas frecuentes — respuestas visibles, sin acordeón: deben coincidir 1:1 con el
   FAQPage del JSON-LD, y un texto oculto tras un click ya no cuenta como "visible" a ojos
   de Google. */
.preguntas{list-style:none;margin:0;padding-left:21px;max-width:74ch}
.preguntas li{padding:26px 0;border-top:1px solid var(--linea);transition:border-color .24s ease}
.preguntas li:last-child{border-bottom:1px solid var(--linea)}
.preguntas li:hover{border-color:var(--asfalto)}
.preguntas h3{font-size:var(--t-cuerpo);font-family:var(--cuerpo);font-weight:700;margin:0 0 8px;color:var(--asfalto)}
.preguntas p{margin:0;font-size:var(--t-chico);color:var(--niebla-papel);max-width:62ch}

/* slideshow de asesoría: los desarrollos son la promesa, no un fondo de color */
.show{margin:30px 0 0}
.show__marco{position:relative;aspect-ratio:16/10;overflow:hidden;background:var(--navy)}
.show__marco img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  opacity:0;transition:opacity .8s ease}
.show__marco img.on{opacity:1}
.show figcaption{display:flex;justify-content:space-between;align-items:center;gap:14px;
  margin-top:10px;font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;color:var(--niebla)}
.show__pts{display:flex;gap:6px}
.show__pts i{width:6px;height:6px;border-radius:50%;background:var(--linea-osc);transition:background .3s ease}
.show__pts i.on{background:var(--cielo)}
@media (prefers-reduced-motion:reduce){.show__marco img{transition:none}}

/* momento a sangre: el mismo carrusel, a todo lo ancho del viewport,
   por fuera de .env — la foto manda, el texto solo la nombra encima. */
.sangre{padding:0}
.show--sangre{margin:0;position:relative}
.show--sangre .show__marco{aspect-ratio:21/9;background:var(--tinta)}
.show--sangre figcaption{position:absolute;left:0;right:0;bottom:0;z-index:2;margin:0;
  padding:22px var(--pad);color:var(--blanco);
  background:linear-gradient(180deg,transparent,rgba(14,27,47,.7))}
.show--sangre .show__pts i{background:rgba(247,249,250,.32)}
@media (max-width:760px){ .show--sangre .show__marco{aspect-ratio:4/3} }

/* formulario por pasos: una pregunta a la vez */
#asesoria .forma{align-items:center}
.fpro{display:flex;align-items:center;gap:14px;margin-bottom:6px}
.fpro span#fpaso{font-size:var(--t-micro);letter-spacing:.16em;text-transform:uppercase;color:var(--niebla);flex:none}
.fpro__riel{flex:1;height:1px;background:var(--linea-osc);position:relative;display:block}
.fpro__riel i{position:absolute;inset:0 auto 0 0;width:20%;background:var(--cielo);transition:width .4s ease}
.fres{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 4px}
.fres:empty{margin:0}
.fres button{background:none;border:1px solid var(--linea-osc);border-radius:var(--r);
  padding:6px 12px;font-size:var(--t-micro);letter-spacing:.08em;color:var(--cielo);cursor:pointer;
  transition:border-color .2s ease,color .2s ease,background .2s ease}
.fres button:hover{border-color:var(--cielo);color:var(--blanco);background:rgba(184,210,221,.08)}
.paso{display:none;border:0;padding:0;margin:18px 0 0}
.paso.activa{display:block;animation:pasoEntra .3s ease}
@keyframes pasoEntra{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.paso.activa{animation:none}}
.paso legend{padding:0;margin-bottom:12px}
.fnav{display:flex;align-items:center;gap:18px;margin-top:26px}
.fnav #atras{background:none;border:0;padding:8px 0;color:var(--niebla);cursor:pointer;
  font-size:var(--t-chico);border-bottom:1px solid transparent;transition:color .2s ease,border-color .2s ease}
.fnav #atras:hover{color:var(--blanco);border-bottom-color:var(--blanco)}
.fnav .btn{flex:1;justify-content:center}

/* ── formulario ──────────────────────────────────────────── */
.forma{display:grid;grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);gap:clamp(28px,5vw,72px);align-items:start}
@media (max-width:900px){ .forma{grid-template-columns:1fr} }
.campo{margin-bottom:22px}
.campo>label,.campo>span.rot,legend.rot{
  display:block;font-size:var(--t-micro);letter-spacing:.16em;text-transform:uppercase;
  color:var(--niebla);margin-bottom:10px;font-weight:500
}
.campo input[type=text],.campo input[type=email],.campo input[type=tel],.campo select{
  width:100%;min-height:52px;padding:0 14px;background:rgba(220,230,242,.06);
  border:1px solid rgba(220,230,242,.42);border-radius:var(--r);color:var(--blanco);
  transition:border-color .2s ease,background .2s ease
}
.campo select{appearance:none;padding-right:40px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;background-image:linear-gradient(45deg,transparent 50%,var(--niebla) 50%),linear-gradient(135deg,var(--niebla) 50%,transparent 50%);background-position:calc(100% - 20px) 24px,calc(100% - 14px) 24px;background-size:6px 6px;background-repeat:no-repeat}
.campo select option{background:var(--tinta);color:var(--blanco)}
.campo input:focus,.campo select:focus{border-color:var(--cielo);background:rgba(220,230,242,.06)}
.campo input::placeholder{color:rgba(143,161,188,.7)}
.opciones{display:flex;flex-wrap:wrap;gap:8px}
.opcion{position:relative}
.opcion input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
.opcion span{
  display:inline-flex;align-items:center;min-height:46px;padding:0 18px;
  background:rgba(220,230,242,.06);
  border:1px solid rgba(220,230,242,.42);border-radius:var(--r);font-size:var(--t-chico);
  transition:background .2s ease,border-color .2s ease,color .2s ease
}
.opcion input:hover+span{border-color:var(--cielo)}
.opcion input:focus-visible+span{outline:2px solid var(--cielo);outline-offset:2px}
.opcion input:checked+span{background:var(--blanco);color:var(--tinta);border-color:var(--blanco);font-weight:500}
.wa{display:flex;gap:12px;align-items:flex-start;margin:6px 0 24px;font-size:var(--t-chico);color:var(--niebla);cursor:pointer}
.wa input{width:20px;height:20px;margin:2px 0 0;accent-color:var(--cielo);flex:none}
.forma .btn{background:var(--blanco);color:var(--tinta);border-color:var(--blanco);width:100%;justify-content:center}
.forma .btn:hover{background:var(--cielo);border-color:var(--cielo);color:var(--tinta)}
.aviso{font-size:var(--t-micro);color:var(--niebla);margin-top:14px;letter-spacing:.02em}
.estado{margin-top:16px;font-size:var(--t-chico);min-height:22px}
.estado.err{color:var(--error)}
.gracias{border:1px solid var(--linea-osc);padding:28px;border-radius:var(--r)}
.gracias h3{margin-bottom:10px}

/* ── el formulario vive en una tarjeta clara ────────────────
   Sobre la pared navy los campos eran cajas transparentes: el formulario estaba ahí
   pero no se leía como algo que se puede llenar, y las etiquetas iban en azul claro.
   La sección sigue siendo navy — lo que cambia es que el formulario tiene superficie.
   (Feedback de Jara, 17-ago: "el formulario de quadrante invita más a ser llenado".) */
#forma, .gracias{
  background:var(--papel);color:var(--asfalto);
  border:1px solid var(--linea);border-radius:10px;
  padding:clamp(22px,2.6vw,34px)
}
#forma .rot,#forma .campo>label,#forma legend.rot{color:var(--niebla-papel)}
#forma #fpaso{color:var(--niebla-papel)}
#forma .fpro__riel{background:var(--linea)}
#forma .fpro__riel i{background:var(--navy)}
#forma .fres button{border-color:var(--linea);color:var(--niebla-papel)}
#forma .fres button:hover{border-color:var(--navy);color:var(--navy);background:rgba(21,53,94,.06)}
#forma .campo input[type=text],#forma .campo input[type=email],
#forma .campo input[type=tel],#forma .campo select{
  background:#FFF;border-color:var(--linea);color:var(--asfalto)}
#forma .campo input::placeholder{color:var(--niebla-papel)}
#forma .campo input:hover,#forma .campo select:hover{border-color:var(--navy);background:#FFF}
#forma .campo input:focus,#forma .campo select:focus{
  border-color:var(--navy);background:#FFF;outline:2px solid rgba(21,53,94,.22);outline-offset:1px}
#forma .campo select{background-image:linear-gradient(45deg,transparent 50%,var(--niebla-papel) 50%),linear-gradient(135deg,var(--niebla-papel) 50%,transparent 50%)}
#forma .campo select option{background:#FFF;color:var(--asfalto)}
#forma .opcion span{background:#FFF;border-color:var(--linea);color:var(--asfalto)}
#forma .opcion input:hover+span{border-color:var(--navy)}
#forma .opcion input:focus-visible+span{outline:2px solid var(--navy);outline-offset:2px}
#forma .opcion input:checked+span{background:var(--navy);color:var(--blanco);border-color:var(--navy)}
#forma .wa{color:var(--niebla-papel)}
#forma .wa input{accent-color:var(--navy)}
#forma .wa input:hover{outline:2px solid var(--navy)}
#forma .fnav #atras{color:var(--niebla-papel);border-bottom-color:var(--linea)}
#forma .fnav #atras:hover{color:var(--navy);border-bottom-color:var(--navy)}
#forma .btn{background:var(--navy);color:var(--blanco);border-color:var(--navy)}
#forma .btn:hover{background:var(--navy-alto);border-color:var(--navy-alto);color:var(--blanco)}
#forma .aviso{color:var(--niebla-papel)}
#forma .estado.err{color:var(--error-papel)}
#forma .estado.err a{color:var(--error-papel);border-bottom-color:currentColor}
#forma .estado a{color:var(--navy)}
.gracias h3{color:var(--asfalto)}
.gracias #graciasTexto{color:var(--niebla-papel)!important}

/* ── contacto ────────────────────────────────────────────── */
.contacto{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(20px,3vw,40px)}
@media (max-width:860px){ .contacto{grid-template-columns:1fr;gap:28px} }
.persona{border-top:1px solid var(--linea);padding-top:18px;padding-left:21px}
/* Foto del equipo. La muesca escalonada va SOLO en la primera: el sistema pide 1-2 usos
   por vista y las tres fotos caben juntas en 1440px — repetida tres veces el corte deja de
   leerse como firma de marca y parece un error de recorte (audit de diseño 16-ago, #4). */
.persona__foto{margin:0 0 16px;overflow:hidden}
.persona:first-child .persona__foto{
  clip-path:polygon(0 0, 62% 0, 62% 5.5%, 76% 5.5%, 76% 11%, 100% 11%, 100% 100%, 0 100%)}
.persona__foto img{aspect-ratio:1/1;object-fit:cover;width:100%;filter:saturate(.92)}
.persona h3{font-size:var(--t-cuerpo);margin-bottom:4px}
.persona .rol{font-size:var(--t-micro);letter-spacing:.16em;text-transform:uppercase;color:var(--niebla-papel);margin:0 0 10px}
.persona a{font-size:var(--t-chico);border-bottom:1px solid var(--linea);transition:border-color .2s ease}
.persona a:hover{border-bottom-color:var(--navy)}

/* ── footer ──────────────────────────────────────────────── */
.pie{background:var(--tinta);color:var(--niebla);padding:56px 0 34px;font-size:var(--t-chico)}
.pie__top{display:flex;flex-wrap:wrap;gap:28px;justify-content:space-between;align-items:flex-start;padding-bottom:30px;padding-left:21px;border-bottom:1px solid var(--linea-osc)}
.pie .marca{color:var(--blanco);display:block;margin-bottom:16px}
.pie .marca svg{height:92px;width:auto;display:block}
.pie__bajo{display:flex;flex-wrap:wrap;gap:14px;justify-content:space-between;padding-top:22px;padding-left:21px;font-size:var(--t-micro);letter-spacing:.06em}
.pie a:hover{color:var(--blanco)}
/* tel/mailto del equipo: son dos líneas consecutivas del mismo párrafo (~22.4px de separación) —
   el truco de padding+margen negativo expande cada una 13px arriba/abajo y las zonas de 44px se
   solapan 26px en medio (detectado con hit-testing real, ronda 4). Un solape no se arregla con más
   padding: la separación entre líneas tiene que llegar a 44px. display:block les da cada una su
   propia fila de 44px — cero solape posible, el texto no se mueve en horizontal, solo se abre
   verticalmente (aceptado). */
.pie__top a[href^="tel:"],.pie__top a[href^="mailto:"]{display:block;height:44px;line-height:44px}

/* ── revelado ────────────────────────────────────────────── */
/* El candado lo pone JS. Si el observer no existe o el script truena,
   el contenido se ve — nunca al revés. */
.js .rev{opacity:0;transform:translateY(18px)}
.js .rev.visible{opacity:1;transform:none;transition:opacity .7s ease,transform .7s var(--ease-out)}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .js .rev{opacity:1;transform:none;transition:none}
  .carta__foto img,.btn,.carta{transition:none}
}

/* R39 (31-ago-2026): el toast flotante de aviso de grabación (.aviso-grab) se
   retiró completo, junto con body.con-aviso, --aviso-alto y su flag de localStorage.
   Clarity SIGUE encendido — los datos son parte de lo que se le enseña al cliente.
   Lo que cambia es la forma de avisar: una línea normal en el pie (.pie__aviso), en
   las 9 páginas. El toast flotaba, tapaba copy en móvil, tenía estado que
   sincronizar, y mientras nadie lo cerrara mantenía el CTA sticky apagado en 8/8
   fichas. Un aviso que hay que cerrar para poder convertir es mal aviso y mala
   página; en el pie avisa igual y no tapa nada. */
.pie__aviso{margin:18px 0 0;font-size:.8125rem;letter-spacing:.02em;color:var(--niebla)}
/* ── micro-interacción · manual 2026 §3 (2026-08-15) ─────────────────
   1) Press en todo lo accionable.
   2) El wordmark del nav no tenía hover; ahora usa el mismo idioma que .liga. */
.btn:active{transform:translateY(-1px) scale(.97);transition-duration:.1s}
.menu-btn:active{transform:scale(.94)}
.hito:active .hito__pt{transform:scale(1.35)}

.nav .marca .ln{border-bottom:1px solid transparent;transition:color .2s ease,border-color .2s ease}
.nav .marca:hover .ln{color:var(--cielo);border-bottom-color:var(--cielo)}
.nav .marca:active{transform:scale(.98)}

@media (prefers-reduced-motion:reduce){
  .btn:active,.menu-btn:active,.nav .marca:active{transform:none}
  .hito:active .hito__pt{transform:scale(1.6)}
}

/* ═══ FICHA DE DESARROLLO ═══════════════════════════════════ */
/* .js .rev{opacity:0} ya lo declara la línea 815; repetirla aquí ganaba la cascada
   sobre el override de prefers-reduced-motion (817-821) y dejaba el texto invisible. */
.rev{transition:opacity .7s ease,transform .7s ease}
.rev.visible{opacity:1;transform:none}

/* RONDA_34 M3 — A3 titular puente (exoape.com): la ficha no tenía ni A1, cuatro
   secciones hueso seguidas con padding 120 (VEREDICTO_DIRECCION_2026-08-29).
   El h1 crece hasta que su caja cruza el corte .fhero → primera sección; el h2
   de "El proyecto" ya está a 40px (--t-h2, 0.55× calzado con el h1) así que
   sigue la misma escala del otro lado del corte. height:100vh + overflow:hidden
   es lo que permite que el título se recorte en el fold en vez de estirar el
   hero. Escala tipográfica pura — sin JS, sin canvas fijo. */
/* height>100vh, anclado abajo (align-items:flex-end): el h1 arranca dentro del
   primer viewport y su propia caja termina después — cruza el fold de verdad,
   sin recortar migas/eyebrow/entrada/afirma/datos/cinta, que sencillamente
   aparecen más abajo, alcanzables con scroll normal, ninguno se pierde.
   Calculado contra las 8 fichas: 148vh deja el h1 arriba del fold y su bottom
   dentro de la ventana siguiente en las 8 (medido, ver RESULTADO_34). */
/* RONDA 43 (BRIEF_CL4UD_43_HERO_PORT, OK Jara 31-ago 20:17) — SE REVIERTE el 148vh
   de la ronda 34/39. Medido contra producción: el hero medía 1332 px a 1440x900
   (1.48 vh) y con align-items:flex-end el bloque arrancaba en y=525, así que el h1
   terminaba en 961-1031 de 900 y el primer CTA sólido en 1309. G2 (h1 Y CTA completos
   arriba del fold) daba 0/8 en 1440x900, 0/8 en 1280x700 y 2/8 en 390x844. El hero del
   HOME publicado (index.html:277, min-height:100svh) entrega 597/701/783 dentro de 900:
   ES la fuente del port. El "titular puente" que cruzaba el corte se cambia por
   entregar la información al cargar — decisión de Jara con capturas de las dos páginas.
   overflow:hidden y align-items:flex-end se conservan (el home logra el mismo anclaje
   con flex-direction:column + justify-content:flex-end). */
.fhero{position:relative;min-height:100svh;height:auto;overflow:hidden;display:flex;align-items:flex-end;background:var(--tinta)}
/* RONDA 55 — andamio sticky para el hero nuevo de HOF (BRIEF_RONDA_55_HERO_HOF.md §3.1).
   Scoped a .fhero--r55: NO se toca la regla base de arriba (flex + align-items:flex-end +
   overflow:hidden), que sostiene el G2 de las otras 7 fichas sin re-verificarlo esta ronda.
   CORRECCIONES_R55_STICKY.md midió dos bloqueadores de sticky independientes en la regla base:
   align-items:flex-end (el pin arrancaría en top=2700 y bajaría a 0 — hero vacío 3vp) y
   overflow:hidden (crea scroll container, el sticky no se despega). Fix: display:block +
   overflow:clip (recorta igual, no crea scroll container, no genera stacking context nuevo
   como contain:paint) en el contenedor; el anclaje al fondo que daba flex-end se baja al
   wrapper .env dentro del pin, mismo patrón que el hero de HOME (flex-column + justify-end). */
/* FASE3 D3: body{padding-bottom:var(--scta-alto,0px)} (linea 74) es la
   reserva ESTATICA (piso, no scopeada) que evita CLS cuando .scta enciende
   cerca del fondo -- pero es siempre-on, asi que deja una franja crema de
   73px bajo el footer aunque .scta ya se haya apagado mucho antes (su ver()
   apaga en cuanto #pedir queda arriba del 60% del viewport, y #pedir vive
   bien antes del footer en las 8 fichas). Medido: docHeight-footerBottom=73px
   en 1440 y 390, idéntico -- confirma que es el alto fijo de la barra, no
   algo responsive. Causa raiz es GLOBAL (linea 74, sin scope, afecta a las
   8 fichas por igual); esta ronda solo tiene scope de HOF, asi que el fix se
   acota con :has() en vez de tocar la regla base -- generalizarlo a las
   otras 7 fichas es decision de Krillin, no de esta ronda. */
body:has(.fhero--r55){padding-bottom:0}
/* R63: el alto del contenedor pasa a --fhero-r55-vh (default 325 = el valor de
   hof, derivado en FASE3 D2). Cada ficha lo DESPEJA contra la caja real de su
   propia máscara (BRIEF_R63_HERO_MADISON.md §1.2) — nunca copia el número de
   hof. El valor vive en FHERO_VARS[slug] (construir_fichas.py), inline en
   <body>. El bloque de keyframes de scrub sigue sin duplicarse por ficha:
   solo cambia el valor de esta variable, no el mecanismo. */
.fhero--r55{display:block;overflow:clip;align-items:unset;height:calc(var(--fhero-r55-vh,325)*1vh);min-height:0}
/* FASE3 D2: 4.00vp -> 3.25vp desktop (900px de ritmo flojo medido u=0.525-0.775,
   11-15% de cambio por paso contra 30-90% en el resto). Movil: mismo ratio
   0.8125 aplicado a 2.80vp -> 2.275vp (proporcional, no un numero nuevo). */
/* z-index:2 obligatorio: .fhero::after (velo, linea 957) es z-index:1 e inset:0
   sobre TODO el contenedor de 400vh, no solo el viewport visible. Sin z-index
   propio aqui, .fhero__pin queda en el nivel "auto" y el velo pinta ENCIMA de
   todo el pin (wordmark, tinta, foto, copy) — medido: papel #F2EEE7 se veia
   mezclado con el navy del velo (grises 131-158, variando por fila = el
   gradiente del velo), 0% de cobertura real detectada. El .env original
   (regla base .fhero .env, linea ~965) ya resolvia esto con z-index:2; el pin
   nuevo necesita el mismo nivel para todo su contenido, no solo el texto. */
.fhero--r55 .fhero__pin{position:sticky;top:0;z-index:2;height:100svh;overflow:clip;display:flex;align-items:flex-end}
.fhero--r55 .env{display:flex;flex-direction:column;justify-content:flex-end;width:100%;position:relative}
/* R68 · BRIEF_R68_CONTRASTE_HERO.md -- el copy del hero está bajo AA en producción desde
   R55 (05-sep), no desde R67: `.fhero::before` (el escudo, §2414 de este archivo) tiene
   inset:0 contra `.fhero`, y en R55 `.fhero` pasa de ~1 viewport (900-1332px) a
   2.7-3.25vp (2680-3250px). Sus stops en % (16/28/40/64/100%) se calibraron contra la
   altura chica; contra la alta, el h1 (que sigue en el mismo Y absoluto, ~20-23% de la
   caja nueva contra ~76-85% de la vieja) cae en el tramo casi transparente del gradiente
   (alpha .18-.28 medido, contra .75-.78 antes de R55 -- qa/r68/verificar_escudo.py).
   NO se toca `.fhero::before` (retocar sus stops para la caja alta empujaría alpha .34-.71
   sobre la VENTANA de la máscara, que vive arriba de .env -- ver overlap medido en
   qa/r68/verificar_escudo.py). En vez de eso: escudo LOCAL, anclado a `.env` (su propia
   caja, no la de `.fhero`), así su tamaño no depende de cuánto mida el hero completo.
   Arranca en 0 en el propio top de `.env` (que en varias fichas todavía pisa el fondo de
   la máscara -- hof solapa 223px, central-escorza 229px) y sólo sube de verdad pasado el
   55% de la caja de `.env`, donde ya no hay wordmark/tinta debajo (medido caso por caso
   en RESULTADO_68.md). Al ser hijo de `.env`, hereda su `opacity` animada por
   `hofEnvFade` -- se apaga con el mismo timing que el texto, sin animación propia. */
.fhero--r55 .env::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  /* R68: alphas por variable -- default fuerte (7/8 fichas tienen 2-9pp de margen bajo
     el 10% relativo de G-VENTANA, medido en qa/r68/gate_ventana_scroll.py). Sólo
     central-escorza (su .env solapa 229px con el wordmark, el doble que el resto) pisa
     el gate a estos valores y necesita el par bajo -- override en R55_MASK_VARS,
     mismo patrón que --fhero-brillo/--fhero-velo por ficha. */
  background:linear-gradient(180deg,
    rgba(6,12,22,var(--fhero-escudo-a0,.22)) 0%, rgba(6,12,22,var(--fhero-escudo-a1,.48)) 20%,
    rgba(6,12,22,var(--fhero-escudo-a2,.62)) 40%, rgba(6,12,22,var(--fhero-escudo-a3,.72)) 60%,
    rgba(6,12,22,.80) 100%)}
@media (max-width:760px){.fhero--r55{height:calc(var(--fhero-r55-vh-m,300)*1vh)}}
/* FASE7 D8 — la cortina del remate. Medido en la referencia
   (REMATE_HERO_MECANISMO_2026-09-04.md): el 70% del "remate" del hero no es
   una animacion del hero -- es la SECCION SIGUIENTE subiendo por flujo normal
   de documento (margin-bottom:-1 viewport en el contenedor del pin colapsa el
   hueco) y tapando el hero pinneado de abajo hacia arriba durante el ultimo
   tramo del pin, sin easing/opacity/transform, puro layout. Portado 1:1:
   margin-bottom:-100svh (misma unidad que la altura de .fhero__pin) en vez de
   -900px fijo, para que funcione igual en cualquier viewport. La seccion
   siguiente necesita z-index > 2 (el pin vive en z:2, arriba) para pintar
   ENCIMA del hero pinneado en vez de flujo normal (que perdería contra el
   z-index explicito del pin) -- scopeado con el selector de hermano
   adyacente, solo afecta a la seccion que sigue al hero de HOF. */
.fhero--r55{margin-bottom:-100svh}
/* BRIEF_CORTINA_SANGRADO_2026-09-17: el selector de hermano ADYACENTE (+) solo le daba
   z-index:3 a la PRIMERA section después del hero. Cuando esa primera section mide menos
   que el tiro de la cortina (-100svh), el hero .fhero--r55 (position:relative, fondo
   navy 14,27,47) sigue pintando por encima de la SEGUNDA section, que quedaba en
   position:static — 0 a 248px de navy sobre papel, medido en las 8 fichas. Fix en la
   raíz: hermano GENERAL (~), todas las sections y el footer que siguen al hero, no
   solo la primera. */
.fhero--r55 ~ section,
.fhero--r55 ~ footer{position:relative;z-index:3}
.fhero__img{position:absolute;inset:0;z-index:0}
/* 23-ago-2026: --fhero-pos permite bajar el encuadre para sacar cielo del cuadro.
   El tinte azul de Escorza/Simona es el cielo real del render, no el velo: recortar
   es la unica via honesta (recolorear el render del cliente lo falsea). Default center. */
.fhero__img img{width:100%;height:100%;object-fit:cover;object-position:var(--fhero-pos,50% 50%);transform:scale(var(--fhero-zoom,1));transform-origin:var(--fhero-origen,50% 50%);filter:brightness(var(--fhero-brillo,1)) saturate(var(--fhero-satf,1)) contrast(var(--fhero-contra,1))}
/* 23-ago: el zoom de encuadre es de escritorio. A <=760px el hero ya recorta mucho
   por si solo y el zoom dejaba solo un muro sin contexto.
   R43: el comentario decía "78svh en vertical" — ninguna regla con ese valor existía en
   el archivo (grep 0 hits); era comentario muerto de una versión previa. Hoy el alto
   móvil lo fija el mismo min-height:100svh de .fhero. */
@media (max-width:760px){.fhero__img img{transform:scale(var(--fhero-zoom-m,1))}}
/* 23-ago: --fhero-velo-rgb — el scrim era navy fijo, y ese navy ES el tinte azul
   que se veia en los heros. Cada ficha puede usar un scrim neutro o calido. */
.fhero::after{content:"";position:absolute;inset:0;z-index:1;
  background:
    /* Velo aligerado (17-ago), igual que el hero del index: la ficha llegaba a .88 abajo
       y .78 al arranque y enterraba el render del desarrollo. Se conserva la carga solo
       bajo el bloque de texto y en los datos del pie del hero.
       22-ago: --fhero-velo/--fhero-velo-v estaban puestas en el <body> de cada ficha
       pero ningún selector las leía — el velo era idéntico en las 8 fichas sin importar
       el valor. Ahora sí multiplican el alpha: -v pesa el degradado vertical, la otra
       pesa el lateral (donde vive el H1). El stop de 100% (el piso, donde vive dl.datos)
       queda fijo en .76 sin importar -v: bajarlo tumbaba dt/.niebla a ~1:1 de contraste
       contra la foto ya aclarada — medido con automation/contraste_glifos.py. */
    /* 23-ago (auditoría Awwwards, punto 1): el vertical arrancaba en .42 arriba y .22 a
       media altura — cubría TODO el cuadro, no solo donde vive el texto, y es lo que
       enterraba el render. Medido: 7 de 8 heros con luminancia media 71–85 y p90 < 180,
       o sea ni el 10% más claro llegaba a gris claro. Ahora el vertical solo carga el
       piso (donde va dl.datos) y el lateral protege la columna del texto; el resto del
       render queda limpio. Sonda nueva: qa/hero_luz.py (media>=95, p90>=200). */
    /* D1 · RONDA 40. Se retira el degradado LATERAL (100deg) de esta capa.
       Medido con qa/probe_velo39.mjs sobre el tercio izquierdo (480×600 desde el
       tope del hero): tiñendo de navy el ancho completo costaba hasta 54% de la luz
       del render del cliente (nikté 54%, quadrante 39%, escorza 37%, simona 33%).
       La geometría dice por qué era gratis ese costo: a 1440 el hero mide 1,332 px
       y el bloque de texto (.env) EMPIEZA en y=645, o sea fuera de la zona medida.
       Arriba de la tipografía el lateral no protegía ninguna letra — sólo pintaba
       de azul el edificio. La carga de legibilidad la lleva el escudo local
       .fhero .env::before, que va pegado al bloque de texto (el brief lo nombra
       .fhero__escudo; el selector real es ése). El VERTICAL se queda tal cual: es
       quien sostiene el piso donde vive dl.datos, y su tramo alto (.10) es
       despreciable en la medición. */
    linear-gradient(180deg,rgba(var(--fhero-velo-rgb,12,22,38),calc(.10*var(--fhero-velo-v,1))) 0%,rgba(var(--fhero-velo-rgb,12,22,38),0) 58%,rgba(var(--fhero-velo-rgb,12,22,38),var(--fhero-floor,.30)) 100%)}
/* M3: padding comprimido — el h1 gigante necesita el espacio que antes se
   iba en aire de sobra arriba/abajo; migas+eyebrow+entrada+afirma+datos+cinta
   se quedan, solo más apretados, para que el titular cruce el fold sin perder
   ni un dato del pie del hero (afirma/datos/cinta siguen 100% visibles). */
/* R43: el padding baja de 6vh/2.6vh a 3.4vh/2.2vh. Con el hero en 148vh sobraba alto y
   el aire era gratis; en una pantalla cada px del padding se lo quita al pie del hero
   (datos/cinta), que es justo lo que G2 exige ver al cargar. */
.fhero .env{position:relative;z-index:2;padding-block:clamp(28px,3.4vh,44px) clamp(18px,2.2vh,26px);width:100%}
/* R43: el ritmo de los bloques del hero se comprime en bloque, no ficha por ficha.
   Ninguno se borra: migas, eyebrow, sobretítulo, h1, entrada, afirma, datos y cinta
   siguen los ocho en el DOM y los ocho arriba del fold. El margin-top del eyebrow venía
   inline desde construir_fichas.py (style="margin-top:22px"), donde el CSS no lo podía
   alcanzar; se movió aquí para que el ritmo del hero se controle en un solo lugar. */
.fhero .migas{margin-bottom:10px}
.fhero .eyebrow{margin-top:14px;margin-bottom:12px}
.fhero .datos{margin-top:clamp(12px,1.8vh,20px);margin-bottom:14px}
.fhero .datos div{padding-top:10px}
/* R39: SOLO en móvil el sticky está encendido desde la carga, así que solo ahí el
   pie del hero (datos/cinta, pegados al fondo de las 148vh) reserva su alto o la
   barra se los come. Reservarlo también en escritorio —primer intento— empujó el
   bloque 68 px hacia arriba y tumbó el cruce del fold de 8/8 a 3/8: en ≥901px la
   barra no aparece hasta que el hero ya pasó, así que ahí no hay nada que reservar. */
@media (max-width:900px){
  .fhero .env{padding-bottom:calc(clamp(20px,2.6vh,30px) + var(--scta-alto,0px))}
}
/* R43 · T2 — EL ESCUDO SE ANCLA AL HERO, COMO EN EL HOME.
   Antes: .fhero .env::before, anclado al BLOQUE DE TEXTO, con inset:-220px, 96deg y un
   mask-image de 220 px encima. Esa máscara era un parche a un problema de anclaje: al
   colgar del texto, el escudo tenía un borde superior horizontal que cortaba el render,
   y había que difuminarlo. El home (index.html:300, .hero::before) no tiene ese problema
   porque cuelga de .hero completo: ocupa toda la altura, así que NO HAY borde superior
   que tapar. Se porta ese anclaje.
   Y se porta el 90deg. A 96deg las líneas de stop se inclinan, así que el alfa todavía
   valía algo cuando la caja se corta en su borde derecho — 25.6 de salto en 8 px, medido
   en el home el 20-ago. A 90deg los stops son verticales y el alfa llega a 0 exacto en el
   borde, en todas las filas (G3).
   Lo que NO se porta son los stops del home tal cual: el h1 de la ficha es una tesis
   ancha (992 px a 1440, contra ~380 px del titular corto para el que estaba calibrado el
   escudo viejo), y la meseta corta del home lo dejaba en 2.38 contra el piso de 4.5 (R39,
   medido por décimo horizontal). Se conserva meseta larga sobre 70vw. G4 manda sobre el
   parecido con el home.
   El fade vertical arranca en y=0 y no en mitad del render: por eso no reintroduce el
   borde recto que el mask-image de 220 px venía a tapar. Protege algo de la luz del
   render arriba de la tipografía (D1/G5) sin costarle contraste al h1, que vive abajo,
   donde la máscara ya vale 1. */
.fhero::before{content:"";position:absolute;z-index:1;
  /* R44b · Jara: "el cuadrado azul corta la imagen". Tenia razon y el culpable NO era
     la carga: era la FORMA. Un escudo lateral es una caja, y toda caja tiene bordes —
     el derecho (donde el alfa cae a 0) y, con mascara vertical, uno horizontal a media
     altura. Bajarle el alfa hace la caja mas clara, no la desaparece. Medido: la
     mascara .06→1 en 33% ponia un salto de luminancia a y=290 sobre 830 px de ancho.
     Se retira la caja entera. Queda un velo VERTICAL de ancho completo, el mismo
     modelo que ya corria en movil: no tiene borde derecho porque cubre los 1440 px,
     y no tiene borde horizontal porque su unica transicion son 900 px de degradado.
     El precio es que el tercio derecho del render tambien recibe algo de velo abajo;
     a cambio no hay una sola linea recta en el hero. Los stops estan puestos contra la
     geometria real del bloque de texto, no a ojo: .env arranca entre y=270 y y=358
     (30-40% del hero), el h1 vive entre 44% y 63%, la entrada en 63-70%, y el pie
     (datos + cinta) del 78% al 92%. Primer intento de stops (.10 en 24%, .52 en 38%)
     dejaba el eyebrow de central-escorza en 3.44 contra el piso de 4.5: su .env es el
     que arranca mas alto (y=270 = 30% del hero) y ahi el velo todavia no cargaba. La
     rampa se adelanta a 16-28-40%: limpio hasta 16%, y para 30% ya vale ~.52. */
  inset:0;
  background:linear-gradient(180deg, rgba(10,19,34,0) 0%, rgba(10,19,34,.08) 16%, rgba(10,19,34,.40) 28%, rgba(10,19,34,.58) 40%, rgba(10,19,34,.70) 64%, rgba(10,19,34,.84) 100%);
  pointer-events:none}
/* En móvil el escudo es vertical y arrancaba en .26: el sobretítulo .fhero__nom es
   nuevo y cae justo en esa franja de arriba, la más débil (3.48 en central-escorza).
   Se sube el arranque; el 40% y el 100% se quedan igual, así que el cielo del render
   pierde poco y la zona que ya estaba resuelta no se mueve. */
/* R43 · el único bloque que no cerró con el ajuste general fue kartesia a 390x844: su
   .entrada mide 7 renglones (212 px medidos) y el hero quedaba en 852 px con el texto
   arrancando en y=0, o sea las migas debajo del nav fijo (64 px). Se baja la escala de la
   entrada SOLO en móvil — no se acorta copy del cliente ni se recortan renglones, que es
   lo que el brief prohíbe; se baja el tamaño, que es lo que el brief manda ("baja lo que
   tengas que bajar"). Aplica a las 8 por consistencia, no como excepción de una ficha. */
@media (max-width:760px){
  .fhero p.entrada{font-size:1rem;line-height:1.42}
  .fhero .datos dd{font-size:var(--t-cuerpo)}
  .fhero .datos div{padding-top:8px;padding-bottom:10px}
}
@media (max-width:760px){ .fhero::before{inset:0 -8vw 0 -8vw;width:auto;
  /* R43: anclado al hero, el degradado ya es 180deg desde y=0 — no hay borde superior
     que difuminar, así que el mask-image de 140 px se retira igual que en escritorio. */
  -webkit-mask-image:none;mask-image:none;
  /* R44b: .52 → .62 en el arranque. Con el hero de 1 viewport (R43) las migas subieron
     a la franja mas alta del bloque y ahi el peor pixel daba 4.32 — bajo el piso AA de
     4.5. Es el unico stop que se toca en movil; el 40% y el 100% se quedan. */
  /* R44c: con la sombra por glifo el velo movil deja de ser lo unico que sostiene la
     tinta chica, asi que baja igual que el de escritorio: .62/.74/.86 → .54/.68/.84. Un escalon
     mas abajo (.46/.62/.78) tumbaba el dt de quadrante a 3.72, bajo AA. */
  background:linear-gradient(180deg, rgba(10,19,34,.54) 0%, rgba(10,19,34,.68) 40%, rgba(10,19,34,.84) 100%)} }
/* R44c · Jara: "menos agresivo". El velo no se puede bajar en bloque porque quien lo
   ata al piso es la tinta MAS CHICA del hero — las migas de central-escorza daban 4.74
   y a −18% de velo caian a 3.19. Bajar el velo para sostener un link de 13 px es pagar
   con la foto entera del cliente. Se invierte el reparto: cada pieza de texto lleva su
   propia sombra (cuesta cero imagen, solo cubre el glifo) y el velo baja global. Los
   valores son por tamano: cuanto mas chico el texto, mas cerrada la sombra. */
.fhero .migas, .fhero .eyebrow, .fhero__nom, .fhero .datos dt, .fhero .datos dd,
.fhero p.precio-cta a{
  /* .cinta NO entra: sus botones traen fondo solido propio, y sobre el blanco la
     sombra se ve como un halo sucio alrededor de la tinta oscura. Solo lleva sombra
     el texto que cae directo sobre la foto. */
  text-shadow:0 1px 2px rgba(6,12,22,.85), 0 0 8px rgba(6,12,22,.6)}
.fhero h1{text-shadow:0 2px 10px rgba(6,12,22,.55)}
/* dt y migas son la tinta mas chica y la mas apagada (--niebla): a 11-13px el halo
   ancho se diluye antes de cubrir el glifo. Sombra mas cerrada y mas densa, solo
   para ellos — 4.38 → AA en quadrante a 390, sin tocar el velo. */
.fhero .datos dt, .fhero .migas, .fhero .eyebrow{
  /* R68: eyebrow se suma a este trío (antes solo dt/migas) — es el peor "p" medido en
     G-CONTRASTE-HERO en 7/8 fichas (qa/r68/contraste_hero.py), y vive en la zona alta de
     .env donde el escudo local nuevo (línea ~995) se mantiene bajo a propósito para no
     invadir la ventana de la máscara (G-VENTANA). Blur del tercer layer sube de 10 a
     16px -- el método de medición muestrea fondo a 14-26px del glifo; a 10px el halo no
     llegaba a esa banda. */
  text-shadow:0 1px 1px rgba(6,12,22,.95), 0 0 5px rgba(6,12,22,.9), 0 0 16px rgba(6,12,22,.75)}
/* R68: opacity:.95 -> 1. Sobre foto (no sobre --papel sólido) opacity mezcla el glifo
   con lo que hay detrás -- exactamente lo contrario de lo que necesita el peor "p"
   medido en 7/8 fichas (qa/r68/contraste_hero.py). Sin costo visual real: 5% de mezcla
   sobre una foto oscura no se percibía como "sutileza", perdía contraste sin que nadie
   lo hubiera pedido a propósito. */
.fhero .eyebrow{color:var(--papel);opacity:1}
/* R39 · T2 — el nombre del desarrollo, ahora que el h1 lo titula con la tesis.
   No desaparece del hero: sube a sobretítulo en la display, a --t-h3, justo encima
   del titular. Sigue siendo lo primero que se lee en grande después del eyebrow. */
.fhero__nom{font-family:var(--display);font-size:var(--t-h3);color:var(--papel);
  letter-spacing:.16em;text-transform:uppercase;margin:8px 0 6px;line-height:1.1}
/* El h1 pasó de una palabra a una frase con cifra. A 11vw una frase de ~30 caracteres
   se come el hero entero y desborda por arriba (overflow:hidden recorta, y lo que
   recorta es el titular). Se baja el tope a 6.5rem y el tramo fluido a 7.2vw; el
   ratio display/cuerpo queda en ×5.8 a 1440 — por encima de la barra de ×4.5 del
   skill y por encima de la landing A (×5.14). max-width en ch para forzar 2-3
   renglones de lectura en vez de una línea kilométrica. La caja sigue cruzando el
   fold en 8/8: al ganar renglones el h1 crece hacia ARRIBA (el pie del hero está
   anclado con align-items:flex-end), así que el bottom no se mueve. */
/* R43 · T3 — el h1 usa --t-hero, el MISMO token del home (68px@1440 / 41.6px@390).
   No es un tamaño nuevo: --t-hero ya vivía en la escala de 7 escalones de este archivo
   (línea 51) y el home lo usa en index.html:329. El clamp propio (hasta 104px) existía
   para que la caja del titular cruzara el fold; con el hero en una pantalla eso ya no
   aplica y 104px no cabe: medido, el bloque completo pedía 781 px a 1280x700 contra un
   presupuesto de 630 (viewport 700 − nav 70). Ahorro medido del cambio: −106 px en
   central-escorza a 1280, −60 px a 390. max-width en ch, así que al bajar el tamaño la
   caja se angosta con él y el número de renglones no cambia. */
.fhero h1{font-family:var(--display);font-size:var(--t-hero);
  line-height:.98;color:var(--papel);letter-spacing:-.025em;margin:0 0 14px;
  max-width:var(--fhero-h1-maxw,16ch);text-wrap:balance}
/* Arreglos #6/#7: el velo lateral se apaga hacia el borde derecho del párrafo y dos
   fichas (montevideo, nikté) traían --fhero-brillo alto, así que el peor píxel del
   glifo caía sobre un punto claro de la foto. Un text-shadow por sí solo deja fugas en
   el borde antialiaseado del glifo cuando la foto tiene un highlight justo detrás; una
   placa sólida (box-decoration-break:clone para envolver cada línea) sí lo cubre siempre,
   sin depender del brillo de la foto en ese punto. */
/* R44 · Jara: el parrafo se leia como "parche pegado". La placa .9 opaca con esquinas
   duras nacio (arreglos #6/#7) cuando el parrafo caia sobre el EDIFICIO del render y el
   escudo local no llegaba hasta ahi. Con el escudo anclado al hero (R43) el parrafo vive
   dentro de la meseta: y=564-634 a 1440, donde la mascara ya vale 1 y el alfa es .74-.80.
   La placa dejo de ser lo que sostiene el contraste y paso a ser solo una caja visible.
   Se retira; queda un text-shadow corto que cubre la fuga de antialias sobre un highlight
   puntual, que era el otro motivo del #6. Numero, no fe: medido con contraste_glifos.py
   sobre `.fhero p.entrada` en las 8 fichas. */
.fhero p.entrada{color:var(--papel);max-width:48ch;margin:0;font-size:var(--t-cuerpo);
  text-shadow:0 1px 3px rgba(6,12,22,.72), 0 0 10px rgba(6,12,22,.5)}
.migas{position:relative;z-index:2;font-size:var(--t-chico);color:var(--papel)}
/* zona táctil ≥44px sin mover el texto: padding en el <a>, borde en .ln (auditoría §tactil) */
.migas a{display:inline-block;padding:13px 0;margin:-13px 0}
.migas a .ln{border-bottom:1px solid rgba(220,230,242,.4);padding-bottom:1px}
.migas a:hover .ln{color:var(--papel);border-bottom-color:var(--papel)}

.datos{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:0;
  border-top:1px solid var(--linea-osc);margin-top:clamp(16px,2.4vh,26px)}
.datos div{padding:14px 22px 4px 0;border-right:1px solid var(--linea-osc)}
.datos div:last-child{border-right:0}
.datos dt{font-size:var(--t-micro);letter-spacing:.2em;text-transform:uppercase;color:var(--niebla);margin:0 0 8px}
.datos dd{margin:0;font-family:var(--display);font-size:var(--t-h3);color:var(--papel);line-height:1.1}
/* fichas sin precio de lista: una sola celda no debe estirarse a todo el ancho */
.datos--una{grid-template-columns:minmax(148px,220px)}

/* precio sin cifra, resuelto como acción en vez de hueco en la rejilla (auditoría §6) */
.fhero p.precio-cta{margin:clamp(14px,2vh,22px) 0 0}
.fhero p.precio-cta a{display:inline-block;font-family:var(--display);font-size:var(--t-cuerpo);
  color:var(--papel);padding:13px 0 15px;margin:-13px 0 -15px;transition:opacity .2s ease}
/* zona táctil ≥44px en el <a> (auditoría §B); la línea vive en el <span> pegada al texto, no al padding */
.fhero p.precio-cta a .ln{border-bottom:1px solid var(--acento-osc, var(--cielo));padding-bottom:2px}
.fhero p.precio-cta a:hover{opacity:.82}
@media (max-width:640px){.datos div{border-right:0;border-bottom:1px solid var(--linea-osc);padding-bottom:16px}}

.dosc{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:clamp(32px,6vw,86px);align-items:start}
@media (max-width:900px){.dosc{grid-template-columns:1fr;gap:34px}}
/* BRIEF_28b: la tinta de .precio a escala --t-clima ("$4.8 MDP" ronda los 850-1100px
   de ancho de glifo real en pantallas grandes) no cabe en una columna de ~500-700px
   sin desbordarse sobre "Plan de pagos" -- medido con Range.getClientRects, no con el
   bounding box (por eso overflow del documento daba 0). No se resuelve encogiendo el
   pico bajo el umbral de 150px: el precio pasa a fila completa cuando trae cifra real,
   dandole todo el ancho de .env (hasta 1240px) en vez de la mitad de la grid. Las 5
   fichas sin precio (CTA de texto) no se tocan -- ahi no hay cruce. */
.dosc:has(.precio):not(:has(.precio > a)){grid-template-columns:1fr}
/* BRIEF_28c: el precio conserva fila propia a todo el ancho (nunca comparte fila con
   nada -- por construccion, no por umbral, asi el cruce de tinta queda imposible en
   vez de "improbable"); el bloque de apoyo (descripcion + plusvalia a la izquierda,
   plan de pagos a la derecha) sube a 2 columnas alineadas al pie del precio, solo
   cuando hay ancho para leerlas comodas. Por debajo de 1100px sigue el apilado 28b.
   div:first-child pasa a display:contents para que sus hijos (eyebrow/precio/desc/
   plus) sean items directos del grid de .dosc y se puedan ubicar por fila -- no
   mueve nada en el HTML, solo cambia que caja los envuelve visualmente. */
@media (min-width:1100px){
  .dosc:has(.precio):not(:has(.precio > a)){
    grid-template-columns:minmax(0,.82fr) minmax(0,1.18fr);
    gap:0 clamp(32px,6vw,86px);
  }
  .dosc:has(.precio):not(:has(.precio > a)) > div:first-child{display:contents}
  .dosc:has(.precio):not(:has(.precio > a)) > div:first-child > p.eyebrow{grid-column:1/-1;grid-row:1}
  .dosc:has(.precio):not(:has(.precio > a)) > div:first-child > .precio{grid-column:1/-1;grid-row:2}
  .dosc:has(.precio):not(:has(.precio > a)) > div:first-child > .precio + p{grid-column:1;grid-row:3}
  .dosc:has(.precio):not(:has(.precio > a)) > div:first-child > .plus{grid-column:1;grid-row:4}
  .dosc:has(.precio):not(:has(.precio > a)) > div:last-child{grid-column:2;grid-row:3 / 5;align-self:start}
  .dosc:has(.precio):not(:has(.precio > a)) > div:last-child .cinta{margin-top:26px}
}

/* BRIEF_29: momento firma — LA PARTICIÓN. El Sistema Lock-Off (6/8 desarrollos venden
   literalmente un depa que se corta en dos espacios) se vuelve el eje visual de la
   sección: un corte real -- línea + bloque de color -- parte el .dosc en dos mitades,
   cada una declarando su propio punto (verbatim de desarrollos.json .dif.puntos, cero
   copy nuevo). Sin motor de motion nuevo: el corte es composición, no efecto. En
   montevideo/nikte (dif:null) el mismo eje se aplica sin .particion--firma: es la
   espina compositiva del sitio, no el momento firma (que solo existe donde el
   argumento de venta lo sostiene). En <900px el eje pasa de vertical a horizontal --
   ambas mitades se apilan pero conservan su fondo propio, nunca se reducen a texto
   plano suelto (regla dura de usabilidad §4 del brief). */
.dosc.particion{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0;align-items:stretch;position:relative}
.dosc.particion > div:first-child{padding:0 clamp(32px,5vw,64px) 0 0}
/* BRIEF_31: el corte se comporta como canto físico, no como raya de color. Grano real
   (SVG feTurbulence inline, sin librería/canvas/imagen) + biselado de 3 capas en vez de
   un border-left uniforme -- luz arriba del canto, sombra de contacto adentro, sombra
   proyectada hacia la mitad calma (una mitad "se monta" sobre la otra). Nada de brillo
   ni sombra difusa grande: eso es "subirle a la luz", el error que este skill nombra
   explícitamente como salida ilegítima. */
.dosc.particion > div:last-child{
  padding:clamp(40px,6vw,72px) clamp(32px,5vw,64px);
  background-color:var(--navy);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.39'/%3E%3C/svg%3E");
  background-blend-mode:overlay;
  color:var(--blanco);
  border-left:1px solid var(--acento-osc, var(--cielo));
  box-shadow:
    inset 3px 0 0 rgba(255,255,255,.14),
    inset 8px 0 6px -6px rgba(0,0,0,.45),
    -5px 0 12px -7px rgba(0,0,0,.32);
}
.dosc.particion > div:last-child .eyebrow{color:var(--niebla)}
/* nikté: .cifras cae dentro de la mitad de color (único dev sin .dosc propio para el
   eje) -- sus tokens de texto son para fondo claro, se invierten aquí a los de
   .seccion-osc para no perder contraste, cero cambio de dato. */
.dosc.particion > div:last-child.cifras,.dosc.particion > div:last-child .cifras{border-top-color:var(--linea-osc)}
.dosc.particion > div:last-child.cifras div,.dosc.particion > div:last-child .cifras div{border-right-color:var(--linea-osc)}
.dosc.particion > div:last-child.cifras b,.dosc.particion > div:last-child .cifras b{color:var(--blanco)}
.dosc.particion > div:last-child.cifras p,.dosc.particion > div:last-child .cifras p{color:var(--niebla)}
/* montevideo (composición sin firma): su .lista-p normal también cae en la mitad de
   color -- mismo ajuste de contraste que .seccion-osc .lista-p, sin tocar el texto. */
.dosc.particion > div:last-child .lista-p li{border-top-color:var(--linea-osc)}
.dosc.particion > div:last-child .lista-p li::before{background:var(--cielo)}
/* firma: los puntos (2 declaraciones, una por mitad conceptual) pasan a tamaño de
   cartel -- el corte se lee de un vistazo. BRIEF_32 (2026-08-28): "height:100% +
   justify-content:center" tapaba el hueco ESTIRANDO -- el bloque se centraba dentro de
   la caja completa (estirada por align-items:stretch de la grid al alto de la mitad
   calma), en vez de que la caja midiera su propio contenido. Llenado real medido contra
   site/_deploy antes de este fix: 23.5%-33.1% @1440, las 6 fichas --firma nunca se
   medían (gate_particion condicionaba el check a SIN_FIRMA). Mismo remedio que 29c:
   align-self:start saca la mitad del stretch. */
.particion--firma > div:last-child{align-self:start}
/* la regla base .dosc.particion > div:last-child (arriba) ya trae padding propio con
   más especificidad (2 clases+pseudo vs 1 clase+pseudo aquí) -- sin calificar con
   .dosc.particion también, este override nunca ganaba y los dos intentos previos de
   apretar el padding no hicieron nada medible (mismos números antes/después). */
.dosc.particion.particion--firma > div:last-child{padding:clamp(10px,1.4vw,16px) clamp(20px,3vw,34px)}
/* FASE4 D4-a — probado y descartado: subir el padding-bottom del panel NO
   mueve el barrido (verificado: mismas 8/6 paradas antes y después). La
   ventana de colisión en scrollY = altura del elemento + altura del header
   es geométrica y depende SOLO de la posición absoluta de cada <li>, no de
   qué haya antes o después en el flujo — padding no la puede cerrar. El fix
   real vive en .nav.pegado, ver más abajo (línea ~173). */
.particion--firma > div:last-child .lista-p{
  list-style:none;padding:0;margin:0;
  display:flex;flex-direction:column;gap:clamp(14px,2vw,22px)
}
/* RONDA 35 M4 — a --t-hero (68px) una frase de 12 palabras no cabe en ninguna
   columna: "Habita un espacio y renta el otro en Airbnb." partía en 4 renglones con
   "Airbnb." solo. Primero baja la escala (42px@1440), después balance -- en ese
   orden: balance sobre 68px seguiría dejando la viuda, solo mejor repartida. */
.particion--firma > div:last-child .lista-p li{
  border:0;padding:0;color:inherit;text-wrap:balance;
  font-family:var(--display);font-size:clamp(1.9rem, 1.2rem + 1.6vw, 2.75rem);line-height:1.15
}
.particion--firma > div:last-child .lista-p li::before{display:none}
/* BRIEF_29c: la mitad degradada (montevideo/nikté, sin dif) medía 12-20% de llenado --
   72% de tinta muerta en nikté, 63% en montevideo (Range.getClientRects, no la caja).
   No hay foto libre que no repita Amenidades (ver brief), así que se llena con el DATO
   que ya vive ahí, a la escala del numeral vivo (mismo criterio de ronda 28: el dato
   manda el tamaño) -- nunca inventando copy ni imagen.
   CORRECCIÓN BRIEF_32 (2026-08-28): el claim de esta línea decía "particion--firma NO
   se toca: ya pasa el umbral" -- nunca se midió. gate_particion.mjs condicionaba el
   check6 a SIN_FIRMA, así que las 6 fichas --firma nunca entraban a la medición. Medido
   ese día contra site/_deploy: 22.8%-33.1% de llenado @1440/1280, muy por debajo de
   cualquier umbral razonable. Corregido en el propio BRIEF_32: ver .particion--firma
   más abajo (align-self:start + padding propio + font-size:var(--t-hero)); piso real
   post-fix 47.5% (hof@1280) -- umbral fijado en 0.47 en gate_particion.mjs. */
/* el hueco no se tapa estirando -- se elimina: align-self:start saca esta mitad del
   stretch de la grid para que su alto sea el de su propio contenido (más padding),
   nunca el de la mitad calma. Combinado con la escala del numeral (--t-hero, ya sin
   gaps de sobra) sube el llenado sin inventar aire nuevo. */
.dosc.particion:not(.particion--firma) > div:last-child{align-self:start;padding:clamp(4px,0.6vw,8px) clamp(16px,2.4vw,32px)}
.dosc.particion:not(.particion--firma) > div:last-child .lista-p{
  list-style:none;padding:0;margin:0;
  display:flex;flex-direction:column;gap:clamp(20px,2.6vw,30px)
}
.dosc.particion:not(.particion--firma) > div:last-child .lista-p li{
  border:0;padding:0;color:inherit;
  font-family:var(--display);font-size:var(--t-h2);line-height:1.25
}
.dosc.particion:not(.particion--firma) > div:last-child .lista-p li::before{display:none}
/* .cifras puede SER div:last-child (nikté, sin .dosc propio) o vivir anidado dentro de
   él (montevideo) -- mismo bug de combinador que las líneas 971-974, mismo fix: forma
   compuesta + descendiente en cada selector. */
.dosc.particion:not(.particion--firma) > div:last-child.cifras,
.dosc.particion:not(.particion--firma) > div:last-child .cifras{
  display:flex;flex-direction:column;gap:clamp(2px,0.3vw,5px);
  margin-top:clamp(5px,0.8vw,9px);border-top:1px solid var(--linea-osc)
}
/* nikté: .cifras ES el propio div:last-child -- padding-top ya lo da la regla del
   contenedor de arriba; darlo dos veces aquí colisionaba con esa. Montevideo (.cifras
   anidado, no es div:last-child) sí necesita su propio respiro bajo el divisor. */
.dosc.particion:not(.particion--firma) > div:last-child .cifras{padding-top:clamp(5px,0.8vw,9px)}
/* fila horizontal (numeral + etiqueta) en vez de apilada: la etiqueta ya no abre una
   línea propia -- ahorra ~40px de alto muerto por cifra sin tocar el dato ni el tamaño. */
.dosc.particion:not(.particion--firma) > div:last-child.cifras div,
.dosc.particion:not(.particion--firma) > div:last-child .cifras div{
  border:0;padding:0;display:flex;align-items:baseline;gap:14px;flex-wrap:wrap
}
/* RONDA 35 M3 — aquí había TRES tamaños distintos (130px base, 165px para el primer
   ítem de las fichas sin precio, 154px para nikté) nacidos de parches sucesivos
   contra un gate viejo que exigía un pico >=150px. Ese gate lo revirtió Jara el
   29-ago: el pico es UNO por página y vive en el h1 del .fhero. Tres números por
   parche son deuda, no diseño: convergen en una sola escala, la misma que .precio.
   El caso que fijó el 130 ("91-292" de montevideo, 551px de tinta contra 460px de
   columna a 1100px) baja a 292px: cabe de sobra.
   R69: esta regla gana por especificidad sobre la base .cifras b -- necesita el
   mismo clamp restated o hereda el valor inicial. Mismo tope --t-cifra-cap. */
.dosc.particion:not(.particion--firma) > div:last-child.cifras b,
.dosc.particion:not(.particion--firma) > div:last-child .cifras b{
  font-size:clamp(2rem, 1.85rem + 2.5cqw, var(--t-cifra-cap));letter-spacing:-.02em
}
.dosc.particion:not(.particion--firma) > div:last-child.cifras p,
.dosc.particion:not(.particion--firma) > div:last-child .cifras p{font-size:var(--t-cuerpo);margin:0}

/* BRIEF_PROYECTO_EDITORIAL_MONTEVIDEO_2026-09-13 [JARA]: retira el mecanismo
   'se compone y se abre' (BRIEF_PROYECTO_ERA_PROTO_2026-09-12) -- Jara:
   "texto grande innecesario (hay hasta regla contra esto)". La mitad navy vuelve
   a hoja técnica, a tamaño de dato: dt/dd copiado tal cual de `.datos` (mismo
   rótulo micro + serif del sitio), pero apilado con hairline (border-top, sin
   cajas) en vez de columnas, y con dd a --t-cuerpo -- nunca mayor que el dd del
   hero en ningún breakpoint (criterio de BRIEF_PROYECTO_EDITORIAL_MONTEVIDEO).

   BRIEF_PROYECTO_PLANO_MONTEVIDEO_2026-09-13 [JARA]: la caja navy de arriba se
   retira entera ("Powerpoint vibes", 02:36) -- .ficha-tec ahora vive siempre
   sobre --papel (ver .dosc.plano-ed más abajo), así que dt/dd/nota pasan de los
   tokens de fondo oscuro (--niebla/--papel/--linea-osc) a los de fondo claro
   (--niebla-papel/--asfalto/--linea) -- mismo componente, un solo contexto de
   color posible ahora (esta función solo la usa montevideo, construir_fichas.py
   L1776). */
.ficha-tec__nota{margin:0 0 14px;color:var(--niebla-papel);font-size:var(--t-chico);max-width:42ch}
.ficha-tec__nota:last-of-type{margin-bottom:0}
.ficha-tec__lista{margin-top:22px;padding-top:2px}
.ficha-tec__lista div{border-top:1px solid var(--linea);padding:12px 0}
.ficha-tec__lista div:first-child{border-top:0;padding-top:0}
.ficha-tec__lista dt{font-size:var(--t-micro);letter-spacing:.2em;text-transform:uppercase;color:var(--niebla-papel);margin:0 0 6px}
.ficha-tec__lista dd{margin:0;font-family:var(--display);font-size:var(--t-cuerpo);color:var(--asfalto);line-height:1.2}

/* BRIEF_PROYECTO_PLANO_MONTEVIDEO_2026-09-13 [JARA]: reemplaza `.dosc.particion`
   para montevideo -- sin caja de color, sin 2 columnas ni en desktop. Bloque
   único: eyebrow/h2/plano/notas/dl comparten el mismo borde izquierdo por
   construcción (misma columna, mismo padding del contenedor .seccion) en
   360/390/412/1440 -- el "brinco de 7px" medido en prod (h2 en x=21, .ficha-tec
   en x=14) no puede volver a pasar porque ya no hay una segunda columna con
   padding propio. Gap entre título y ficha-tec: clamp(20px,3vw,40px), mismo
   valor que .devs/.contacto (línea 746/897), no un número nuevo. */
.dosc.plano-ed{display:block}
.dosc.plano-ed > div:first-child{margin:0 0 clamp(20px,3vw,40px)}
/* BRIEF_PROYECTO_WORDSPACING_MONTEVIDEO_2026-09-13 [JARA]: `.ficha-tec__plano`/
   `.ficha-tec__nivel` retirados -- el plano en línea se descartó ("como un
   modelo de los que ves después, pero peor hecho"). Los .webp se quedan en
   disco, sin referencia HTML ni CSS. Mecánica de reveal nueva: § R56
   wordspacing, más abajo junto al resto de mecanismos --on. */

@media (max-width:900px){
  .dosc.particion{grid-template-columns:1fr;gap:0}
  .dosc.particion > div:first-child{padding:0 0 32px}
  .dosc.particion > div:last-child{
    border-left:0;
    box-shadow:
      inset 0 3px 0 rgba(255,255,255,.14),
      inset 0 8px 6px -6px rgba(0,0,0,.45),
      0 -5px 12px -7px rgba(0,0,0,.32);
    margin-inline:calc(-1 * var(--pad));padding-inline:var(--pad);
  }
  /* BRIEF_29c: a 390 el padding lateral de la regla de arriba (clamp con piso 32px)
     seguía ganando por especificidad y se comía ~16% del ancho en pura tira lateral --
     se reduce solo para la mitad degradada, donde el llenado es la métrica que manda. */
  .dosc.particion:not(.particion--firma) > div:last-child{
    padding:clamp(6px,1vw,12px) clamp(12px,3.4vw,20px)
  }
  /* BRIEF_45 · defecto 2 (RESPUESTA_KRILLIN_45): este override rompía la escala única de
     .cifras b -- 84px aquí contra 48px (clamp fluido) en las otras seis fichas, mismo
     componente. Se retira: nikté y montevideo vuelven al mismo clamp(3rem,2rem+3.4vw,5.5rem)
     de la línea 1300-1302, que ya heredaban antes de este parche. Sin escalón propio. */
}

.lista-p{list-style:none;padding:0;margin:0}
.lista-p li{padding:16px 0 16px 30px;border-top:1px solid var(--linea);position:relative}
.lista-p li::before{content:"";position:absolute;left:0;top:26px;width:14px;height:1px;background:var(--navy);opacity:.55}
.seccion-osc .lista-p li{border-top-color:var(--linea-osc)}
.seccion-osc .lista-p li::before{background:var(--cielo)}

/* BRIEF_28: momento firma awwwards-bar — un solo pico por ficha, escala --t-clima
   (mismo token que #conteo del home: una sola voz de "el dato manda el tamaño"
   en todo el sitio). Solo aplica cuando .precio trae una cifra real (Quadrante,
   Kartesia, Nikté); en las 5 fichas sin precio publicado .precio es un CTA de
   texto ("Con asesor →") y el pico se corta aquí para no forzar una frase larga
   a tamaño de cartel — se traslada al primer dato de .cifras (ver esa regla). */
.precio{font-family:var(--display);font-size:var(--t-h2);line-height:1;margin:0}
/* RONDA 37: el color vivía en un style inline del generador, así que ninguna
   sección de otro fondo podía corregirlo — sobre la Inversión clara de kartesia
   daba 2.05:1 con 18px. --niebla está medido contra --tinta (7.3:1). */
.precio__sub{color:var(--niebla);margin:16px 0 0;max-width:46ch}
/* RONDA 35 M3 — --t-clima daba 164px@1440: un precio de $5 MDP a ese tamaño grita
   el costo en vez de vender el activo (Jara, 29-ago). Sigue siendo el elemento
   más grande de SU sección, deja de serlo de la página. nowrap se queda:
   "$5 MDP" nunca debe partirse.
   R69 · Krillin: mismo defecto que .cifras b (81px > 68px del h1). .precio no vive
   dentro de un .cifras con container-type, y no lo necesita -- es una sola línea
   nowrap, nunca se apila. Mismo tope --t-cifra-cap, crece con vw como antes.
   Medido: 55.7px@1440 · 48.2px@1100 · 32.6px@390 (quadrante/kartesia/nikté, las
   3 fichas con precio publicado). */
.precio:not(:has(> a)){font-size:clamp(2rem, 1.5rem + 2.2vw, var(--t-cifra-cap));letter-spacing:-.03em;text-wrap:nowrap;white-space:nowrap}
.precio small{display:block;font-family:var(--cuerpo);font-size:var(--t-micro);letter-spacing:.2em;
  text-transform:uppercase;color:var(--niebla);margin-bottom:14px}
/* precio sin cifra en "Inversión": mismo tratamiento de acción que el hero (auditoría §6/A) */
.precio a{display:inline-block;color:inherit;padding:11px 0;margin:-11px 0;transition:opacity .2s ease}
.precio a .ln{border-bottom:2px solid var(--acento-osc, var(--cielo));padding-bottom:2px}
.precio a:hover{opacity:.8}
.plan{list-style:none;padding:0;margin:30px 0 0;counter-reset:p}
.plan li{counter-increment:p;position:relative;padding:15px 0 15px 46px;border-top:1px solid var(--linea-osc)}
.plan li::before{content:counter(p);position:absolute;left:0;top:15px;
  font-family:var(--display);font-size:var(--t-chico);color:var(--niebla);
  border:1px solid var(--linea-osc);width:26px;height:26px;display:grid;place-items:center;border-radius:50%}
/* stats nunca en cajas (decisión Jara): hairline, como .datos/.cifras */
.plus{margin-top:34px;padding:18px 0 0;border-top:1px solid var(--linea-osc)}
.plus dt{font-size:var(--t-micro);letter-spacing:.2em;text-transform:uppercase;color:var(--niebla);margin:0 0 8px}
.plus dd{margin:0;font-family:var(--display);font-size:var(--t-h3);color:var(--papel);line-height:1.15}
.plus .fuente{display:block;margin-top:12px;font-family:var(--cuerpo);font-size:var(--t-micro);
  line-height:1.6;letter-spacing:.04em;text-transform:none;color:var(--niebla);max-width:42ch}

.galeria{display:grid;grid-template-columns:repeat(6,1fr);gap:clamp(8px,1.2vw,16px)}
.galeria figure{margin:0;overflow:hidden;background:var(--papel-2);grid-column:span 2}
.galeria figure:first-child{grid-column:span 4;grid-row:span 2}
.galeria img{width:100%;height:100%;object-fit:cover;aspect-ratio:4/3;
  transition:transform .8s var(--ease-out)}
.galeria figure:first-child img{aspect-ratio:16/10}
.galeria figure:hover img{transform:scale(1.045)}
@media (max-width:760px){.galeria{grid-template-columns:repeat(2,1fr)}
  .galeria figure,.galeria figure:first-child{grid-column:span 1;grid-row:auto}
  .galeria figure:first-child{grid-column:span 2}}
/* con solo 2 fotos el mosaico de 6 celdas se ve hueco: dos piezas grandes lado a lado */
.galeria--dos{grid-template-columns:repeat(2,1fr)}
.galeria--dos figure,.galeria--dos figure:first-child{grid-column:span 1;grid-row:auto}
.galeria--dos img,.galeria--dos figure:first-child img{aspect-ratio:4/3}
@media (max-width:760px){.galeria--dos{grid-template-columns:1fr}}
/* con 1 sola foto: pieza única grande, nunca el mosaico de 6 celdas a medio llenar */
.galeria--una{grid-template-columns:1fr}
.galeria--una figure,.galeria--una figure:first-child{grid-column:1;grid-row:auto}
.galeria--una img,.galeria--una figure:first-child img{aspect-ratio:21/9}
/* Jara 14-sep: la fachada real de Escorza es 16:9; a 21:9 se cortaba el techo o la calle. */
.galeria--una figure:first-child img[src$="/central-escorza/fachada.webp"]{aspect-ratio:16/9}
/* con 4 fotos el mosaico de 6 celdas deja la cuarta huérfana: 2×2 parejo (auditoría §9) */
.galeria--cuatro{grid-template-columns:repeat(2,1fr)}
.galeria--cuatro figure,.galeria--cuatro figure:first-child{grid-column:span 1;grid-row:auto}
.galeria--cuatro img,.galeria--cuatro figure:first-child img{aspect-ratio:4/3}

.amen{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:0 30px;list-style:none;padding:0;margin:0}
.amen li{padding:13px 0;border-top:1px solid var(--linea);font-size:var(--t-chico)}

.otras{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(14px,2vw,26px)}
@media (max-width:820px){.otras{grid-template-columns:1fr}}
/* R59 G-HUECO: --papel (#F2EEE7) mide luminancia 0.858, por encima del piso
   0.85 del gate -- se ve casi blanco mientras la imagen carga. --papel-2
   (#D4E4EA, Arctic tint) es el mismo azul pálido que ya usan .galeria figure,
   .ld-img y .media como reserva (luminancia 0.754). */
.otra{display:block;border:1px solid var(--linea);border-radius:var(--r);overflow:hidden;background:var(--papel-2);
  transition:transform .26s ease,background .26s ease,border-color .26s ease}
.otra:hover{transform:translateY(-3px);background:#fff;border-color:var(--navy)}
.otra img{aspect-ratio:16/10;object-fit:cover;width:100%;transition:transform .6s var(--ease-out)}
.otra:hover img{transform:scale(1.045)}
.otra .cuerpo{padding:18px 20px 22px}
.otra__cta{display:inline-flex;align-items:center;gap:8px;margin-top:14px;
  font-size:var(--t-micro);font-weight:500;color:var(--navy)}
.otra__cta .flecha{display:inline-block;transition:transform .24s ease}
.otra:hover .otra__cta .flecha{transform:translate(3px,-3px)}
.otra h3{margin:0 0 6px}
.otra .meta{font-size:var(--t-chico);color:var(--niebla-papel);margin:0}

.cinta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-top:34px}

/* ══ TEMPLATE MAESTRO (2026-08-16) ═══════════════════════════════════════
   Lo que la ficha tomó de la página suelta de Quadrante: afirmar un beneficio
   en el hero, cifras grandes en vez de viñetas, el plan de pagos como tramos
   con línea de tiempo, y una ruta de conversión siempre visible.
   Cada pieza se imprime SOLO si su dato existe — ver README del spec. */

/* afirmación del hero: el beneficio antes que el nombre */
.afirma{display:flex;flex-wrap:wrap;align-items:baseline;gap:10px 16px;margin:12px 0 0;
  padding:10px 0 0;border-top:1px solid var(--linea-osc)}
.afirma b{font-family:var(--display);font-size:var(--t-h2);
  line-height:1;color:var(--papel);font-weight:400}
.afirma span{font-size:var(--t-micro);letter-spacing:.18em;text-transform:uppercase;color:var(--papel)}

/* CTA del hero: B no tenía ninguno sobre la línea de flotación */
.fhero .cinta{margin-top:14px}

/* la tesis: cifras grandes. Solo con viñetas que traen número. */
/* R69: container-type aquí (no en un selector nuevo) porque .cifras es la única
   clase compartida por las dos formas del panel -- grid (hof/madison/…) y flex
   apilado (montevideo/nikté, ver la regla .dosc.particion… .cifras). Cualquiera
   de las dos hereda el contexto de contenedor sin declarar la propiedad dos veces. */
.cifras{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:0;
  margin-top:36px;border-top:1px solid var(--linea);container-type:inline-size}
.cifras div{padding:26px 26px 30px 0;border-right:1px solid var(--linea)}
.cifras div:last-child{border-right:0}
/* BRIEF_29b H-29.3: el rango "51-92 m²" (nikté) rompía en 4 rects -- el guion colgaba
   de renglón y el segundo número caía solo. nowrap lo cierra a 1 rect por cifra en
   todos los devs con rango, no solo nikté (mismo patrón en quadrante "63-123m²"). */
/* RONDA 35 M3 — la escala única de .cifras. Antes había cuatro tamaños distintos
   por parches sucesivos (68 base / 130 partición / 154 nikté / 165 sin-precio);
   convergieron en un clamp fijo de 81px@1440, 88px@1920, 69px@1100 -- SUPERIOR al
   h1 del .fhero (68px@1440): dos picos por página. */
/* R69 · Krillin, medido (BRIEF_R69_ESCALA_Y_BARRIDO §1-2): 81px contra 68px del h1,
   y el mismo clamp en vw no distinguía panel angosto (montevideo 491px, nikté 556px,
   1 columna) de panel ancho (1112px, 3-5 columnas) -- el número se leía "enorme"
   apilado en la caja angosta. Ahora crece con cqw (ancho del panel, container arriba)
   y el tope es --t-cifra-cap (calc de --t-hero * .85): nunca alcanza al h1, y en
   paneles angostos crece menos porque su cqw es menor. Números de la curva, medidos
   con Playwright real: hof(1112px)@1440→57.4px · montevideo(491px)@1440→41.9px ·
   nikté(556px)@1440→43.5px · las 8 fichas@390 (panel ≈350-364px)→35.4px (tope). */
.cifras b{display:block;font-family:var(--display);font-weight:400;line-height:1;
  font-size:clamp(2rem, 1.85rem + 2.5cqw, var(--t-cifra-cap));color:var(--navy);white-space:nowrap}
.cifras b i{font-style:normal;font-size:var(--t-cuerpo);letter-spacing:.02em;padding-left:.18em}
/* RONDA 35 M3 — esta regla era el 164px que seguía vivo en central-escorza, hof,
   madison y simona (las 4 fichas sin precio publicado): el "pico" se mudaba al
   primer dato de .cifras. Se retira. El pico de la ficha es UNO y vive en el h1 del
   .fhero; una partición de m² no necesita tamaño de cartel para leerse como dato.
   El primer ítem conserva la distinción por tracking, no por escala. */
body:has(.precio > a) .cifras div:first-child b{letter-spacing:-.03em}
.cifras p{margin:14px 0 0;color:var(--niebla-papel);font-size:var(--t-chico);max-width:34ch}
.cifras-resto{margin:24px 0 0;color:var(--niebla-papel);font-size:var(--t-chico);max-width:64ch}
@media (max-width:640px){.cifras div{border-right:0;border-bottom:1px solid var(--linea);padding:22px 0 24px}
  .cifras div:last-child{border-bottom:0}}

/* pagos: tramos + línea de tiempo */
.tramos{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:0;margin-top:30px;
  border-top:1px solid var(--linea-osc)}
.tramos div{padding:22px 24px 6px 0;border-right:1px solid var(--linea-osc)}
.tramos div:last-child{border-right:0}
.tramos div + div{padding-left:24px}
@media (max-width:640px){
  .tramos div{border-right:0;border-bottom:1px solid var(--linea-osc);padding-bottom:18px}
  .tramos div + div{padding-left:0}
  .tramos div:last-child{border-bottom:0}
}
.tramos b{display:block;font-family:var(--display);font-weight:400;line-height:1;
  font-size:var(--t-h2);color:var(--papel)}
.tramos p{margin:12px 0 0;color:var(--niebla);font-size:var(--t-chico)}
.linea-t{display:flex;align-items:center;gap:14px;margin-top:26px;
  font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;color:var(--niebla)}
.linea-t i{flex:1;height:1px;background:linear-gradient(90deg,var(--cielo),var(--linea-osc))}

/* CTA pegajoso.
   R39 (BRIEF_39, T1) — INVERSIÓN DEL APILADO. Antes el sticky se montaba ENCIMA del
   aviso (bottom:var(--aviso-alto)) y además el JS lo apagaba mientras el aviso
   estuviera en pantalla, por la regla "nunca los dos a la vez" de la auditoría §8.
   Medido a 390×844 en las 8 fichas: el aviso aparece al primer scroll y se queda
   hasta que alguien pulsa "Entendido", así que el sticky quedaba en
   visibility:hidden PERMANENTE — 0/8 — y el primer CTA sólido caía a 1,175 px.
   La regla se cumplía con la ruta de conversión muerta, que es peor que la regla.
   Ahora: el sticky es dueño del borde inferior (bottom:0) y el aviso —que es un
   toast de esquina de 340 px, no una barra— se levanta por encima de él. Conviven
   sin taparse (hit-test en el gate) y ninguno depende del otro. */
/* R58 FASE1/2 — G-CORTE. La lectura a ciega la puso como defecto #1: el
   border-top:1px solid duro de .scta decapitaba cualquier titular que
   quedara a media página bajo la barra (H2 al 59% en hof, P al 65% en
   móvil — BRIEF_R58 §1). Medido en R58 Fase 1 con tres rutas: esta (borde
   suave) es la única que no mueve layout (Δaltura 0px, docH idéntico en
   las 8 fichas) ni agrega bytes de JS. NO reduce cuánto tapa la barra
   (G-BARRA, retirado por Jara — insatisfacible por construcción: una barra
   fija sobre una página larga siempre coincide con algo en alguna parada).
   Solo mata el CORTE: el ::before de abajo repite el degradado de 28px que
   ya usa .nav.pegado::after (FASE4 D4, línea ~190) pero en la dirección
   opuesta, para que el titular se funda en la sombra en vez de partirse en
   la línea recta del borde. */
.scta{position:fixed;left:0;right:0;bottom:0;z-index:58;
  padding:10px var(--pad) calc(10px + env(safe-area-inset-bottom));
  background:rgba(12,22,38,.94);backdrop-filter:blur(10px);
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  /* En reposo se va entero fuera del cuadro. Ya no suma --aviso-alto: desde R39
     vive en bottom:0, así que su propio alto + el colchón de la sombra basta. */
  transform:translateY(calc(100% + 18px));
  visibility:hidden;pointer-events:none;
  transition:transform .3s var(--ease-out),visibility 0s .3s}
.scta::before{
  content:"";position:absolute;top:-28px;left:0;right:0;height:28px;z-index:-1;pointer-events:none;
  background:linear-gradient(to bottom,rgba(12,22,38,0) 0%,rgba(12,22,38,.55) 60%,rgba(12,22,38,.94) 100%);
  -webkit-mask-image:linear-gradient(to bottom,transparent 0%,black 100%);
          mask-image:linear-gradient(to bottom,transparent 0%,black 100%);
}
.scta.on{transform:none;visibility:visible;pointer-events:auto;transition-delay:0s,0s}
.scta__d{min-width:0;display:flex;flex-direction:column;gap:2px}
.scta__d b{font-family:var(--display);font-weight:400;font-size:var(--t-cuerpo);color:var(--papel);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.scta__d span{font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;color:var(--niebla)}
.scta .btn{flex:none}
@media (prefers-reduced-motion:reduce){.scta{transition:none}}
@media (min-width:901px){.scta{padding-inline:clamp(28px,5vw,90px)}}

/* 22-ago: CTA duplicado — Jara: "las páginas... tienen dos veces siempre visible
   'pedir asesoría'". El header es fijo, así que CUALQUIER otro botón "Pedir asesoría"
   que entre al viewport (el del hero, el de .scta) siempre convive con el del nav.
   Se turnan en vez de acumularse: el botón del header se esconde en cuanto el .scta
   sticky se activa (misma función, más contexto). En ≤900px se esconde siempre —
   ahí la vía persistente es el menú hamburguesa + el CTA del propio hero, y a 390px
   el botón del header y el del hero caían juntos en la primera pantalla sin scroll. */
@media (max-width:900px){ .nav .btn{display:none} }
body:has(.scta.on) .nav .btn{display:none}

/* Los campos del formulario no respondían al hover: 12 controles planos en el gate
   del §3, y justo en la superficie que convierte. Las opciones de chip ya lo tenían. */
.campo input[type=text],.campo input[type=email],.campo input[type=tel],.campo select,
.wa input{transition:border-color .2s ease,background-color .2s ease}
.campo input[type=text]:hover,.campo input[type=email]:hover,.campo input[type=tel]:hover,
.campo select:hover{border-color:var(--cielo);background:rgba(220,230,242,.05)}
.wa input:hover{outline:2px solid var(--cielo);outline-offset:2px}

/* ═══ ACENTO POR DESARROLLO ═════════════════════════════════
   --acento / --acento-osc llegan en el style del <body>, sacados de los vectores del
   brochure de cada proyecto (ver ACENTOS en construir_fichas.py). Solo decoración, y
   solo dentro de la ficha: el nav, el pie y los CTAs siguen siendo Eikonic puro. */
.fhero .eyebrow, .seccion .eyebrow{display:flex;align-items:center;gap:12px}
.fhero .eyebrow::before, .seccion .eyebrow::before{
  content:"";flex:none;width:22px;height:2px;background:var(--acento, var(--niebla-papel))}
.seccion-osc .eyebrow::before, .fhero .eyebrow::before{background:var(--acento-osc, var(--cielo))}
.afirma{border-top-color:var(--acento-osc, var(--linea-osc))}
.linea-t i{background:linear-gradient(90deg,var(--acento-osc, var(--cielo)),var(--linea-osc))}
/* el nav y el pie no heredan acento aunque estén dentro del body */
.nav .eyebrow::before, .pie .eyebrow::before{display:none}

/* ═══ PILOTO KARTESIA — SPEC_FICHA_V2 (2026-08-19) ═══════════════════════
   Todo lo de aquí abajo es ADITIVO: clases nuevas, ningún selector existente
   se toca. --stone es un token nuevo (no pisa --papel, que el chrome —nav,
   pie, botones— también usa como color de texto sobre fondo oscuro: pisarlo
   global hubiera cambiado el chrome de las 8 fichas, no solo el fondo de
   Kartesia). El fondo crema tibio va scoped solo a kartesia, vía el atributo
   data-ficha que solo esa ficha imprime en <body>. */
:root{--stone:#F2EEE7}
body[data-ficha="kartesia"] .seccion-papel{background:var(--stone)}

/* Hero: mismo shield local sobre el texto (.env::before, sin tocar — ahí vive
   la legibilidad y la regla #5 de "45% del ancho limpio"), pero el velo que
   cubre TODO el ancho (::after) baja de intensidad solo en kartesia — era el
   último tramo de navy plano medible que no es formulario ni pie. */
/* 23-ago: este override se retira. Existía para aligerar el velo SOLO en kartesia
   cuando la regla base era pesada; ahora la base ya es ligera y parametrizada por
   ficha, y mantenerlo dejaba a kartesia fuera del gate de luz nuevo (hero_luz.py). */

/* "Inversión" ya trae cifras grandes (cumple regla #3 sin ser plano), pero
   pintada de navy era el mayor bloque de navy plano controlable por el piloto
   (medido: ~6.8pp del 21.3% total). Pasa a clara SOLO en kartesia; se listan
   los textos/botón fantasma uno por uno en vez de redefinir --papel/--niebla
   local, porque --papel también es el fondo de los botones primarios
   (.seccion-osc .btn) — pisarlo los hubiera vuelto texto oscuro sobre botón
   oscuro, ilegible. */
/* RONDA 37 · T1: la Inversión de kartesia era la única clara (--stone = #F2EEE7,
   el mismo hex que --papel). No rompía la racha, la alargaba a 8 secciones — la
   peor de las 8 fichas — y arrastraba un texto a 2.05:1. Vuelve a --tinta como
   las otras 7; con eso los overrides de color de abajo sobraban y se retiran. */
/* modelos con planta */
.modelos{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
@media (max-width:900px){.modelos{grid-template-columns:repeat(2,1fr)}}
@media (max-width:520px){.modelos{grid-template-columns:1fr}}
.modelo{background:#fff;border:1px solid var(--linea);border-radius:var(--r);overflow:hidden;
  transition:transform .24s ease,border-color .24s ease}
.modelo:hover{transform:translateY(-3px);border-color:var(--navy)}
.modelo .ph{position:relative;aspect-ratio:3/2;background:var(--papel-2) center/contain no-repeat}
/* D2 SPEC_PASADA_ERA §C/§F.5: inerte en las 7 fichas sin data-sys=era (no meten <img>
   dentro de .ph, siguen con el background-image de siempre); en quadrante sustituye
   el fondo por el <img loading=lazy srcset> real -- mismo encaje visual (contain,
   centrado) que el background que reemplaza. */
.modelo .ph img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;object-position:center}
.modelo .ph .lo{position:absolute;left:10px;top:10px;background:var(--navy);color:#fff;
  font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;padding:5px 9px;border-radius:2px}
.modelo .mi{padding:16px 18px}
.modelo .mn{font-family:var(--display);font-weight:400;font-size:var(--t-cuerpo);color:var(--navy)}
.modelo .ms{color:var(--asfalto);font-weight:600;font-size:var(--t-chico);margin-top:2px}
.modelo .md{color:var(--niebla-papel);font-size:var(--t-chico);margin-top:6px}

/* ── carrusel de "ver todas" ─────────────────────────────────────────────────
   BRIEF 36. `.modelos-slider__track` no tuvo NUNCA una regla propia — ni en esta
   versión ni en construir_fichas.py.bak.20260822_172508. El marcado y el JS del
   slider daban por hecho un scroller horizontal que no existía: el track caía a
   display:block y las 12 cards se apilaban a ancho completo (10,267px de alto en
   quadrante@1440), y las flechas ‹ › corrían sin error moviendo nada, porque
   scrollWidth == clientWidth. Medido en qa/k36_slider.mjs. */
.modelos-slider__track{
  display:grid;grid-auto-flow:column;
  /* El carrusel es el mismo objeto que los destacados, no otra familia: misma card y
     mismo gap. A 1440 .modelos reparte 1112px en 4 columnas con gap 18 → 264.5px por
     card; (100% - 3×18px)/4 reproduce ese número exacto dentro del mismo contenedor.
     Los breakpoints siguen el reparto 4/2/1 de .modelos, no uno propio. */
  grid-auto-columns:calc((100% - 54px) / 4);
  gap:18px;
  overflow-x:auto;overflow-y:hidden;
  /* .modelo:hover levanta la card 3px: sin este aire el borde de arriba se recorta
     contra la caja de scroll. */
  padding-block:4px;
  scroll-snap-type:x mandatory;
  overscroll-behavior-x:contain;
  /* Barra oculta a propósito, por dos razones: con barra clásica el contenido mide
     ~15px menos y las cards dejan de coincidir con los destacados, y la barra comía
     alto dentro del <details>. La navegación la dan las flechas y las teclas ← →
     sobre el track, que ya trae tabindex="0" y su handler. */
  scrollbar-width:none;
}
.modelos-slider__track::-webkit-scrollbar{display:none}
.modelos-slider__track > .modelo{scroll-snap-align:start}
/* el track es focusable (tabindex="0"); sin esto el foco de teclado es invisible */
.modelos-slider__track:focus-visible{outline:3px solid var(--navy);outline-offset:3px}
@media (max-width:900px){.modelos-slider__track{grid-auto-columns:calc((100% - 18px) / 2)}}
@media (max-width:520px){
  /* Una card por vista a ancho completo (que es lo que hacen los destacados aquí)
     dejaría el carrusel sin ninguna señal de que hay más: se descuenta un asomo de
     52px para que la siguiente se vea entrar. */
  .modelos-slider__track{grid-auto-columns:calc(100% - 52px)}
}

/* flechas del slider "ver todas": sin estilo propio, el navegador las servía en botón nativo
   (borde outset negro, 22×31.6px). El <button> mide 44×44 de verdad (zona táctil real, medible con
   getBoundingClientRect); el círculo visible vive en .dot y se queda en 34px (auditoría §C). */
.modelos-slider__nav{display:flex;gap:10px;margin-top:16px}
.slider-nav{width:44px;height:44px;padding:0;border:0;background:transparent;cursor:pointer;
  display:grid;place-items:center}
.slider-nav .dot{display:grid;place-items:center;width:34px;height:34px;border:1px solid var(--linea);
  border-radius:50%;background:#fff;color:var(--navy);font-size:var(--t-chico);line-height:1;
  transition:border-color .2s ease}
.slider-nav:hover .dot{border-color:var(--navy)}
.slider-nav:disabled{cursor:default}
.slider-nav:disabled .dot{opacity:.35}

/* ── PROTO 2026-09-17: riel como presentación principal (ver BRIEF_FICHA_RIEL_MODELOS) ──
   El riel deja de vivir dentro de <details>: ya no hay reja de destacados encima ni
   "Ver las N configuraciones" que abrir. Reglas nuevas, .modelos y .modelo intactas. */
.modelos-slider--principal{margin-top:0}
.modelos-slider__count{margin:0 0 14px;font-size:var(--t-chico);color:var(--niebla-papel);
  font-variant-numeric:tabular-nums}
.modelos-slider__count-n{color:var(--navy);font-weight:600}
/* Escritorio: 3 cards a la vista (antes 4, medido en el <details> viejo). Mismo cálculo que
   .modelos-slider__track pero con 2 gaps de 18px repartidos entre 3 columnas en vez de 4. */
.modelos-slider--principal .modelos-slider__track{grid-auto-columns:calc((100% - 36px) / 3)}
@media (max-width:900px){
  .modelos-slider--principal .modelos-slider__track{grid-auto-columns:calc((100% - 18px) / 2)}
}
@media (max-width:520px){
  .modelos-slider--principal .modelos-slider__track{grid-auto-columns:calc(100% - 52px)}
}

/* lock-off: banda navy con imagen, nunca navy plano (regla dura #3 del spec) */
/* el diagrama trae fondo claro propio (#F1F1F1, medido) — un marco oscuro
   le agregaba un letterbox que no pinta nada y solo sumaba navy plano */
.lockoff-diagrama .ld-img{border-radius:var(--r);overflow:hidden;aspect-ratio:4/3;
  background:var(--papel-2);border:1px solid var(--linea)}
.lockoff-diagrama .ld-img img{width:100%;height:100%;object-fit:contain}

/* ubicación + mapa: layout split reusado en amenidades con foto */
.split{display:grid;grid-template-columns:1.05fr 1fr;gap:clamp(28px,5vw,56px);align-items:center}
@media (max-width:900px){.split{grid-template-columns:1fr;gap:28px}}
.split .media{border-radius:var(--r);overflow:hidden;aspect-ratio:4/3;background:var(--papel-2)}
.split .media img{width:100%;height:100%;object-fit:cover;transition:transform .6s ease}
.split .media:hover img{transform:scale(1.04)}
.split--amen{align-items:start}
/* BRIEF_29 §6: greybox con buen gusto (pecado #3 de awwwards-bar) -- no es momento
   firma, es economía: más alto para que el mapa sea usable, sin el lavado gris que
   lo hacía sentir un trámite y no un dato. Cero copy nuevo, cero segundo mecanismo. */
.mapbox{margin-top:26px;border:1px solid var(--linea);border-radius:var(--r);overflow:hidden;
  height:clamp(280px,38vw,420px)}
.mapbox iframe{width:100%;height:100%;border:0}
.mapbox:focus-within{outline:3px solid var(--navy);outline-offset:2px}
/* F2J T1: a 390px el clamp(280px,38vw,420px) resuelve en su piso (280px) -- gate de
   altura de UBICACIÓN (<=1.6vh) no cierra con un mapa de 280px de alto en móvil. */
@media (max-width:760px){.mapbox{height:160px}}
/* F2J T1 (BRIEF_F2J_UBICACION_Y_HERO_2026-09-17): <details> nativo, cero JS. Cerrado
   por default -- el dato completo (vialidades + puntos) sigue en el DOM, un tap lo abre. */
details.entorno-disc{margin-top:clamp(8px,1vw,12px)}
details.entorno-disc>summary{cursor:pointer;list-style:none;margin-bottom:0}
details.entorno-disc[open]>summary{margin-bottom:10px}
details.entorno-disc>summary::-webkit-details-marker{display:none}
details.entorno-disc>summary::after{content:"+";margin-left:auto;font-family:var(--display);
  font-weight:400;font-size:1.1em;color:var(--niebla-papel)}
details.entorno-disc[open]>summary::after{content:"–"}
details.entorno-disc .entorno,
details.entorno-disc>div{margin-top:0}
/* BRIEF_30: Ubicación deja de ser greybox. El mecanismo es "el entorno nombrado" -- la
   lista real de vialidades y puntos de interés del brochure (verbatim, ver
   UBICACION_FUENTE.json) pasa a ser el contenido principal; el mapa se subordina
   (.mapbox--sub, más chico que el .mapbox base de la ronda 29). Donde el brochure
   declara minutos, el número sube de escala -- mismo criterio "el dato manda el
   tamaño" de la ronda 28 -- donde no, manda el nombre. Cero distancia calculada. */
/* 2 columnas lado a lado dejaban a la más corta (vialidades, 2-7 ítems) estirada al
   alto de la más larga (puntos, hasta 12) -- esa "mitad corta" es la misma trampa que
   la partición degradada (ronda 29c): la caja mide el alto de la más alta y la más
   corta aporta tinta solo en su propio tramo de arriba. Apilado: cada bloque ocupa
   exactamente su propio alto, sin complemento vacío. Tamaños: SOLO los 7 tokens ya
   existentes (--t-h2/--t-hero) -- un calc() con multiplicador propio agrega un
   escalón nuevo por cada valor distinto y rompió gate_barra (escalones>8, apretados
   en 40/41/42px) en la primera pasada. El llenado se ajusta con densidad de layout,
   nunca con un tamaño de fuente inventado. */
.entorno{display:flex;flex-direction:column;gap:clamp(8px,1vw,12px);
  margin-top:clamp(8px,1vw,12px);max-width:760px}
/* el empaquetado de flex-wrap contra el ancho de columna es no-monótono (un ítem que
   cruza a la fila siguiente cambia el conteo de filas) -- quadrante/kartesia miden
   mejor en una columna más angosta; solo geometría, cero tamaño de fuente nuevo. */
/* D2 · RONDA 40: los cuatro topes por ficha (410/440/380/420) existían para
   optimizar el empaquetado de flex-wrap A 40px CON nowrap — geometría al servicio
   de una escala que ya no está. A 18px el índice cabe en menos renglones cuanto
   más ancha sea la caja, así que los topes por ficha estorban y se retiran. */
.entorno .eyebrow{margin-bottom:6px}
/* una calle por fila completa dejaba ~65-80% de la fila vacía (medido: "Calz. Central"
   de 672px de fila con 139px de tinta). Fluye como texto en vez de una fila por calle.
   nowrap: envolver a 2 líneas en columna angosta inflaba el alto con puro line-height
   sin sumar tinta (140px de caja por 2 líneas de "Av. Circunvalación..."). */
.entorno .lista-p{margin-top:0;display:flex;flex-wrap:wrap;gap:6px 8px;row-gap:8px}
/* D2 · RONDA 40. Estaban a var(--t-h2) = 40px a 1440, con el h2 de la sección a
   67.84px: los nombres de calle pesaban el 59% del titular y se leían como una
   lista enorme, no como el dato de apoyo que son. Bajan a --t-cuerpo (18px, dentro
   del gate 15-20) y se componen como ÍNDICE: gris de marca, versalitas con
   tracking y los separadores de punto medio que ya existían. No se inventa un
   escalón tipográfico nuevo — --t-cuerpo es uno de los 7 tokens ya vigentes, que
   es la regla que rompió gate_barra en su momento. */
.entorno .lista-p li{padding:0;font-size:var(--t-cuerpo);white-space:nowrap;border-top:0;
  display:flex;align-items:baseline;gap:8px;color:var(--niebla-papel);
  text-transform:uppercase;letter-spacing:.08em}
.entorno .lista-p li::before{content:"";display:none}
/* D2 · RONDA 40: el separador iba ANTES de cada ítem (order:-1), así que al
   envolver la fila el renglón nuevo arrancaba con un "·" colgando —visible en
   quadrante a 1440, "· AV. INGLATERRA". Ahora cuelga del ítem anterior y va al
   final: si hay corte, el punto se queda cerrando el renglón de arriba, que es
   como cierra un índice, y ningún renglón puede empezar con un separador. */
.entorno .lista-p li:not(:last-child)::after{content:"·";color:var(--niebla-papel);
  margin-left:7px}
.puntos-lista{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}
.puntos-lista li{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;
  border-top:1px solid var(--linea);padding-top:6px}
.puntos-lista li:first-child{border-top:0;padding-top:0}
.puntos-lista b{font-family:var(--display);font-weight:400;color:var(--navy);
  font-size:var(--t-hero);line-height:1;white-space:nowrap}
/* RONDA 35 M4 — a 40px "Glorieta Niños Héroes" partía en dos renglones dejando
   "Héroes" solo (5 casos en central-escorza, 1 en simona). balance reparte los
   renglones sin tocar la escala ni el copy — que es como pide arreglarse una viuda,
   no encogiendo la tipografía. */
/* El tracking cierra el caso de "Avenida Chapultepec": 382px de tinta en una caja de
   380 — 2px de déficit, o sea el ítem entero cabía en un renglón por dos píxeles.
   -.01em recupera ~8px sin tocar la escala (piso de web-punch: nunca más apretado
   que -.04em). Es control de MEDIDA, no encoger la tipografía para tapar la viuda. */
.puntos-lista span{color:var(--niebla-papel);font-size:var(--t-h2);text-wrap:balance;letter-spacing:-.01em}
/* D2 · RONDA 40 — los "Puntos de interés" viven DENTRO de .entorno, la misma caja
   que mide el gate de renglones, y seguían a --t-h2 (40px a 1440): exactamente el
   mismo defecto que las vialidades, sólo que en la segunda columna del bloque.
   Reciben el mismo tratamiento de índice para que el bloque lea como una unidad y
   no como dos escalas peleándose. Fuera de .entorno, .puntos-lista no se toca. */
.entorno .puntos-lista span{font-size:var(--t-cuerpo);text-transform:uppercase;
  letter-spacing:.08em;text-wrap:initial}
/* BRIEF_45 · defecto 1 (RESPUESTA_KRILLIN_45, piso de G3 por rol): fuera de .entorno,
   .puntos-lista b sigue en --t-hero (68px/42px) -- ahí no hay defecto, es la lista de
   otro contexto. DENTRO de .entorno (madison, central-escorza: 6-10 traslados con
   tiempo) el mismo tratamiento gigante SÍ era el pico -- 7-10 renglones consecutivos a
   68px, ninguno funcional (AUDIT_FICHAS_2026-09-02 #1). Baja a cuerpo, igual que el
   span vecino (línea de arriba); el peso 600 conserva la distinción tiempo/lugar sin
   escalón tipográfico nuevo. */
.entorno .puntos-lista b{font-size:var(--t-cuerpo);font-weight:600;white-space:nowrap}
.entorno .puntos-lista li{white-space:nowrap}
.entorno .puntos-lista:not(:has(li > b)) li:not(:last-child)::after{content:"·";
  color:var(--niebla-papel);margin-left:7px}
.entorno .puntos-lista:not(:has(li > b)) li:not(:first-child)::before{content:none}
/* kartesia/montevideo/simona: puntos SIN tiempo (el brochure no lo declara) --
   apilados a fila completa, cada nombre corto dejaba ~70% de su propia fila vacía
   (medido: "Bosque Los Colomos" de 502px de ancho de fila ocupaba ~200px de tinta).
   Sin dato numérico que darles más peso, la solución es densidad: fluyen como texto
   en vez de una fila por nombre. */
.puntos-lista:not(:has(li > b)){flex-direction:row;flex-wrap:wrap;gap:6px 8px;row-gap:8px}
.puntos-lista:not(:has(li > b)) li{border-top:0;padding-top:0;gap:8px;max-width:100%}
.puntos-lista:not(:has(li > b)) li:not(:first-child)::before{content:"·";color:var(--niebla-papel)}
/* D3 · RONDA 40. Medido: 1112×200 = 5.56:1. Un embed de Google Maps a z=15
   dentro de esa rendija no muestra contexto — no se entiende dónde está el
   desarrollo, que es exactamente para lo que está el mapa. Se cuadra con
   aspect-ratio en vez de un alto fijo: 3:2 en escritorio (dentro del gate 1:1–16:10,
   sin pegarse al borde) y 9:10 en móvil (dentro de 1:1–4:5). La altura pasa a
   derivarse del ancho, así que el bloque se comporta igual en cualquier columna. */
.mapbox--sub{aspect-ratio:3/2;height:auto;margin-top:clamp(28px,4vw,40px)}
@media (max-width:600px){ .mapbox--sub{aspect-ratio:9/10} }
/* nowrap a t-h2 desborda vialidades largas ("Av. Circunvalación Agustín Yáñez") en
   viewports angostos -- medido overflow horizontal real en kartesia/simona a 390px. */
@media (max-width:600px){
  /* D2 · RONDA 40: a 390 el índice caía a 6-7 renglones (uno por calle: cada ítem
     ocupaba más de media caja de 350px, así que flex-wrap no empacaba ninguno).
     Un escalón menos de escala (--t-chico, token ya existente) y la mitad del
     tracking meten dos calles por renglón sin cambiar el tratamiento: sigue siendo
     el mismo índice en versalitas grises, mismo markup, no un diseño aparte. */
  /* nowrap vuelve: se puso `normal` cuando el índice iba a 40px y desbordaba a 390.
     A --t-chico la vialidad más larga mide 277px en una caja de 350, así que ya no
     desborda — y nowrap es lo que mantiene ítem y separador en el mismo renglón
     (con `normal` el "·" se despegaba y abría el renglón siguiente). */
  .entorno .lista-p li,
  .entorno .puntos-lista li{white-space:nowrap;font-size:var(--t-chico);letter-spacing:.02em}
  .entorno .puntos-lista span{font-size:var(--t-chico);letter-spacing:.02em}
  .entorno{gap:2px;margin-top:2px}
  .entorno .eyebrow{margin-bottom:0}
  .entorno .lista-p{gap:0 7px;row-gap:0}
  .puntos-lista{gap:0}
  .puntos-lista li{padding-top:0}
  .puntos-lista:not(:has(li > b)){gap:0 8px;row-gap:0}
  /* D2 · RONDA 40: este recorte a 300px es el gemelo móvil de los topes por ficha
     de escritorio — se calibró para el empaquetado a 40px con nowrap. A 14px sólo
     cuesta renglones: la tinta de simona suma 1,275px, y en una caja de 300 el
     mínimo teórico son 5 filas contra las 4 del gate. Con los 350px completos de
     la columna el índice baja a 4. Se retira; la caja del texto ya la pone .env. */
}

.plano-nivel{margin-top:20px;border-top:1px solid var(--linea);padding-top:20px}
/* mismo truco de padding+margen negativo que migas/marca — el flex+min-height centraba el texto
   distinto a como caía en flujo normal y lo corrió 10.8px (detectado al aislar esta ronda con
   getBoundingClientRect antes/después). Con padding no se mueve un píxel (auditoría §tactil). */
.plano-nivel summary{cursor:pointer;font-size:var(--t-chico);color:var(--niebla-papel);list-style:none;
  padding:11px 0;margin:-11px 0}
.plano-nivel summary::-webkit-details-marker{display:none}
.plano-nivel summary::after{content:"→";margin-left:6px}
.plano-nivel[open] summary::after{content:"↓"}
.plano-nivel img{margin-top:14px;width:100%;border-radius:var(--r)}

/* "Ver las N configuraciones": <summary> nativo sin ningún estilo propio (triángulo del
   navegador). Zona táctil ≥44px con padding+margen negativo — se deja el display:list-item
   nativo intacto para no arriesgar el marcador (auditoría §tactil). */
.modelos-todas > summary{padding:11px 0;margin:-11px 0}

/* respaldo del desarrollador: cifras, sin caja (mismo lenguaje que .datos/.cifras) */
.respaldo__stats{display:flex;flex-wrap:wrap;gap:34px;margin-top:30px;padding-top:22px;border-top:1px solid var(--linea)}
.respaldo__stats div{min-width:140px}
.respaldo__stats .n{display:block;font-family:var(--display);font-weight:400;font-size:var(--t-h2);color:var(--navy)}
.respaldo__stats .l{font-size:var(--t-chico);color:var(--niebla-papel)}

/* ═══ BRIEF_46 · lightbox de fotos (galería + amenidades + modelos) ═══════════ */
/* .nav (z-index:60) y .scta (z-index:58) pintan por encima del top-layer del
   <dialog> en este motor -- confirmado en modo headed (no es la captura), con
   elementFromPoint probando que el diálogo sí recibe el clic igual: es un defecto
   de PINTADO, no de interacción. Se ocultan mientras el visor está abierto en vez
   de perseguir la causa exacta del stacking. */
.lb-open .nav,.lb-open .scta{visibility:hidden}
/* Disparadores: cursor y foco visible en cualquier elemento con data-lb, sea <img>
   o el .ph de una planta (que ya trae su propio bg vía background-image). */
[data-lb]{cursor:pointer}
[data-lb]:focus-visible{outline:3px solid var(--blanco);outline-offset:3px}

/* el fondo vive en el propio <dialog>, no solo en ::backdrop: el diálogo es el
   elemento que `elementFromPoint` confirma cubriendo el viewport completo siempre;
   ::backdrop probó pintar por debajo de .nav/.scta (position:fixed) en algunos
   encuadres pese a ser top-layer -- el fondo opaco en el propio elemento no
   depende de esa garantía. */
dialog#lb{--lb-mh:76vh;border:0;padding:0;margin:auto;background:rgba(14,27,47,.97);max-width:100vw;max-height:100vh;
  width:100vw;height:100vh;color:var(--blanco)}
dialog#lb::backdrop{background:rgba(14,27,47,.94)}
dialog#lb[open]{display:flex;align-items:center;justify-content:center}
dialog#lb figure{margin:0;display:flex;flex-direction:column;align-items:center;gap:14px;
  max-width:92vw;max-height:86vh}
dialog#lb img{display:block;max-width:100%;max-height:76vh;width:auto;height:auto;
  object-fit:contain;border-radius:var(--r);background:rgba(255,255,255,.03)}
dialog#lb figcaption{font-size:var(--t-chico);color:var(--blanco);text-align:center;max-width:70ch}
.lb__contador{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);
  font-size:var(--t-micro);letter-spacing:.08em;color:var(--niebla);margin:0}
.lb__cerrar,.lb__prev,.lb__next{position:fixed;background:rgba(15,27,47,.55);color:var(--blanco);
  border:1px solid rgba(255,255,255,.2);border-radius:999px;width:48px;height:48px;
  display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;
  cursor:pointer;transition:background .2s ease,border-color .2s ease}
.lb__cerrar:hover,.lb__prev:hover,.lb__next:hover{background:rgba(15,27,47,.85);border-color:rgba(255,255,255,.4)}
.lb__cerrar{top:20px;right:20px}
.lb__prev{top:50%;left:20px;transform:translateY(-50%)}
.lb__next{top:50%;right:20px;transform:translateY(-50%)}
.lb__prev[hidden],.lb__next[hidden]{display:none}
@media (max-width:600px){
  .lb__prev{left:8px} .lb__next{right:8px}
  .lb__cerrar{top:12px;right:12px;width:42px;height:42px}
  dialog#lb figure{max-width:96vw;max-height:80vh}
  dialog#lb{--lb-mh:66vh}
  dialog#lb img{max-height:66vh}
}
/* la transición de apertura SOLO existe sin prefers-reduced-motion -- desaparece,
   no se acelera (piso de verification-harness §2a). */
@media (prefers-reduced-motion:no-preference){
  dialog#lb{opacity:0;transform:scale(.97);transition:opacity .2s ease,transform .2s ease}
  dialog#lb.lb--in{opacity:1;transform:scale(1)}
  dialog#lb::backdrop{opacity:0;transition:opacity .2s ease}
  dialog#lb.lb--in::backdrop{opacity:1}
}

/* ═══ BRIEF_48 · capa de micro-motion, animation-timeline nativo ═════════════
   Soporte (MDN, leído 3-sep-2026): Chrome/Edge 115+, Safari 26+ (iOS espeja),
   Chrome Android/Samsung Internet espejan Chrome. Firefox: preview, todavía no.
   Todo bajo @supports -- sin soporte, el estado final (visible, sin recorte) es
   el default: fail-open, nunca una página rota. prefers-reduced-motion:reduce
   apaga el bloque entero (regla dura del spec, §2.3): la página queda como hoy. */

/* ═══ RONDA 56 (Fable, 2026-09-04, PLAN_R56_RITMO_SECCIONES) · disparo sin JS ═══
   Referencia medida (MECANISMO_SECCIONES_MEDIDO_2026-09-04.md): post-hero, findrealestate
   NO scrubbea texto ni fotos; dispara tweens de tiempo fijo (1.83 s texto / 2.0 s foto,
   quart-out) al cruzar una línea del viewport. Reproducido sin librería: una animación de
   1 ms sobre un ViewTimeline salta --on de 0 a 1 en el elemento; los HIJOS heredan el
   cambio y su `transition` corre con reloj, no con scroll. Spike: qa/r56/spike_trigger.*
   Trampa medida: animación y transición en el MISMO elemento no arrancan la transición;
   la transición va siempre en un hijo (::after o img). */
/* FIX R56 durante el apply (Opus, 2026-09-04, gate G4b en rojo): con `linear` sobre un
   rango de 1% (~10 px) el progreso se interpola, y si el scroll se DETIENE dentro de esa
   franja --on queda fraccionario y la transición se congela a media asta -- medido:
   madison@390 fig[1] terminó en inset(0 85.4% 0 0) y hof@390 en 90.2%, o sea la foto
   tapada. Con steps(1,jump-start) --on sólo vale 0 o 1: escalón limpio al entrar, nunca
   un estado intermedio. Verificado tras el fix en las 10 combinaciones. */
@property --on{syntax:'<number>';inherits:true;initial-value:0}
@supports (animation-timeline: view()) {

  /* Los tres mecanismos SUSTITUYEN el fade de 8px de .rev como entrada de sus
     propios elementos (spec M2/M3, "sustituye"): la sección ya no espera al
     IntersectionObserver para hacerse visible -- cada foto/card lleva su propio
     progreso atado a SU paso por el viewport. :has() vive en el mismo set de
     navegadores que animation-timeline, no suma un piso de compatibilidad nuevo. */
  .js .rev:has(> .galeria),
  .js .rev:has(.galeria--amen),
  .js .rev:has(.modelos),
  .js .rev:has(.amen){opacity:1;transform:none;transition:none}

  /* ── M1 · titular por letras (codrops/OnScrollTypographyAnimations, fx2,
     src/js/index.js:95-115, MIT). UN pico por página: el h2 de la 1ª galería
     (decisión Krillin 2026-09-03 -- la costura A1 hero→galería de la ronda 45).
     Números de la fuente: yPercent 120→0, scaleY 2.3→1, scaleX 0.7→1,
     transformOrigin 50% 0%, stagger 0.03s sobre duration:1s (= 3% de la duración
     por carácter). ease `back.inOut(2)` no existe en CSS nativo: se aproxima con
     el cubic-bezier publicado de "easeInOutBack" en easings.net (0.68,-0.6,0.32,1.6,
     overshoot constante 1.70158) -- el (2) de GSAP pide más rebote del que ese
     cubic-bezier da; se declara la diferencia, no se finge igualdad.

     RONDA 49 -- el 3%/carácter original (48% de spread total para 16-17 letras)
     estirado sobre `calc(0%+i*3%) calc(70%+i*3%)` hacía que las últimas letras
     (var(--i) alto) terminaran su recorrido en cover ~118%, MUCHO después de que
     el propio h2 (42px de alto, minúsculo contra el viewport) ya había salido de
     pantalla (medido: el h2 deja de ser visible en cover ~100%, y la última letra
     seguía resolviendo hasta 118% -- una animación que termina invisible). Bajado
     a 1.5%/carácter y ventana de 55% (antes 70%), con arranque en 5% (no 0%, para
     no empezar la primera letra con el h2 apenas asomando): la última letra ahora
     termina en cover ~84%, adentro de la ventana visible. Verificado con traza fina
     en madison y quadrante -- ver RESULTADO_48.md § Ronda 49. */
  /* RONDA 54 (Fable, 2026-09-03, RESULTADO_54.md) -- tres cambios medidos:
     (a) .pal = palabra en inline-block. Chromium permite quebrar línea ENTRE dos
         inline-block contiguos aunque no haya espacio: con los .ch sueltos, HOF
         partía "sus" en 112/113 anchos y Madison "edificio" en 113/113. Ahora el
         único punto de quiebre es el espacio entre palabras.
     (b) El cascade cierra con el título al 40% del viewport, como la fuente
         (codrops fx2, src/js/index.js: start 'center bottom+=50%', end 'bottom
         top+=40%', duration 1, stagger 0.03, scrub). En unidades `cover` eso es
         64% (top del h2 ≈ 250px a 1440 y a 390, medido). Ventana por letra
         W = 64% / (1 + 0.03·(n−1)) y stagger d = 0.03·W, con --n emitido por el
         generador: el fin NO depende del largo del título. Antes (1.5%/letra
         fijo), los títulos de 29-34 glifos de la ronda 52 terminaban en cover
         102-108% -- la última letra resolvía con el h2 fuera de pantalla (top
         −44/−103 px a 1440), el mismo defecto de la ronda 49. Arranque en 0% y
         no en −40% como la fuente: con un h2 de 85px, las primeras 7 letras
         reventarían antes de entrar al viewport.
     (c) La opacidad va en animación propia con curva suave (.45,0,.55,1). El
         back-inOut de la fuente se sale de [0,1] y el navegador recorta: el fade
         quedaba concentrado en ~18% de la ventana (núcleo .1→.9 = 95-105px de
         scroll = 0.16 s a 600 px/s). Con la curva suave el núcleo mide 155-215px
         (0.26-0.36 s a 600 px/s) y el transform conserva el rebote de la fuente
         intacto. Desviación declarada, no fidelidad ciega: es la lectura de
         "un poquito más de tiempo en pantalla" de Jara, medida. */
  .titulo-pico{--w:calc(64% / (1 + 0.03 * (var(--n, 20) - 1)));--d:calc(0.03 * var(--w))}
  .titulo-pico .pal{display:inline-block}
  /* RONDA 56 -- M1 retirado (PLAN_R56_RITMO_SECCIONES I2). Medido 2026-09-04 contra
     findrealestate: su título mueve 0 px y no escala (sólo un velo de contraste, 1.83 s);
     el nuestro movía translateY 121% de su alto, scaleY 2.3→1, scaleX .7→1 con anticipación
     -10.5% y rebote +10.3%, en 310-390 px de scroll (0.26 s en un flick). Jara, 3-sep:
     "llega haciendo una curva… podemos hacer cosas más elegantes". Los <span class="ch">
     se quedan en el HTML (los emite construir_fichas.py); sin animación son inocuos.
     El velo va en I3 sobre TODOS los h2, incluido éste. */
  .titulo-pico .ch{display:inline-block}
  @keyframes m48-char-t{
    from{transform:translateY(120%) scaleY(2.3) scaleX(.7)}
    to{transform:none}
  }
  @keyframes m48-char-o{from{opacity:0}to{opacity:1}}

  /* ── M2 · foto revelada por clip-path (codrops/TextClipScroll, js/index.js:44-56,
     MIT). Números de la fuente: start 'top bottom' → end 'bottom top', ease NONE,
     scrub:true -- progreso LINEAL atado al recorrido completo del elemento por el
     viewport. Ese tramo completo es exactamente lo que `animation-timeline:view()`
     cubría por default (`cover 0% → cover 100%`) -- Sustituye al fade de 8px en
     galería (los 3 bloques de la ronda 45B/45C, mismo data-lb) y en el mosaico de
     fotos de amenidades -- ambos son fotos reales (decisión Krillin: M2 = cualquier
     FOTO, sin importar la sección).

     RONDA 49 -- fidelidad estricta a la fuente (cover 0%→100%, el recorrido
     COMPLETO) resultó en un reveal demasiado lento para el uso real: medido en
     madison (figure de 556px) y quadrante (411px), al momento en que la figura
     está 100% contenida en el viewport por primera vez, el reveal apenas iba en
     ~39% (madison) / ~36% (quadrante) -- todavía muy oculta cuando ya se ve
     completa. Y seguía resolviendo mientras la figura SALÍA por arriba, así que en
     scroll normal (no el de 25px del trace) buena parte del reveal ocurre fuera de
     una lectura cómoda. Decisión de UX sobre la aplicación, no fidelidad ciega al
     número de la fuente (autorizado explícitamente por el brief de la ronda 49):
     se acorta a `cover 5% cover 45%`, concentrando el reveal en la franja de
     entrada+primera-visión-completa en vez de estirarlo hasta la salida. Sigue
     siendo progreso lineal continuo atado a scroll -- se acorta la VENTANA, no se
     toca `ease:none`. Verificado con traza fina, ver RESULTADO_48.md § Ronda 49.

     BUG REAL cazado construyendo, no en el spec: la animación va en `figure`, NO en
     `img`. Medido con getAnimations()[0].currentTime en scroll real (mouse.wheel,
     no solo scrollTo): con la animación en `.galeria img`, `currentTime` se queda
     CLAVADO en 50% sin importar cuánto se scrollee -- `overflow:hidden` en un
     ANCESTRO (el propio `figure`) rompe el cálculo de `animation-timeline:view()`
     del descendiente en este Chromium (148.0.7778.96). Con la misma animación
     aplicada al `figure` (que tiene overflow:hidden sobre SÍ MISMO, no como
     ancestro de otra cosa) el progreso corre limpio, verificado -1.9%→53% en el
     mismo barrido. El resultado visual es idéntico -- el figure ya recorta a sus
     bordes y el img lo llena por `object-fit`, así que animar el clip-path del
     figure se ve igual que animarlo en el img. */
  /* RONDA 52 -- Jara pidió "estacionar" la sección (scroll-lock real): eso rompe
     G2 de Ronda 48 (0 bytes de librería, cero JS -- Lenis+ScrollTrigger.pin ya se
     evaluó y descartó por peso, 114KB contra 49-52KB de la ficha entera). Opción
     sin JS aplicada en su lugar (autorizada por el propio brief si cumple el
     criterio numérico: 600-800px de scroll real cubiertos por el tramo VISIBLE
     del recorrido). Medido ANTES: madison (figure 556px) resolvía 5%→98% en
     SOLO 550px de scroll real (mouse.wheel, no el trace de 25px) -- por debajo
     del piso. Se ensancha la ventana de `cover 5% cover 45%` (40% del cover
     total) a `cover 5% cover 60%` (55%): en madison da ~750px verificado, sigue
     terminando con la figura ya adentro del viewport (no se corre el final más
     allá de donde Ronda 49 lo puso, solo se abre el inicio). Se prefirió esto
     sobre "alargar el contenedor" (la otra opción del brief): mismo resultado
     medible sin agregar whitespace nuevo al layout. Ver RESULTADO_52.md. */
  /* RONDA 56 -- M2 pasa de scroll-scrub lineal (610-760 px = 1.0-1.3 s @600, 0.5-0.6 s
     en un flick) a tiempo fijo (PLAN_R56 I4). Referencia medida (§4-B): clip HORIZONTAL
     inset 100%→0 en 1.97-2.01 s, quart-out (RMSE 0.005-0.009), disparado cuando el top
     entra al viewport (99-100% del vh); en cada pareja las dos fotos van en direcciones
     opuestas. Resuelve "estacionar" (R52) sin scroll-lock ni JS: la animación corre
     completa aunque el usuario siga bajando. El figure sólo DISPARA (--on); el clip vive
     en el img porque animación y transición en el mismo elemento no arrancan (spike). */
  .galeria figure,.galeria--amen figure{
    animation:r56-on 1ms steps(1,jump-start) both;
    animation-timeline:view();
    animation-range:entry 0% entry 1%;
  }
  .galeria figure>img,.galeria--amen figure>img{
    clip-path:inset(0 0 0 calc((1 - var(--on)) * 100%));
    transition:transform .8s var(--ease-out),clip-path 2s var(--ease-out);
  }
  .galeria figure:nth-child(even)>img,.galeria--amen figure:nth-child(even)>img{
    clip-path:inset(0 calc((1 - var(--on)) * 100%) 0 0);
  }
  /* Móvil (<=760px): geometría distinta (figure más baja, ~220-260px contra
     556/411px en desktop -- .galeria pierde el span de 4 columnas del
     first-child). El mismo `cover 3% cover 55%` cae en 14-18% de progreso en
     el punto de 50%-visible (bajo la banda 20-80% de G5) -- verificado en
     madison y quadrante, no solo desktop. `cover 0% cover 25%` centra ambos
     en 42-49%. La distancia de 600-800px del criterio de arriba es una
     referencia de mouse/trackpad (redactada así en el brief); a scroll
     táctil no aplica igual y no se persigue aquí -- lo que manda es G5. */
  @media (max-width:760px){
    /* RONDA 54 -- medido a 390 (RESULTADO_54.md): con 25% el tramo visible de M2 era
       230-250px de scroll = 0.19-0.21 s a 1200 px/s (flick táctil), bajo el umbral
       de 300 ms del brief. 33% lo lleva a ~330px (0.28 s) sin sacar la figura chica
       (128px) de la banda 20-80% de G5 -- 40% la dejaba en 17%, fuera. */
    /* R56: sin override -- el disparo por entrada (entry 0%) es el mismo a 390 (ref: 834/844 = 99%). */
  }

  /* ── M3 · cards con contraste de escala, no stagger parejo (codrops/ConnectedGrid,
     js/index.js:49-69, MIT). Números de la fuente: ease 'power4' (GSAP sin sufijo =
     .out; aproximado con el cubic-bezier publicado de "easeOutQuart" en easings.net,
     0.25,1,0.5,1), ventana 'top bottom-=15%' → '+=100%' (arranca un poco ANTES de
     que el ítem entre del todo). La mecánica real es el CONTRASTE: unos ítems
     escalan 0→1, otros 5→1, alternados por posición -- no el valor de escala en sí.
     Sustituye el fade de 8px en la rejilla de modelos (planta = card sin foto que
     revelar, decisión Krillin) y en la lista de texto de amenidades (mismo
     criterio: sin imagen, es card).

     RONDA 49 -- `cover 0% cover 45%` (el tramo de entrada, fiel a la fuente) hacía
     que la card TERMINARA de escalar mientras seguía mayormente fuera de cuadro:
     medido en madison (.modelo, 291px), al primer instante 100% visible (cover
     ~24%) el progreso ya iba en 96%. Se corre la ventana a `cover 8% cover 55%`:
     con eso, al punto de 50% de altura visible el progreso queda ~30-40% (medio,
     ni arranque ni asentado) y el escalado termina de resolver ya con la card
     cómodamente adentro del viewport, no en el borde. Ajuste de UX sobre la
     aplicación (autorizado por el brief de la ronda 49), no la fracción original
     de la fuente -- declarado, no escondido. Verificado con traza fina en madison
     y quadrante, ver RESULTADO_48.md § Ronda 49. */
  /* RONDA 52 -- mismo criterio que M2 arriba: medido en madison (.modelo,
     291px), resolvía 2%→99% en solo 375px de scroll real, el más corto de los
     tres. Se ensancha `cover 8% cover 55%` (47%) a `cover 3% cover 95%` (92%). */
  /* RONDA 53 -- el mismo bug de amplitud extrema de Ronda 51 (HOME) vivía aquí:
     scale(0)→1 / scale(5)→1 con origin en esquina. Ronda 52 ensanchó el
     animation-range (arriba) sin tocar la amplitud -- efecto colateral: la
     card pasa MÁS scroll en sus extremos 0×/5×, no menos. Mismo fix que
     Ronda 51 en HOME (RESULTADO_51.md), banda un poco más generosa porque
     esta card es más chica (291px contra los ~611px de .carta en HOME) y el
     contraste necesita notarse más para no leerse como nada: 0.8↔1 / 1.2↔1
     en vez de 0.85↔1 / 1.15↔1. El `animation-range` de Ronda 52 NO se toca
     (ya está bien, es lo que arregló el "se resuelve muy rápido"). Ver
     RESULTADO_53.md -- incluye una nota sobre una discrepancia real entre
     getComputedStyle()/getBoundingClientRect() (siguen reportando valores
     grandes en ciertos frames) y lo que el compositor efectivamente pinta
     (capturas: normal, sin desborde) -- no se pudo reproducir el desborde
     visual en captura automatizada pese a confirmar el swing extremo por
     número; el fix se aplica igual porque es correcto con independencia de
     esa duda, y dentro de la nueva banda 0.8-1.2 el propio swing extremo
     deja de ser posible. */
  /* RONDA 56 -- M3 pasa de contraste de escala scrubbed a subida+fade de tiempo fijo
     (PLAN_R56 I7, OK de Jara 2026-09-04). Referencia medida (§4-C): el ítem sube 70 px y
     entra de opacity 0 a 1 en 1.97-1.98 s, expo-out var(--ease-out) (RMSE 0.011),
     disparado con el top al 91-92% del vh.
     DOS DESVIACIONES DEL PLAN, ambas por medición, declaradas:
     (a) El plan dice "57% del alto". Ese % sale de un ítem de 123 px; la referencia mueve
         **70 px constantes** (a 390 el mismo ítem mide 169-196 px y sigue moviendo 70 px,
         = 36-41%). En una .modelo de 291 px, 57% serían 166 px: la amplitud excesiva que
         R51/R53 ya corrigieron. Se porta el invariante, 70 px.
     (b) El disparo NO puede vivir en el elemento animado: `.amen li` es `<li>Texto</li>`
         sin hijo donde colgar la transición, y animación+transición en el mismo elemento
         no arrancan (spike qa/r56/spike_trigger). El disparo se pone en el CONTENEDOR
         (.modelos / .amen), que ya existe en el HTML; los ítems de un mismo contenedor
         entran juntos, como ya entraban (stagger medido de M3 a 1440: 0 px). */
  .modelos,.amen{
    animation:r56-on 1ms steps(1,jump-start) both;
    animation-timeline:view(block 0 8%);
    animation-range:entry 0% entry 1%;
  }
  .modelo,.amen li{
    transform:translateY(calc((1 - var(--on)) * 70px));opacity:var(--on);
    transition:transform 1.97s var(--ease-out),opacity 1.97s var(--ease-out);
  }

  /* ── R56 · velo de contraste sobre títulos (findrealestate, medido 2026-09-04,
     MECANISMO_SECCIONES_MEDIDO §4-A): la línea de texto no se mueve; un velo del color
     del fondo al 72% se retira de izquierda a derecha en 1.83 s, quart-out
     (1-(1-t)^4 = var(--ease-out), RMSE 0.017-0.027 sobre 12 líneas), disparado
     cuando el TOP del título cruza el 78% del alto del viewport (697-706 px de 900;
     644-650 de 844). Bajo el velo el texto se lee al ~28% de su tinta: nunca invisible.
     view(block 0 22%) = la línea del 78%, verificado en el spike (77.8%).
     width:fit-content: el velo tiene que medir lo que mide el texto -- sin esto, un h2
     de 1112 px con 284 px de texto destaparía el texto en 130 ms de los 1830.
     Diferencia declarada: la referencia hace un velo POR LÍNEA con 60 ms de stagger y
     es one-shot; aquí es un velo por h2 (1-3 líneas) y se vuelve a cerrar al subir. */
  .seccion h2{
    position:relative;width:fit-content;
    animation:r56-on 1ms steps(1,jump-start) both;
    animation-timeline:view(block 0 22%);
    animation-range:entry 0% entry 1%;
  }
  .seccion h2::after{
    content:"";position:absolute;inset:10% -1% -10% 0;pointer-events:none;
    background:rgba(242,238,231,.72); /* --papel #F2EEE7 al 72% */
    transform:scaleX(calc(1 - var(--on)));transform-origin:100% 50%;
    transition:transform 1.83s var(--ease-out);
  }
  .seccion-osc h2::after{background:rgba(14,27,47,.72)}      /* --tinta #0E1B2F */
  .seccion-papel-2 h2::after{background:rgba(212,228,234,.72)} /* --papel-2 #D4E4EA */
  @keyframes r56-on{from{--on:0}to{--on:1}}

  /* ── R56-bis · wordspacing sobre "El proyecto" (BRIEF_PROYECTO_WORDSPACING_
     MONTEVIDEO_2026-09-13, [JARA]): mecánica fuente
     `[data-circle-text]{word-spacing:'0rem'→'10rem',scrub:true}` --
     era_slater_60900_2026-09-08.js L3432-3450, MECANISMO_MEDIDO_2026-09-08.md:101.
     Aquí scrub→disparo único (mismo criterio que R56 en M2/M3): el CONTENEDOR
     (`.dosc.plano-ed`, SOLO montevideo -- construir_fichas.py L1973) dispara
     --on con la animación de 1ms; la `transition` de word-spacing va en los
     HIJOS (eyebrow + notas), nunca en el mismo elemento que anima --on (spike
     R56: animación+transición en un elemento no arrancan). --on hereda
     (`@property --on{inherits:true}`, línea 2119) de un div a otro sin que el
     contenedor intermedio (`.dosc.plano-ed > div`) necesite su propia regla.
     Duración/curva: 1.2s cubic-bezier(.25,1,.5,1) = "Out" medida para la
     entrada de ERA (misma tabla, ~L60-71) -- mismo criterio R56 de portar
     tiempo fijo con la curva de la fuente, no la del scrub original.
     X: ratio 10rem/3.9375rem (font-size del `.h4` circular a 1440,
     `--_fonts---h4--size:calc(63rem/16)`, era_webflow_2026-09-08.min.css) =
     2.54 aplicado a --t-micro (eyebrow, .6875rem) y --t-chico (nota, .875rem)
     da 1.75rem/2.22rem. Medido con gate_ws.mjs (getClientRects por palabra,
     360/390/412/430/1440): ESE valor pasa el gate de renglones en los 5
     anchos -- la ratio no rompe líneas. Se baja igual, por criterio visual
     (screenshot en los 3 candidatos, qa/r_wordspacing/): 2.22rem entre CADA
     palabra de una nota de 8-9 palabras (no un titular circular de 2-3) se ve
     roto, no "separado" -- gaps de ~35px que leen como error de render, no
     mecanismo. Bajado a 0.9rem (eyebrow, 1 solo hueco en "El proyecto") /
     0.7rem (nota) -- visible y legible, mismo gate limpio en los 5 anchos
     (verificado de nuevo tras bajar). Desviación declarada del [CRITERIO]
     "aplícalo tal cual", no del [MEDIDO] -- ver
     RESULTADO_WORDSPACING_MONTEVIDEO_2026-09-13.md. */
  .dosc.plano-ed{
    animation:r56-on 1ms steps(1,jump-start) both;
    animation-timeline:view();
    animation-range:entry 0% entry 1%;
  }
  .dosc.plano-ed .eyebrow{
    word-spacing:calc(var(--on) * 0.9rem);
    transition:word-spacing 1.2s cubic-bezier(.25,1,.5,1);
  }
  .dosc.plano-ed .ficha-tec__nota{
    word-spacing:calc(var(--on) * 0.7rem);
    transition:word-spacing 1.2s cubic-bezier(.25,1,.5,1);
  }

  /* ═══ SYS-ERA M-B/M-C (spec §C2 tabla, §D4/§F.6) — [data-sys="era"] únicamente,
     F1 = quadrante. Generaliza el MISMO patrón de arriba (R56-bis) de 1 aparición
     ("El proyecto", montevideo) a las 8-10 secciones de la ficha: gramática, no
     excepción única (gate de concepto awwwards-bar Q6, spec §C cierre).
     M-B: hairline .75px al 100% de tinta que se dibuja izq→der al abrir cada
     sección (B4: hoy 48 líneas al 16% de alfa, 0 dibujadas -- la costura visible
     que no existe). M-C: mismo word-spacing 0→.9rem del eyebrow, ya aceptado.
     Ambos leen --on de SU PROPIO trigger en `.env` (no el de `.seccion h2`, que
     dispara distinto: view(block 0 22%) vs view() a secas -- no se reutiliza el
     de h2 porque el eyebrow no es descendiente de h2, es su hermano). Fail-open
     sin soporte: como en R56, toda la regla vive dentro de este @supports -- un
     navegador sin animation-timeline nunca genera el ::before ni aplica el
     word-spacing, así que el eyebrow y el texto quedan en su estado normal
     (visible, sin recorte), nunca a medio dibujar. */
  [data-sys="era"] .seccion > .env{
    animation:r56-on 1ms steps(1,jump-start) both;
    animation-timeline:view();
    animation-range:entry 0% entry 1%;
  }
  [data-sys="era"] .seccion > .env::before{
    content:"";display:block;height:var(--hair);margin-bottom:clamp(20px,2.6vw,32px);
    background:var(--tinta);clip-path:inset(0 calc((1 - var(--on)) * 100%) 0 0);
    transition:clip-path var(--dur-l) var(--sys-out);
  }
  [data-sys="era"] .seccion-osc > .env::before{background:var(--blanco)}
  /* Jara 14-sep (captura Escorza 1440): en un .env que ES grid (.dosc/.forma/.split)
     el ::before en flujo se volvía celda 1 de la retícula -- la raya ocupaba media
     columna izquierda y empujaba el primer bloque a la derecha y el segundo a la fila
     de abajo (8 de 8 fichas en desktop). Fuera de flujo: la raya se ancla arriba del
     contenido y el padding-top repone el mismo aire que daba en flujo. */
  [data-sys="era"] .seccion > .env:is(.dosc,.forma,.split){position:relative;
    padding-top:calc(var(--hair) + clamp(20px,2.6vw,32px))}
  [data-sys="era"] .seccion > .env:is(.dosc,.forma,.split)::before{
    position:absolute;top:0;left:var(--pad);right:var(--pad);margin:0}
  [data-sys="era"] .seccion .eyebrow{
    word-spacing:calc(var(--on) * 0.9rem);
    transition:word-spacing var(--dur-l) var(--sys-out);
  }
}

/* prefers-reduced-motion:reduce APAGA, no acelera (piso verification-harness §2a,
   spec §2.3): ninguno de los tres mecanismos corre, y como .rev ya vuelve a su
   propio estado final bajo reduced-motion (línea ~870, existente), el contenido
   queda visible y quieto -- nunca a medio recortar ni a medio escalar. */
@media (prefers-reduced-motion:reduce){
  .titulo-pico .ch,.galeria figure,.galeria--amen figure,.modelo,.amen li{
    animation:none !important;clip-path:none !important;transform:none !important;opacity:1 !important
  }
  /* R56: lo nuevo también APAGA -- --on:1 deja velo y clip en su estado final, sin transición. */
  .seccion h2,.galeria figure,.galeria--amen figure,.modelos,.amen{--on:1;animation:none !important}
  .modelo,.amen li{transition:none !important;transform:none !important;opacity:1 !important}
  .seccion h2::after{transition:none !important;transform:scaleX(0) !important}
  .galeria figure>img,.galeria--amen figure>img{transition:none !important;clip-path:none !important}
  /* R56-bis: wordspacing también APAGA -- --on:1 deja el estado final asentado, sin transición. */
  .dosc.plano-ed{--on:1;animation:none !important}
  .dosc.plano-ed .eyebrow,.dosc.plano-ed .ficha-tec__nota{transition:none !important}
  /* SYS-ERA M-B/M-C: mismo criterio -- --on:1 deja la línea dibujada y el
     word-spacing asentado, sin transición ni animación. */
  [data-sys="era"] .seccion > .env{--on:1;animation:none !important}
  [data-sys="era"] .seccion > .env::before{transition:none !important}
  [data-sys="era"] .seccion .eyebrow{transition:none !important}
}
/* BRIEF_FUSION_CINE_QUADRANTE_2026-09-13 · Tarea 1: separación del sub-bloque
   fundido (galería2+dif, mapa+amenidades) respecto al bloque anterior dentro del
   mismo .env. Mismos números que el patrón `b2` ya existente (construir_fichas.py,
   "El proyecto" con bloque2: margin-top:34px;padding-top:26px;border-top" para la
   variante sin fondo propio); la variante .dosc (particion) no lleva hairline -- su
   propio bloque de color (var(--navy)) ya es la separación visual. */
[data-sys="era"] .fusion-bloque{margin-top:34px}
[data-sys="era"] .fusion-bloque:not(.dosc){padding-top:26px;border-top:1px solid var(--linea)}
/* ============================================================
   RONDA 55 FASE 2 — mecanismos M-A/M-B/M-C, .fhero--r55 only.
   Curva unica p(u)=u(5-2u)/3, 21 paradas cada 5%. Ley de fades (1-t)^2.
   Fuente: BRIEF_RONDA_55_HERO_HOF.md §3, §4-A. animation-timeline:
   scroll(root block) — el documento, no 'nearest' (evita que .fhero--r55/
   .fhero__pin, ambos overflow:clip, se elijan como scroller por error).
   animation-range en vh == misma fraccion que la altura del contenedor
   (400vh/280vh), asi el % de cada keyframe = u exacto sin JS.
   ============================================================ */

@keyframes hofImgScrub{
0%{transform:scale(1.0000) translateY(0.000vh)}
5%{transform:scale(1.0235) translateY(-4.467vh)}
10%{transform:scale(1.0461) translateY(-8.752vh)}
15%{transform:scale(1.0677) translateY(-12.854vh)}
20%{transform:scale(1.0883) translateY(-16.775vh)}
25%{transform:scale(1.1080) translateY(-20.513vh)}
30%{transform:scale(1.1267) translateY(-24.068vh)}
35%{transform:scale(1.1445) translateY(-27.441vh)}
40%{transform:scale(1.1613) translateY(-30.632vh)}
45%{transform:scale(1.1771) translateY(-33.641vh)}
50%{transform:scale(1.1920) translateY(-36.467vh)}
55%{transform:scale(1.2059) translateY(-39.111vh)}
60%{transform:scale(1.2189) translateY(-41.572vh)}
65%{transform:scale(1.2309) translateY(-43.851vh)}
70%{transform:scale(1.2419) translateY(-45.948vh)}
75%{transform:scale(1.2520) translateY(-47.863vh)}
80%{transform:scale(1.2611) translateY(-49.595vh)}
85%{transform:scale(1.2693) translateY(-51.145vh)}
90%{transform:scale(1.2765) translateY(-52.512vh)}
95%{transform:scale(1.2827) translateY(-53.697vh)}
100%{transform:scale(1.2880) translateY(-54.700vh)}
}
@media (max-width:760px){
@keyframes hofImgScrub{
0%{transform:scale(1.0000) translateY(0.000vh)}
5%{transform:scale(1.0225) translateY(-1.225vh)}
10%{transform:scale(1.0441) translateY(-2.400vh)}
15%{transform:scale(1.0647) translateY(-3.525vh)}
20%{transform:scale(1.0845) translateY(-4.600vh)}
25%{transform:scale(1.1033) translateY(-5.625vh)}
30%{transform:scale(1.1212) translateY(-6.600vh)}
35%{transform:scale(1.1382) translateY(-7.525vh)}
40%{transform:scale(1.1543) translateY(-8.400vh)}
45%{transform:scale(1.1694) translateY(-9.225vh)}
50%{transform:scale(1.1837) translateY(-10.000vh)}
55%{transform:scale(1.1970) translateY(-10.725vh)}
60%{transform:scale(1.2094) translateY(-11.400vh)}
65%{transform:scale(1.2209) translateY(-12.025vh)}
70%{transform:scale(1.2314) translateY(-12.600vh)}
75%{transform:scale(1.2411) translateY(-13.125vh)}
80%{transform:scale(1.2498) translateY(-13.600vh)}
85%{transform:scale(1.2576) translateY(-14.025vh)}
90%{transform:scale(1.2645) translateY(-14.400vh)}
95%{transform:scale(1.2704) translateY(-14.725vh)}
100%{transform:scale(1.2755) translateY(-15.000vh)}
}
}

@keyframes hofEnvScale{
0%{transform:scale(1.0000)}
5%{transform:scale(0.9922)}
10%{transform:scale(0.9846)}
15%{transform:scale(0.9774)}
20%{transform:scale(0.9706)}
25%{transform:scale(0.9640)}
30%{transform:scale(0.9578)}
35%{transform:scale(0.9518)}
40%{transform:scale(0.9462)}
45%{transform:scale(0.9410)}
50%{transform:scale(0.9360)}
55%{transform:scale(0.9314)}
60%{transform:scale(0.9270)}
65%{transform:scale(0.9230)}
70%{transform:scale(0.9194)}
75%{transform:scale(0.9160)}
80%{transform:scale(0.9130)}
85%{transform:scale(0.9102)}
90%{transform:scale(0.9078)}
95%{transform:scale(0.9058)}
100%{transform:scale(0.9040)}
}

@keyframes hofImgFade{
0%{opacity:1}
37.50%{opacity:1.0000}
38.75%{opacity:0.8100}
40.00%{opacity:0.6400}
41.25%{opacity:0.4900}
42.50%{opacity:0.3600}
43.75%{opacity:0.2500}
45.00%{opacity:0.1600}
46.25%{opacity:0.0900}
47.50%{opacity:0.0400}
48.75%{opacity:0.0100}
50.00%{opacity:0.0000}
100%{opacity:0}
}

@keyframes hofTintaFade{
0%{opacity:0}
37.50%{opacity:0.0000}
38.75%{opacity:0.1900}
40.00%{opacity:0.3600}
41.25%{opacity:0.5100}
42.50%{opacity:0.6400}
43.75%{opacity:0.7500}
45.00%{opacity:0.8400}
46.25%{opacity:0.9100}
47.50%{opacity:0.9600}
48.75%{opacity:0.9900}
50.00%{opacity:1.0000}
100%{opacity:1}
}

@keyframes hofEnvFade{
0%{opacity:1}
2.50%{opacity:0.8100}
5.00%{opacity:0.6400}
7.50%{opacity:0.4900}
10.00%{opacity:0.3600}
12.50%{opacity:0.2500}
15.00%{opacity:0.1600}
17.50%{opacity:0.0900}
20.00%{opacity:0.0400}
22.50%{opacity:0.0100}
25.00%{opacity:0.0000}
100%{opacity:0}
}

@keyframes hofWordmarkFade{
0%{opacity:0}
10%{opacity:0}
15%{opacity:1}
35%{opacity:1}
37.50%{opacity:0.8100}
40.00%{opacity:0.6400}
42.50%{opacity:0.4900}
45.00%{opacity:0.3600}
47.50%{opacity:0.2500}
50.00%{opacity:0.1600}
52.50%{opacity:0.0900}
55.00%{opacity:0.0400}
57.50%{opacity:0.0100}
60.00%{opacity:0.0000}
100%{opacity:0}
}

/* --- Aplicación de los mecanismos, scopeada a .fhero--r55 --- */
/* Gate 9 — degradación: geometría/máscara viven siempre (inertes sin animar);
   TODO animation-name/timeline/range vive dentro de @supports. Sin soporte,
   wordmark/tinta quedan en opacity:0 (default de abajo) y el hero cae al
   estado estático de Fase 1 (foto quieta + copy quieto), nunca a medio gesto. */

/* M-C: geometría de la máscara — vive siempre, no depende de @supports.
   La máscara es site/assets/brand/hof-lockup.svg (paths, no <text>).

   .fhero__wordmark es una CAJA CHICA (el tamaño real del lockup en pantalla):
   sólido, no se mueve, sólo cambia opacidad.

   .fhero__tinta es FULL-BLEED, del mismo tamaño/posición que .fhero__img —
   la máscara ahí es una VENTANA pequeña (mask-size/mask-position, no
   mask-size:contain sobre una caja chica). Por qué: "hereda el mismo
   transform del render" (§3.3) exige que el <img> de dentro tenga EXACTAMENTE
   el mismo sistema de coordenadas que .fhero__img — scale()+translateY()
   componen en una sola matriz donde la traslación se multiplica por el
   escalado (matrix(s,0,0,s,0,s·ty)); aplicar esa traslación calibrada para
   una imagen a pantalla completa dentro de una caja chica la saca completa
   de la ventana (medido: caja vacía a u=0.60, cobertura 0% en vez de ~50%).
   Con .fhero__tinta a pantalla completa, el <img> de dentro se mueve en el
   MISMO marco que el de fondo — la ventana solo recorta qué parte se ve. */
.fhero--r55 .fhero__wordmark{
  position:absolute;z-index:1;left:50%;top:46%;
  /* R63: aspect-ratio y mask-image pasan a --fhero-mask-ratio / --fhero-mask,
     con el valor de hof (F/H 0.7339, asset hof-lockup.svg) de default — el
     resto de la ronda 55 (FASE6, RESULTADO_55.md §21) sigue intacta, esto solo
     desconecta el NÚMERO del bloque para que cada ficha meta el suyo, medido
     contra su propio asset, sin duplicar la regla.
     R64: el ancho de la caja pasa a --fhero-mask-w (default min(82vw,1180px),
     el valor de hoy) — una letra casi cuadrada (madison, ratio 1.572028) con
     el ancho de hof da 2.06× más alto y se sale del área pintada de la foto
     (BRIEF_R64_M_SIN_RESPALDO.md §1). Se sustituye en los tres sitios donde
     vivía el literal (wordmark width, tinta mask-size, tinta mask-position):
     wordmark y tinta deben medir lo mismo o la ventana se desalinea del
     sólido y el relevo se parte. */
  width:var(--fhero-mask-w,min(82vw,1180px));aspect-ratio:var(--fhero-mask-ratio,3.237322)/1;
  transform:translate(-50%,-50%);
  opacity:0; /* default sin soporte / antes de que @supports lo anime */
  background:var(--papel);
  -webkit-mask-image:var(--fhero-mask,url(/assets/brand/hof-lockup.svg));
  mask-image:var(--fhero-mask,url(/assets/brand/hof-lockup.svg));
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:contain;mask-size:contain;
  -webkit-mask-position:center;mask-position:center;
}
.fhero--r55 .fhero__tinta{
  position:absolute;inset:0;z-index:1;overflow:hidden;
  opacity:0; /* default sin soporte / antes de que @supports lo anime */
  -webkit-mask-image:var(--fhero-mask,url(/assets/brand/hof-lockup.svg));
  mask-image:var(--fhero-mask,url(/assets/brand/hof-lockup.svg));
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:var(--fhero-mask-w,min(82vw,1180px)) auto;mask-size:var(--fhero-mask-w,min(82vw,1180px)) auto;
  /* CORRECCIONES_R55_FASE2_MASCARA.md · DEFECTO 1: mask-position con % no
     centra, ancla proporcional — offsetY=(H-mH)*0.46, centro real en
     0.46H+0.04mH. Restar 0.04*mH (=mW/(ratio/0.04)) alinea el centro real con
     .fhero__wordmark (top:46% + translate(-50%,-50%), que sí centra).
     R63: divisor generalizado a var(--fhero-mask-ratio)*25 (=ratio/0.04) —
     antes era 80.933054, el número fijo de hof (3.237322*25). */
  -webkit-mask-position:center calc(46% - var(--fhero-mask-w,min(82vw,1180px))/(var(--fhero-mask-ratio,3.237322)*25));
          mask-position:center calc(46% - var(--fhero-mask-w,min(82vw,1180px))/(var(--fhero-mask-ratio,3.237322)*25));
}
.fhero--r55 .fhero__tinta img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  object-position:var(--fhero-pos,50% 50%);
}

@supports (animation-timeline:scroll()){
  /* M-A: sube+zoom. scroll(root block) explícito, no 'nearest' — .fhero--r55 y
     .fhero__pin son overflow:clip y podrían elegirse por error como scroller. */
  .fhero--r55 .fhero__tinta img{
    animation-name:hofImgScrub;
    animation-timeline:scroll(root block);
    animation-range:0 var(--hero-ventana,1400px);
    animation-fill-mode:both;
    animation-timing-function:var(--ease-scrub);
  }
  .fhero--r55 .fhero__img img{
    animation-name:hofImgScrub,hofImgFade;
    animation-timeline:scroll(root block),scroll(root block);
    animation-range:0 var(--hero-ventana,1400px),0 var(--hero-ventana,1400px);
    animation-fill-mode:both,both;
    animation-timing-function:var(--ease-scrub),var(--ease-scrub);
  }
  .fhero--r55 .env{
    animation-name:hofEnvScale,hofEnvFade;
    animation-timeline:scroll(root block),scroll(root block);
    animation-range:0 var(--hero-ventana,1400px),0 var(--hero-ventana,1400px);
    animation-fill-mode:both,both;
    animation-timing-function:var(--ease-scrub),var(--ease-scrub);
  }

  /* M-C: relevo render→tinta + wordmark sólido */
  .fhero--r55 .fhero__wordmark{
    animation-name:hofWordmarkFade;
    animation-timeline:scroll(root block);
    animation-range:0 var(--hero-ventana,1400px);
    animation-fill-mode:both;
    animation-timing-function:var(--ease-scrub);
  }
  .fhero--r55 .fhero__tinta{
    animation-name:hofTintaFade;
    animation-timeline:scroll(root block);
    animation-range:0 var(--hero-ventana,1400px);
    animation-fill-mode:both;
    animation-timing-function:var(--ease-scrub);
  }

  /* ═══ ADENDA_R72_VENTANA_HERO.md — sustituye el fix parcial de R72 (el que
     dejaba el rango en `(--fhero-r55-vh - 100)*1vh`, variable por ficha/tier:
     ~2025px en largo, ~1400px en medio, y corto sin pin del todo). Jara aprobó
     unificar: LAS 8 fichas usan la MISMA ventana de scroll, `--hero-ventana`
     (1400px, arriba en :root) -- ya no hay fórmula por tier. Con el valor fijo
     ya puesto en la regla base de arriba (M-A/M-C, sin condicionar por clase de
     gesto), gL no necesita override propio -- hereda tal cual. gM sí lo
     necesita para su wordmark (ver bloque GESTO MEDIO, su propio
     animation-name no es hofWordmarkFade puro) y su tinta (r70MedioMt, mismo
     bloque). La ALTURA del contenedor (--fhero-r55-vh) NO cambia para
     hof/central-escorza/nikté (325/312/321vh, ya sobra para que el pin aguante
     los 1400px sin soltarse antes de tiempo) -- lo único que cambiaba de tier a
     tier era CUÁNTO scroll tardaba en resolver el gesto, y eso es justo lo que
     esta adenda retira: "la diferencia entre desarrollos deja de vivir en la
     distancia de scroll" (adenda §3.2). Medido: cov_fin 0.90-1.00 en las 8,
     ver RESULTADO_72.md. */
}

/* Gate 7 — prefers-reduced-motion: estado final legible y completo, no vacío,
   0 animaciones activas. Se prioriza la INFORMACIÓN (foto + copy) sobre el
   momento firma, que es decorativo por definición. !important gana sobre
   @supports arriba sin importar el orden de cascada. */
@media (prefers-reduced-motion:reduce){
  .fhero--r55 .fhero__img img,
  .fhero--r55 .fhero__tinta img,
  .fhero--r55 .fhero__tinta,
  .fhero--r55 .fhero__wordmark,
  .fhero--r55 .env{ animation:none!important; }
  .fhero--r55 .fhero__img img{ opacity:1!important;transform:none!important; }
  .fhero--r55 .env{ opacity:1!important;transform:none!important; }
  .fhero--r55 .fhero__wordmark,
  .fhero--r55 .fhero__tinta{ opacity:0!important; }
}

/* ============================================================
   RONDA 70 — tres gestos, tres tiempos (BRIEF_R70_TRES_GESTOS.md).
   Diagnóstico de Jara: el gesto de R55 (arriba) se repite ocho veces
   con la MISMA duración (~3.25vp) sin importar cuánto material tenga
   la ficha. El scaffold de máscara/wordmark/tinta de R55 NO se toca --
   sigue siendo la única infraestructura visual, para las 8 fichas.
   Lo que cambia por TIER es el mecanismo de la FOTO (M-A) y el de la
   copy (.env): gesto largo (sin clase extra = comportamiento de
   arriba, intacto) en hof/central-escorza/nikté; gesto medio
   (.fhero--gM) en madison/quadrante; gesto corto (.fhero--gC) en
   kartesia/montevideo/simona. Reparto aprobado por Jara 07-sep.

   BUG REAL cazado con captura, no en el spec (R70): el truco de R55
   (`.fhero--r55{margin-bottom:-100svh}` + `.fhero__pin{position:sticky;
   height:100svh}`) exige contenedor >=200vh -- el margen negativo tira
   la SIGUIENTE sección hacia arriba una cantidad FIJA de 100svh sin
   importar cuánto mida el contenedor, y esa sección siguiente tiene
   z-index:3 (línea ~1042, mayor que el z-index:2 del pin): con H<200vh
   la sección siguiente ya se solapa por encima del pin incluso en
   scroll=0. R70 "arregló" esto quitando el pin ENTERO de medio y corto;
   R71 encontró que sin pin, medio resolvía con el hero fuera de pantalla
   (cobertura 50%) y le devolvió el pin con H>=200vh de verdad. R72 encontró
   el MISMO defecto en el tier largo (que sí conservaba el pin, pero con un
   animation-range que llegaba hasta el borde de soltarse). La
   ADENDA_R72_VENTANA_HERO unifica: las 8 fichas usan pin + `--hero-ventana`
   (arriba en :root), sin excepción de tier -- corto RECUPERA el pin que R70
   le había quitado (su H ya sube a >=200vh en R55_MASK_VARS, igual que medio;
   el mecanismo de bandas pasa de resolver en 10vh a hacerlo en toda la
   ventana de 1400px, ver bloque GESTO CORTO abajo). */

@supports (animation-timeline:scroll()){
  /* Ni medio ni corto escalan ni paneé la foto de fondo (G-SIN-ZOOM: <=1.00 en
     .fhero__img img) -- se apaga el M-A/M-C de R55 para las 5 fichas de estos dos
     tiers. El copy (.env) tampoco se apaga: con un pin corto no hay "epica" que
     sostenga dejar al usuario sin texto -- se queda legible todo el recorrido
     (deviación declarada de R55, autorizada por el propio brief §5 de R70). */
  .fhero--r55.fhero--gM .fhero__tinta img,
  .fhero--r55.fhero--gC .fhero__tinta img,
  .fhero--r55.fhero--gM .env,
  .fhero--r55.fhero--gC .env{
    animation:none;
  }
  /* R74 (BRIEF_R74_WORDMARK_MADISON_QUADRANTE.md) -- BUG REAL: apagar TODO
     `.fhero__img img` (transform+opacity juntos, `hofImgScrub,hofImgFade`) para
     que no escalara dejó la foto SIEMPRE opaca detrás de `.fhero__tinta`. Como
     `.fhero__tinta img` es un <img> DISTINTO pero apuntando al MISMO archivo,
     con el MISMO object-fit/object-position y SIN transform propio, pinta
     exactamente los mismos píxeles que la foto de fondo -- el "relieve" de la
     máscara (R74 arriba) revela una copia idéntica de lo que ya se ve debajo,
     invisible salvo por el antialiasing del borde. Medido: con la foto de fondo
     al 100% de opacidad, el nombre apenas se distingue (ver qa/r74/screens/
     madison_y1400_antes.png). En largo (gL), `hofImgFade` SÍ apaga la foto de
     fondo -- por eso su relevo se lee nítido, la tinta queda como la única
     capa visible. Fix: separar SCALE (que debe quedarse en 1, G-SIN-ZOOM) de
     OPACIDAD (que sí puede apagarse) -- `.fhero__img img` de medio anima SOLO
     `hofImgFade` (nunca `hofImgScrub`, que es la que mete el zoom). Sin
     `hofImgScrub` activo, `transform` cae al valor estático de la hoja base
     (`scale(var(--fhero-zoom,1))`, línea ~1047) -- 1 en madison/quadrante (sin
     override en FHERO_VARS), cumple G-SIN-ZOOM sin necesidad de `transform:none`
     explícito.
     R75 (SPEC_HERO_MECANISMO_2026-09-08.md §5.2): corto se suma a esta regla --
     su .fhero__tinta ahora SÍ se usa (la ventana que las letras destapan) y
     necesita que el fondo se apague igual que en medio para el mismo relevo. */
  .fhero--r55.fhero--gM .fhero__img img,
  .fhero--r55.fhero--gC .fhero__img img{
    animation-name:hofImgFade;
    animation-timeline:scroll(root block);
    animation-range:0 var(--hero-ventana,1400px);
    animation-fill-mode:both;
    animation-timing-function:var(--ease-scrub);
  }
}

/* ── GESTO MEDIO — "clip que viaja, sin zoom" (madison, quadrante).
   Fuente citada en el brief: codrops/TextClipScroll, js/index.js:48-56 (MIT) --
   recorrido del clip con scrub:true/ease:'none'. Se porta como scroll(root
   block)/linear, la misma convención que M-A de R55 arriba (mismo repo de
   técnica, mismo navegador objetivo). Lo que viaja NO es la foto (que se queda
   en --fhero-zoom:1, ninguna de las dos fichas lo sobreescribe -- FHERO_VARS) --
   es la VENTANA: mask-position de .fhero__tinta recorre el marco de arriba a
   abajo sobre la MISMA foto estática de fondo, revelando tramos distintos de
   ella a través del recorte del wordmark. --fhero-tinta-centro reusa la fórmula
   de alineación de R63 (línea ~2510 arriba) para que el punto MEDIO del
   recorrido siga centrado con .fhero__wordmark; --mt (custom property numérica,
   registrada con @property para que la animación interpole el número, no el
   string) mueve +-22% de esa base. Duración del gesto (ADENDA_R72_VENTANA_HERO):
   `var(--hero-ventana)` (1400px, en :root) -- la misma ventana única de las 8
   fichas, ya no una fracción del contenedor propia de este tier.

   R74 (BRIEF_R74_WORDMARK_MADISON_QUADRANTE.md) -- BUG REAL, no visto hasta
   ahora: `.fhero__tinta` sólo tenía `animation-name:r70MedioMt` (mueve
   --mt/mask-position) -- perdió el `hofTintaFade` que la regla BASE de R55 le
   da a hof/escorza/nikté para abrir su OPACIDAD. Con `opacity:0` de base
   (R55, línea ~2500: "sin soporte / antes de que @supports lo anime") y sin
   nada que la suba, `.fhero__tinta` quedaba invisible en TODO el recorrido --
   medido: opacity=0 en los 8 puntos de scroll muestreados (0 a 1400px). El
   "clip que viaja" literalmente no se veía nunca: la ventana sí se movía
   (--mt sí interpola) pero sobre una capa transparente. Lo único visible era
   el flash de 150-750px del wordmark SÓLIDO (`hofWordmarkFade`, que sí seguía
   activo) -- y esa curva, calcada de R55, se apaga otra vez a partir del 60%
   de su rango (pensada para un pin largo con mucho scroll de foto sola
   DESPUÉS del flash). Sobre la ventana de 1400px eso significa: apagado por
   completo desde los 840px, así que a y=1400 (el punto que Jara y el brief
   miden) no hay wordmark NI tinta -- de ahí "no muestran rótulo gigante".
   Fix: se le agrega `hofTintaFade` (la MISMA curva que usa gL para su propio
   relevo -- cero número nuevo, misma fuente ya citada en R55/R63) como
   SEGUNDA animación sobre `.fhero__tinta`, en paralelo a `r70MedioMt`. Efecto:
   el mismo relevo sólido-a-foto de largo (wordmark flashea 15-35%, se apaga
   35-60%; tinta sube 37.5-50% y se queda) pero con la ventana VIAJANDO encima
   -- a partir de la mitad del recorrido, el nombre gigante queda formado por
   la foto vista a través de la máscara, con la ventana todavía moviéndose
   dentro de ese recorte. Verificado: legible y completo a y=1400 en las 2
   fichas (ver RESULTADO_74.md).

   Piso de motion — 3 curvas en TODO el archivo (ver :root): `--ease-scrub`
   (linear, el recorrido primario del clip) y `--ease-out` (absorbe el `sine`
   que tenía el wordmark como capa secundaria -- ya no es una curva aparte,
   ver consolidación en :root). */
@property --mt{ syntax:'<number>'; inherits:true; initial-value:0 }
.fhero--r55.fhero--gM .fhero__tinta{
  --fhero-tinta-centro:calc(46% - var(--fhero-mask-w,min(82vw,1180px))/(var(--fhero-mask-ratio,3.237322)*25));
  /* R75 (SPEC_HERO_MECANISMO_2026-09-08.md §5.3): --mt-dir invierte el sentido
     del viaje por ficha (1 desciende/default=madison, -1 asciende=quadrante,
     R55_MASK_VARS). Amplitud 22% y --mt=0 en u=0.5 sin cambio -- los gates de
     R74 sobre el relevo no se mueven. */
  -webkit-mask-position:center calc(var(--fhero-tinta-centro) + var(--mt,0)*var(--mt-dir,1)*22%);
          mask-position:center calc(var(--fhero-tinta-centro) + var(--mt,0)*var(--mt-dir,1)*22%);
}
@keyframes r70MedioMt{ from{ --mt:-1 } to{ --mt:1 } }
@supports (animation-timeline:scroll()){
  .fhero--r55.fhero--gM .fhero__tinta{
    animation-name:r70MedioMt,hofTintaFade;
    animation-timeline:scroll(root block),scroll(root block);
    animation-range:0 var(--hero-ventana,1400px),0 var(--hero-ventana,1400px);
    animation-fill-mode:both,both;
    animation-timing-function:var(--ease-scrub),var(--ease-scrub);
  }
  /* El animation-range del wordmark ya se unificó arriba (bloque "ADENDA_R72_
     VENTANA_HERO", junto al de .fhero--gL) -- aquí solo queda su propia curva. */
  .fhero--r55.fhero--gM .fhero__wordmark{
    animation-timing-function:var(--ease-out);
  }
}
@media (prefers-reduced-motion:reduce){
  .fhero--r55.fhero--gM .fhero__tinta{ animation:none!important; --mt:0!important; }
}

/* ── GESTO CORTO — "la palabra se compone y se abre letra por letra"
   (kartesia, montevideo, simona). SPEC_HERO_MECANISMO_2026-09-08.md.
   Sustituye a las bandas de R70-R74 (tiras horizontales: escalones = "8bit", Jara 08-sep).
   Fuente: codrops/OnScrollTypographyAnimations src/js/index.js:95-107 (subida por
   letra, yPercent 120->0, stagger .03) y :77 (cascada casi secuencial, stagger .4 sobre
   duracion default .5) -- MIT, leido 2026-09-08. Desviacion declarada: back.inOut(2)/power4
   -> var(--ease-out) (arquetipo Premium, 0% overshoot; un logotipo con overshoot se clipea).
   Regla dura: NINGUN corte atraviesa tinta -- las unidades salen de los huecos del PNG
   (construir_fichas.py . unidades_letra). Nada se traslada en X. Sin scale/skew/rotate:
   es el logotipo del cliente. */
.fhero--r55.fhero--gC .fhero__wordmark{ animation:none!important; opacity:0!important; }
/* .fhero__tinta SI se usa en corto (hofTintaFade, regla base R55 arriba): es la
   ventana que las letras destapan. Se retira el apagado de R70. */
.fhero--r55.fhero--gC .fhero__letras{
  position:absolute;z-index:1;left:50%;top:46%;pointer-events:none;
  width:var(--fhero-mask-w,min(82vw,1180px));aspect-ratio:var(--fhero-mask-ratio,3.237322)/1;
  transform:translate(-50%,-50%);
}
/* Scrim R72 P1 sin cambio; se apaga con el relevo para no velar la ventana. */
.fhero--r55.fhero--gC .fhero__letras::before{
  content:"";position:absolute;z-index:-1;inset:-18% -8%;
  background:radial-gradient(ellipse 62% 68% at 50% 50%,
    rgba(6,12,22,.38) 0%, rgba(6,12,22,.22) 45%, rgba(6,12,22,0) 78%);
}
/* Una unidad = una ventana recortada a la columna de su letra (overflow:hidden corta
   en X en los huecos medidos y en Y en el borde de la caja: ese borde inferior es la
   unica linea que atraviesa un glifo, la misma para todas las letras). */
.fhero--r55.fhero--gC .fhero__letra{
  position:absolute;top:0;height:100%;
  left:calc(var(--lx,0)*100%);width:calc(var(--lw,1)*100%);
  overflow:hidden;
}
/* Dentro, la mascara COMPLETA al tamano exacto de la caja (mask-size 100% 100%, sin el
   truco de % de mask-position que costo R73), corrida a la izquierda lo que la unidad
   esta corrida a la derecha: solo se ve su letra. Sube desde 120% (fuera de la caja). */
.fhero--r55.fhero--gC .fhero__letra i{
  position:absolute;top:0;height:100%;
  width:var(--fhero-mask-w,min(82vw,1180px));
  left:calc(-1 * var(--lx,0) * var(--fhero-mask-w,min(82vw,1180px)));
  background:var(--papel);
  -webkit-mask-image:var(--fhero-mask,url(/assets/brand/hof-word-mask.png));
  mask-image:var(--fhero-mask,url(/assets/brand/hof-word-mask.png));
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:100% 100%;mask-size:100% 100%;
  -webkit-mask-position:0 0;mask-position:0 0;
  transform:translateY(120%);
  opacity:1;
}
@keyframes r75LetraSube{ from{ transform:translateY(120%) } to{ transform:translateY(0) } }
@keyframes r75LetraAbre{ from{ opacity:1 } to{ opacity:0 } }
@supports (animation-timeline:scroll()){
  .fhero--r55.fhero--gC .fhero__letra i{
    animation-name:r75LetraSube,r75LetraAbre;
    animation-timeline:scroll(root block),scroll(root block);   /* R70: nunca view() en el primer elemento */
    animation-range:
      calc(var(--a0,0)*var(--hero-ventana,1400px)) calc(var(--a1,.35)*var(--hero-ventana,1400px)),
      calc(var(--c0,.5)*var(--hero-ventana,1400px)) calc(var(--c1,.875)*var(--hero-ventana,1400px));
    animation-fill-mode:both,both;
    animation-timing-function:var(--ease-out),var(--ease-out);
  }
  .fhero--r55.fhero--gC .fhero__letras::before{
    animation-name:hofImgFade;
    animation-timeline:scroll(root block);
    animation-range:0 var(--hero-ventana,1400px);
    animation-fill-mode:both;
    animation-timing-function:var(--ease-scrub);
  }
}
@media (prefers-reduced-motion:reduce){
  /* Misma politica que R55: informacion (foto + copy) sobre momento firma.
     Letras y ventana apagadas, foto quieta al 100%, copy al 100%. */
  .fhero--r55.fhero--gC .fhero__letras,
  .fhero--r55.fhero--gC .fhero__letras::before,
  .fhero--r55.fhero--gC .fhero__letra i{ animation:none!important; }
  .fhero--r55.fhero--gC .fhero__letras{ opacity:0!important; }
}

/* ============================================================
   R17 — "el índice se recorre": mecanismo compartido del índice
   pineado de amenidades (AUDIT_GALERIA_FICHAS_2026-09-11.md §6).
   Validado en dos extremos: proto/R17_simona_indice_390.html (N=2,
   RESULTADO_R17_PASO3.md) y proto/R17_quadrante_indice.html (N=15,
   FIX_R17_PROTO2_ANCLAJE.md, cerrado 8/8 en RESULTADO_R17_PROTO2_FIX.md).
   Este bloque es la mecánica COMPARTIDA -- un solo lugar para las 8
   fichas. Los números que dependen de N por ficha (ventanas de
   `.indice li`, paradas del zigzag `@keyframes progreso`, ventanas
   de reveal por foto) los emite `construir_fichas.py` en un <style>
   propio de cada página (`build_amenidades_indice`), nunca a mano.
   ============================================================ */
/* REGRESION_R17_HUECO_MOVIL.md: `--on` YA está registrado en `ficha.src.css:2074`
   (`inherits:true`, para que R56 herede el valor del contenedor `.modelos`/`.amen`
   a sus hijos `.modelo`/`li`). Una segunda `@property --on{...}` con `inherits:false`
   pisaba esa herencia -- `.modelo`/`.amen li` dejaban de recibir `--on` de su
   contenedor y se quedaban en el valor inicial (0): `opacity:0` fijo, la sección de
   modelos invisible. NO volver a registrar `--on` aquí -- se reusa el de línea 2074,
   que ya trae exactamente `syntax:'<number>'; initial-value:0` (lo único que cambia
   es `inherits`, y el mecanismo R17 no necesita `false`: cada `.indice li`/
   `.visor-pan img` fija su propio `--on` local antes de animar, así que hereda o no,
   da igual para R17 -- pero si rompe R56). Solo `--t` es nuevo de verdad. */
@property --t{ syntax:'<number>'; inherits:false; initial-value:0; }

.pin-track{
  view-timeline-name:--idx;
  view-timeline-axis:block;
  /* Ventana de pin = track - stage (FIX_R17_PASO1 §1); 0.4vp × N_pasos
     (AUDIT §6.2). --n-pasos llega inline por ficha: style="--n-pasos:N". */
  height:calc(100vh + (var(--n-pasos) * 40vh));
  position:relative;
}
.pin-stage{
  position:sticky; top:0; min-height:100vh;
  display:flex; flex-direction:column; justify-content:center; gap:26px;
  padding:32px 0;
}
.visor{
  position:relative; width:100%; max-width:350px; margin:0 auto;
  aspect-ratio:16/9; background:var(--papel-2); overflow:hidden;
}
.visor-pan{
  position:absolute; inset:0;
  --t:0;
  animation:progreso linear both;
  animation-timeline:--idx;
  animation-range:contain 0% contain 100%;
  /* Addendum RESULTADO_R17_PASO3.md: ±5% de la caja propia, invariante
     frente a N_pasos -- mismo techo de overscan que scale(1.12) en N=2 o N=15. */
  transform:scale(1.12) translateY(calc((var(--t) - .5) * -10%));
}
.visor-pan img{
  position:absolute; inset:0; opacity:0; width:100%; height:100%; object-fit:cover;
}
@keyframes reveal{ from{--on:0} to{--on:1} }
/* @keyframes progreso vive en el <style> por ficha -- las paradas dependen de N. */

.indice{list-style:none;margin:0;padding:0}
.indice li{
  position:relative; padding:13px 0 13px 26px; border-top:1px solid var(--linea);
  font-size:.95rem; color:var(--niebla-papel);
}
.indice li .mark{
  position:absolute; left:4px; top:50%; width:9px; height:9px; margin-top:-4.5px;
  border-radius:50%; background:var(--asfalto);
  transform:translateY(calc((1 - var(--on)) * -170%)) scale(var(--on));
  /* Sin transition: el scroll maneja --on lineal vía animation-timeline
     (FIX_R17_PROTO2 §4.2) -- un transition de reloj pelearía contra el dedo. */
}
@keyframes idx-on{ from{--on:0;color:var(--niebla-papel)} to{--on:1;color:var(--asfalto)} }
.indice li{
  --on:0;
  animation:idx-on linear both;
  animation-timeline:--idx;
}
/* animation-range por :nth-child, generado por ficha (ventanas dependen de N). */

@media (min-width:1024px){
  /* Escritorio: dos columnas, visor grande (FIX_R17_PASO3_DESKTOP, portado).
     .env ya da --ancho:1240px de columna -- no hace falta ensancharlo. */
  .pin-stage{ flex-direction:row; align-items:center; justify-content:center; gap:64px; }
  .visor{ max-width:700px; flex:1 1 62%; }
  .indice{ flex:1 1 32%; max-width:340px; }
  .indice li{ padding:11px 0 11px 30px }
  .indice li .mark{ width:10px; height:10px; margin-top:-5px }
}

.indice-estatica{display:none}
.indice-estatica figure{margin:0 0 14px;aspect-ratio:16/9;overflow:hidden;background:var(--papel-2)}
.indice-estatica img{width:100%;height:100%;object-fit:cover;display:block}
.indice-estatica li.con-foto{color:var(--asfalto)}

@media (prefers-reduced-motion:reduce){
  .pin-track{display:none}
  .indice-estatica{display:block !important}
}

@supports not (animation-timeline:view()){
  .pin-track{height:auto}
  .pin-stage{position:static;min-height:0;padding:0;flex-direction:column}
  .visor-pan{position:relative;transform:none;animation:none}
  .visor-pan img{animation:none;opacity:0}
  .visor-pan img:first-of-type{opacity:1}
  .indice li{animation:none}
  .indice li:first-child{color:var(--asfalto)}
}

/* ═══ 12-sep · AMENIDADES OPCIÓN A: un paso por foto ═══════════════════════════════
   El pin ya solo recorre las amenidades CON foto (construir_fichas.py). En celular el
   visor era una tira de 350x196 px -- ahora ocupa el ancho de la columna y un cuadro
   1:1 (~358 px en 390, cerca de media pantalla), para que el cambio de foto se lea.
   Debajo del pin, la lista COMPLETA fija, sin animación: nada que "seleccionar" fuera
   de pantalla. En reduced-motion / sin soporte la lista la cubre .indice-estatica. */
@media (max-width:1023px){
  .visor{ max-width:none; aspect-ratio:1/1; }
  /* T1 · BRIEF_F2K (17-sep): min-height:100vh forzaba el stage a una pantalla
     completa aunque su contenido (foto 350px + índice de 3 líneas) mida ~450px
     -- justify-content:center repartía el sobrante como aire arriba/abajo
     (494px medidos = 59% del stage). Con min-height:0 el sticky se ajusta a su
     contenido; el mecanismo de pin (view-timeline sobre .pin-track, más alto
     que el stage) no depende de que el stage sea 100vh, solo de que sea sticky. */
  .pin-stage{ min-height:0; justify-content:flex-start; }
}
/* T2 · BRIEF_F2K (17-sep): subtítulo entre la escena pineada y la lista de
   amenidades sin foto. Token --t-cuerpo (18px, línea ~92) -- el único de los 7
   escalones entre el h3 de la sección (--t-h3, 21.6px@390) y el cuerpo de
   .amen-lista li (.95rem/15.2px): --t-chico (14px) queda POR DEBAJO del propio
   body de la lista, no sirve de jerarquía intermedia. Sin margin propio: el
   gap escena→subtítulo lo da .pin-stage (32px padding-bottom heredado del
   ajuste T1) y subtítulo→lista queda en 0 para no sumar aire nuevo al cupo de
   28px que ya tenía la lista (gate 7, F2K). */
.amen-otras{
  margin:28px 0 0; font-size:var(--t-cuerpo); font-weight:500; color:var(--asfalto);
}
.amen-lista{
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:24px;
}
.amen-lista li{
  padding:11px 0; border-top:1px solid var(--linea);
  font-size:.95rem; color:var(--asfalto);
}
@media (min-width:1024px){ .amen-lista{ grid-template-columns:repeat(3,minmax(0,1fr)); } }
@media (prefers-reduced-motion:reduce){ .amen-lista{ display:none } }
@supports not (animation-timeline:view()){ .amen-lista{ display:none } }

/* ═══ T3 · BRIEF_FICHAS_F2I (17-sep) · retícula compacta en las secciones de puro texto ═══
   Medido por Krillin en el draft y confirmado por desglose propio (quadrante @390, vh 844):
   "EL PROYECTO"+.cifras = 749px (0.89vh, 227 chars, 0 imgs, densidad 0.30) e
   "INVERSIÓN" = 1,063px (1.26vh, 332 chars, 0.31) → 1,812px = 2.15 viewports para 559
   caracteres y ni una imagen. El problema no es el contenido: es que cada dato ocupa una
   FILA COMPLETA de ancho total con filete. Desglose: .cifras 356px en 3 filas de ~119px,
   .tramos 317px en 3 filas, y 352px solo de padding vertical de las dos secciones.
   Aquí los datos pasan a retícula de 3 en línea. No se borra ni un dato: los 3 de .cifras
   y los 3 del plan de pagos siguen en el DOM, solo dejan de apilarse.
   Va en la hoja compartida, así que aplica a las 8 fichas, no a quadrante a mano. */
@media (max-width:520px){
  /* El aire de ERA (--sec-pad 88px @390) es lo que más pesa en una sección sin imagen:
     176px por sección, contra 573px de contenido. Se recorta solo en las de puro texto. */
  /* El padding NO se puede bajar por selector: `[data-sys="era"] .seccion:not(:has(.mapbox))
     :not(:has(.amen))` es (0,4,0) y gana a cualquier `.seccion:has(...)` razonable — medido,
     mi primer intento no movió un px. Se baja el TOKEN que esa regla consume, que aplica al
     elemento sin pelear especificidad. 88px de aire por lado, en una sección sin imagen y de
     297px de contenido, era más marco que cuadro. */
  .seccion:has(.cifras),
  .seccion.inversion-clara{--sec-pad:40px;padding-block:40px}
  /* las dos columnas del plan apiladas: el hueco entre ellas es de escritorio */
  /* el padding-top de .dosc alinea las DOS columnas en escritorio; apiladas a 390 es
     un hueco de 20.75px que no separa nada (medido en el desglose). */
  .inversion-clara .dosc{gap:16px;padding-top:0}
  .linea-t{margin-top:12px;margin-bottom:8px}
  .inversion-clara .eyebrow,.seccion:has(.cifras) .eyebrow{margin-bottom:12px}
  .precio__sub{margin-top:10px}
  .cinta{margin-top:20px}

  /* .cifras: 3 en línea. El filete vuelve a ser vertical (como en escritorio), no
     horizontal: es lo que convierte tres filas en una. */
  .cifras{grid-template-columns:repeat(3,1fr);margin-top:20px}
  .cifras div{border-right:1px solid var(--linea);border-bottom:0;padding:12px 8px 12px 0}
  .cifras div:last-child{border-right:0}
  /* 6.4vw = 24.96px @390 contra 18px de cuerpo → ratio 1.39. El piso del gate es 1.35:
     por debajo de eso la cifra deja de leerse como jerarquía (la trampa del h2 de hoy,
     18.4px contra 18px = ratio 1.02). white-space:nowrap sigue vivo de la regla base,
     así que el rango "63–123" no puede partirse dentro de su columna. */
  .cifras b{font-size:clamp(1.55rem,6.4vw,2rem)}
  .cifras b i{font-size:var(--t-micro);padding-left:.1em}
  .cifras p{margin:6px 0 0;font-size:12px;line-height:1.28;max-width:none}
  .cifras-resto{margin-top:14px}

  /* plan de pagos: 5% / 10% / resto ya son tres piezas paralelas — se leen como tres,
     no como tres renglones. */
  .tramos{grid-template-columns:repeat(3,1fr);margin-top:18px}
  .tramos div{border-right:1px solid var(--linea-osc);border-bottom:0;padding:14px 8px 4px 0}
  .tramos div + div{padding-left:8px}
  .tramos div:last-child{border-right:0;padding-right:0}
  /* 6.6vw = 25.7px @390 → ratio 1.43 contra el cuerpo. "Resto" es la pieza más ancha
     de las tres y es la que fija el techo. */
  .tramos b{font-size:clamp(1.5rem,6.6vw,2rem)}
  .tramos p{margin:6px 0 0;font-size:12px;line-height:1.28}

  .plus{margin-top:16px;margin-bottom:8px;padding-top:12px}
  .linea-t{margin-top:16px}
}
