/* =========================================================================
   CORE OPTIMIZATION LAYER · ginderhernandez.com
   -------------------------------------------------------------------------
   Capa aditiva SIN media query: se aplica en TODOS los anchos. Nace de
   partir desktop-opt.css, que se enlazaba sin `media` y por tanto se
   descargaba y aplicaba entera también en móvil.
   La parte >=761px se queda en desktop-opt.css, cuyo <link> ya lleva
   media="(min-width:761px)".
   NO REORDENAR: la cascada tokens.css -> mobile-opt.css -> core-opt.css ->
   desktop-opt.css reproduce el orden interno del archivo original.
   OJO: @keyframes dtChipIn y la regla de reduced-motion de .mo-fchip TIENEN
   que quedarse aquí: mobile-opt.css depende de ese keyframes.
   ========================================================================= */

/* =========================================================================
   ULTRAWIDE  ·  fluir en pantallas grandes (hoy congelado en 1000px).
   Aditivo: por debajo de 1441px todo queda EXACTAMENTE igual que ahora.
   ========================================================================= */
@media (min-width: 1441px) {
  .wrap { max-width: 1200px; }
  .work.wrap { max-width: 1440px; }
  .grid {
    grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)) !important;
    gap: 20px !important;
  }
  .act-card { max-width: 680px; }
  /* Medida de lectura más amplia SOLO en el hero (no en el contacto, cuyo
     subtítulo "Hablemos." debe seguir a ancho completo y centrado). */
  .hero .sub { max-width: 40ch; }
  .intro .fp, .journey .act-card p { max-width: 46ch; }
}

@media (min-width: 1920px) {
  .wrap { max-width: 1320px; }
  .work.wrap { max-width: 1600px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important; }
}

/* =========================================================================
   PULIDO DE MOTION (universal, todos los tamaños)
   -------------------------------------------------------------------------
   Los modales cerraban de golpe (display:none instantáneo) — un corte seco.
   Les damos salida animada con `allow-discrete` (el display espera al final
   de la transición) y entrada garantizada vía @starting-style. La salida es
   MÁS RÁPIDA que la entrada (asimetría deliberada: el sistema responde ya).
   Navegadores antiguos ignoran allow-discrete y quedan como estaban.
   ========================================================================= */
/* La entrada de los modales la controlan las animaciones del tema
   (modalFade/modalPop con fill:both) — NO la tocamos. Por eso la salida
   debe ser también animación: una propiedad poseída por una animación no
   dispara transiciones al retirarse esa animación (spec de CSS Transitions).
   `allow-discrete` solo retrasa el display:none hasta el final. */
.modal,
.vmodal {
  transition: display 0.26s allow-discrete;
}
.modal:not(.open),
.vmodal:not(.open) {
  animation: ghModalOut 0.24s cubic-bezier(0.23, 1, 0.32, 1) both;
}
.modal:not(.open) .modal-card,
.vmodal:not(.open) .vmodal-inner {
  animation: ghModalCardOut 0.24s cubic-bezier(0.23, 1, 0.32, 1) both;
}

/* Solo corre durante la ventana de salida (con display:none no hay
   animaciones, así que en reposo cerrado no hace nada). */
@keyframes ghModalOut {
  from { opacity: 1; }
  to { opacity: 0; }
}
@keyframes ghModalCardOut {
  from { opacity: 1; transform: scale(1); }
  to { opacity: 0; transform: scale(0.96); }
}

/* El scroll no debe "sangrar" a la página de fondo desde el modal (iOS) */
.modal, .vmodal { overscroll-behavior: contain; }

/* Asentamiento rápido del comparador al imantarse al 50% (la transición
   .cg-anim del tema es de 0.65s — bien para la auto-invitación, lenta para
   una respuesta del sistema; al soltar debe ser ágil). */
.cg-ba.gh-snap .cg-flat { transition: clip-path 0.22s cubic-bezier(0.23, 1, 0.32, 1); }
.cg-ba.gh-snap .cg-handle { transition: left 0.22s cubic-bezier(0.23, 1, 0.32, 1); }

@media (prefers-reduced-motion: reduce) {
  .modal, .vmodal { transition-duration: 0.12s; }
  .modal:not(.open), .vmodal:not(.open),
  .modal:not(.open) .modal-card, .vmodal:not(.open) .vmodal-inner {
    animation-duration: 0.1s;
  }
  .dt-fchip, .mo-fchip { animation: none !important; }
}

/* Entrada de chips de filtro (compartida escritorio/móvil) */
@keyframes dtChipIn {
  from { opacity: 0; transform: translateY(6px); }
}

.gh-st-eyebrow {
  display: block;
  font-family: "Space Mono", ui-monospace, monospace;
  font-size: 12px;
  letter-spacing: 0.22em;
  color: var(--mist, #a6a6b0);
  margin-bottom: 34px;
}

/* Footer plano original RESTAURADO (la sección final gh-finale fue eliminada;
   contacto vuelve a ser la última sección, como en el diseño original). */

/* --- 3b-bis) SECCIÓN NEBLINA (espaciolanube "home_stck"):
       5 medios alternando izquierda/derecha. El difuminado que "arropa" la
       imagen con el blanco del fondo lo hace UNA máscara SVG con desenfoque
       gaussiano (ver .gh-mist-media). Imagen estática; solo el botón central
       persigue el cursor como un imán. */
/* Registro oculto: el grid viejo alimenta modal/ambilight/proxies del tema */
.gh-registry { display: none !important; }
.gh-mist {
  background: #f6f4ef; /* PAPEL CÁLIDO (#7) — no #fff clínico; entona los reels */
  padding: 4vh 0 14vh;
  position: relative;
  overflow: hidden;
  z-index: 2;
}
/* CLAVE: la clase .sec-light (que uso para que el nav se adapte) trae un
   glow de color del tema vía ::before/::after (azul izq + rosa der). En el
   mist lo ANULAMOS → blanco total. (La sección "featured" sí lo conserva.) */
.gh-mist::before,
.gh-mist::after {
  content: none !important;
  display: none !important;
  background: none !important;
}

/* ZONA BLANCA PURA: la referencia no tiene vignette ni granos — mientras
   las secciones blancas dominan el viewport (html.gh-white-zone, lo pone
   premium.js), TODAS las capas cinematográficas fijas del tema se
   desvanecen (incluidas las bandas .lb, que viven en z-index 91). */
.lb, .veil, .spot, .grain, .grain-film, .mottle, .aura, #scrollBar {
  transition: opacity 0.6s ease;
}
html.gh-white-zone .lb,
html.gh-white-zone .veil,
html.gh-white-zone .spot,
html.gh-white-zone .grain,
html.gh-white-zone .grain-film,
html.gh-white-zone .mottle,
html.gh-white-zone .aura,
html.gh-white-zone #scrollBar {
  opacity: 0 !important;
}
.gh-mist-head { text-align: center; margin-bottom: 9vh; }
.gh-mist-head .gh-st-eyebrow { color: #5c5c66; }
.gh-mist-flow {
  max-width: 1320px;
  margin: 0 auto;
  padding: 0 clamp(20px, 4.5vw, 64px);
  display: flex;
  flex-direction: column;
  gap: 11vh;
}
/* Tres tamaños con el ritmo exacto de la referencia (S 439 · L 586 · M 488
   sobre 1440 → 30.5 / 40.7 / 33.9 de altura equivalente), y la PROPORCIÓN
   se adecua a la foto (fotogramas verticales 786×1400). La niebla escala
   con cada tamaño (≈ blur 39px + spread 17.8% del ancho, como el original).
   Entrada por opacidad (.gh-on) — el transform es del parallax, sin pelea. */
.gh-mist-item {
  position: relative;
  display: block;
  aspect-ratio: 480 / 854;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  text-align: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  will-change: opacity;
  opacity: 0;
  transition: opacity 1.2s cubic-bezier(0.23, 1, 0.32, 1);
  /* SIN overflow:hidden, SIN border-radius, SIN fondo. El difuminado del
     borde lo hace una MÁSCARA sobre la propia imagen (bordes transparentes
     de verdad) → integración total con el blanco, imposible que haya línea. */
}
.gh-mist-item.gh-on { opacity: 1; }
/* Auditoría awwwards #3 — foco de teclado + feedback de presión en la
   constelación (los 16 reels reales): era la ÚNICA sección sin ninguno.
   Foco en el item (anillo ámbar del ramp); presión en el media estático
   (no pelea con el transform que el JS escribe en el botón central). */
/* #7 — cada reel descansa DESATURADO (log) y revela COLOR pleno al centrarse
   en el viewport (o al hover): el portafolio se grada pieza a pieza. El JS
   (premium.js) pone .gh-incolor con un IntersectionObserver de banda central. */
.gh-mist-media {
  transition: transform 0.2s var(--ease-exit);
  filter: none;  /* color normal siempre (Ginder: quitar el blanco y negro) */
}
.gh-mist-item.gh-incolor .gh-mist-media { filter: none; }
.gh-mist-item:focus-visible {
  outline: 2px solid var(--grade-warm);
  outline-offset: 6px;
  border-radius: 2px;
}
.gh-mist-item:active .gh-mist-media { transform: scale(0.972); }
.gh-mist-item:active .gh-mist-btn { opacity: 0.82; }
@media (hover: hover) and (pointer: fine) {
  .gh-mist-item:hover .gh-mist-media { filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .gh-mist-media { transition: none; filter: none; }
  .gh-mist-item:active .gh-mist-media { transform: none; }
}
/* Botón central (réplica del Abtn de la referencia): píldora glass gris
   30% + blur 5px, radius 39px, siempre visible, sigue al cursor con más
   recorrido que la imagen (profundidad). */
.gh-mist-btn {
  position: absolute;
  z-index: 4;
  top: 50%;
  left: 50%;
  margin: -24px 0 0 0;
  transform: translate(-50%, 0);
  display: inline-flex;
  align-items: center;
  gap: 12px;
  height: 48px;
  padding: 0 24px;
  border-radius: 39px;
  background: rgba(128, 128, 128, 0.3);
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  color: #f6f6f8;
  font: 500 14px/1 "Inter", system-ui, sans-serif;
  letter-spacing: 0.02em;
  white-space: nowrap;
  pointer-events: none;
  will-change: transform;
}
.gh-mist-btn b { font-weight: 600; }
.gh-mist-btn i {
  font-style: normal;
  font-family: "Space Mono", ui-monospace, monospace;
  font-size: 12px;
  opacity: 0.75;
}
/* CONSTELACIÓN EDITORIAL: filas de 3 verticales con ritmo S·L·M y desfases
   estáticos (nunca cuadrícula), 2 respiros anchos alternando lado, cierre
   de 2 centradas. Los items son <button> (portafolio real, spec 2026-07-15). */
.gh-mist-row {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(32px, 6vw, 84px);
}
.gh-mr-1 > :nth-child(1) { margin-top: 24px; }
.gh-mr-1 > :nth-child(3) { margin-top: 40px; }
.gh-mr-2 > :nth-child(1) { margin-top: 12px; }
.gh-mr-2 > :nth-child(2) { margin-top: 48px; }
.gh-mr-3 > :nth-child(2) { margin-top: 36px; }
.gh-mr-3 > :nth-child(3) { margin-top: 20px; }
.gh-mr-4 > :nth-child(1) { margin-top: 32px; }
.gh-mr-4 > :nth-child(3) { margin-top: 24px; }
.gh-mr-fin > :nth-child(1) { margin-top: 28px; }
.gh-ms-s { height: min(34vw, 500px); }
.gh-ms-m { height: min(38vw, 560px); }
.gh-ms-l { height: min(43vw, 630px); }
.gh-mw-a { width: min(62vw, 900px); height: auto; aspect-ratio: 960 / 538; align-self: flex-start; margin-left: clamp(16px, 6vw, 90px); }
.gh-mw-b { width: min(66vw, 940px); height: auto; aspect-ratio: 960 / 456; align-self: flex-end; margin-right: clamp(16px, 6vw, 90px); }
/* DIFUMINADO/GLOW FIEL A espaciolanube — UNA sola máscara hace todo el trabajo.
   Un SVG con desenfoque gaussiano (feGaussianBlur) sobre un rectángulo de
   esquinas muy redondeadas: sus bordes se degradan hasta transparente en una
   transición MUY LARGA (stdDeviation alto = glow), así el blanco del fondo
   entra suavísimo y "arropa" la imagen — el borde NO se marca, se funde como
   un resplandor. Al ser un único mecanismo (no capas apiladas que no casan) es
   imposible que aparezcan anillos ni líneas. La FOTO va nítida (sin blur): el
   usuario aclaró que el "difuminado" es SOLO el degradado del borde, no la
   imagen. La máscara comparte el aspect 786/1400 del marco → escala sin
   distorsión y el glow crece proporcional en cada tamaño. Imagen ESTÁTICA.
   PERILLAS de calibración: stdDeviation (largo del glow) + rect inset x/y
   (dónde empieza a fundirse) en el data-uri de abajo. */
.gh-mist-media {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 561 1000' preserveAspectRatio='none'%3E%3Cdefs%3E%3Cfilter id='b' x='-80%25' y='-80%25' width='260%25' height='260%25'%3E%3CfeGaussianBlur stdDeviation='60'/%3E%3C/filter%3E%3C/defs%3E%3Crect x='124' y='124' width='313' height='752' rx='155' fill='%23fff' filter='url(%23b)'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 561 1000' preserveAspectRatio='none'%3E%3Cdefs%3E%3Cfilter id='b' x='-80%25' y='-80%25' width='260%25' height='260%25'%3E%3CfeGaussianBlur stdDeviation='60'/%3E%3C/filter%3E%3C/defs%3E%3Crect x='124' y='124' width='313' height='752' rx='155' fill='%23fff' filter='url(%23b)'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}
/* Anchos: la MISMA receta de niebla transpuesta (σ60, inset 124 ≈ 2σ,
   rx155 — calibración anti-borde intacta), viewBox horizontal. */
.gh-mw-a .gh-mist-media,
.gh-mw-b .gh-mist-media {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 561' preserveAspectRatio='none'%3E%3Cdefs%3E%3Cfilter id='b' x='-80%25' y='-80%25' width='260%25' height='260%25'%3E%3CfeGaussianBlur stdDeviation='60'/%3E%3C/filter%3E%3C/defs%3E%3Crect x='124' y='124' width='752' height='313' rx='155' fill='%23fff' filter='url(%23b)'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 561' preserveAspectRatio='none'%3E%3Cdefs%3E%3Cfilter id='b' x='-80%25' y='-80%25' width='260%25' height='260%25'%3E%3CfeGaussianBlur stdDeviation='60'/%3E%3C/filter%3E%3C/defs%3E%3Crect x='124' y='124' width='752' height='313' rx='155' fill='%23fff' filter='url(%23b)'/%3E%3C/svg%3E");
}
/* Video vivo en hover: el iframe vive DENTRO de .gh-mist-media → hereda la
   máscara; el video se reproduce dentro de la niebla. Lo inyecta premium.js. */
.gh-mist-media .gh-live {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  opacity: 0;
  transition: opacity 0.45s ease;
  pointer-events: none;
}
.gh-mist-media .gh-live.on { opacity: 1; }

/* --- 3c) ENTRADA DE TIPOGRAFÍA POR PALABRAS (espaciolanube):
       las palabras suben y se enfocan en cascada. premium.js parte el
       texto en .gh-w; el observer del tema añade .on y dispara todo. ----- */
.gh-split { opacity: 1 !important; transform: none !important; filter: none !important; }
.gh-split .gh-w {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.55em);
  filter: blur(7px);
  transition: opacity 0.7s var(--ease, ease), transform 0.7s var(--ease, ease), filter 0.7s var(--ease, ease);
  transition-delay: calc(var(--i, 0) * 0.045s);
  will-change: transform, filter;
}
.gh-split.on .gh-w { opacity: 1; transform: none; filter: none; }
/* Si el titular tiene <em> con degradado, el clip de texto debe vivir en
   cada palabra (los spans inline-block rompen el clip del padre). */
.gh-split em { background: none !important; }
.gh-split em .gh-w {
  background: var(--grad, linear-gradient(108deg, #ffd9a0, #ff9b5e 30%, #ff5fa2 56%, #a78bff 80%, #7fb4e8));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
@media (prefers-reduced-motion: reduce) {
  .gh-split .gh-w { transition: opacity 0.3s ease; transform: none; filter: none; }
}

/* --- 4) AMBILIGHT DEL MODAL (espaciolanube) ----------------------------- */
.gh-glow {
  position: absolute;
  z-index: 0;
  inset: 8%;
  border-radius: 40px;
  background-size: cover;
  background-position: center;
  /* [20-jul-2026] SIN filter en vivo. El blur(64px) creaba una superficie de
     filtro gigante solapando el vídeo (que se compone en una overlay de
     hardware) y contribuía al camino frágil del compositor. El desenfoque y la
     saturación ahora vienen PRE-RASTERIZADOS: premium.js reduce el póster a una
     miniatura en un canvas (con el color ya horneado) y el estirado del GPU
     hace de gaussiana gratis. Mismo aspecto, cero filtro en composición. */
  filter: none;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 0.8s ease 0.15s;
  pointer-events: none;
}
.vmodal.open .gh-glow { opacity: 0.48; }

/* Glow cálido (halation) alrededor del showreel expandido y del reel del modal:
   al abrir, el vídeo "enciende" e irradia luz cálida (no rosa). */
/* [C1 · 19-jul-2026] + filo cálido de 1px, para que el fotograma pequeño del
   paréntesis se lea como un PLANO y no como un hueco. Tono papel/cálido, nunca
   un hairline negro. Las dos sombras con offset 0. */
.reel-video.float {
  box-shadow:
    0 0 0 1px rgba(120, 80, 50, 0.14),
    0 0 120px -18px rgba(255, 150, 90, 0.38);
}
.reel-modal-video { border-radius: 24px;
  box-shadow:
    0 0 140px -20px rgba(255, 120, 60, 0.36),
    0 0 90px -30px rgba(255, 106, 51, 0.24);
}
/* El vídeo del portafolio es un IFRAME (Vimeo): su vídeo interno se compone en
   una OVERLAY de hardware que ignora el border-radius/mask del contenedor Y del
   propio iframe → esquinas cuadradas que parpadeaban en reposo. FIX real:
   CLIP-PATH en el PROPIO IFRAME (recorte GEOMÉTRICO, no una pista de composición
   como el mask): el compositor lo respeta siempre → esquinas redondas y estables
   en TODOS los reels, con el cursor quieto o en movimiento. */
/* IMPORTANTE: el tema da a .vm-frame `position:absolute; inset:0` para que LLENE
   el .vmodal-inner (y el iframe hereda width/height:100%). Aquí solo AÑADIMOS el
   border-radius y conservamos absolute+inset:0 — si se cambia a `position:relative`
   el marco colapsa a la altura por defecto del iframe (150px) y el vídeo se ve
   recortado en una franja superior. absolute también ancla el ::after de esquinas. */
.vm-frame { position: absolute; inset: 0; border-radius: 34px; }
.vm-frame iframe { border-radius: 34px; }
/* SOLUCIÓN GARANTIZADA para el parpadeo de esquinas: Chrome desktop compone el
   vídeo del iframe (Vimeo) en una overlay de hardware que ignora cualquier
   recorte (border-radius/clip-path/mask/transform/filter — todo probado, todo
   falla). En vez de recortar, se PINTAN 4 piezas de esquina POR ENCIMA del vídeo
   (::after, z-index alto) que tapan las puntas cuadradas con el color del fondo
   del modal → redondeo perceptual. Al ser un elemento normal encima, NO depende
   de la composición del vídeo → estable, sin parpadeo, en todos los reels. */
.vm-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  --vr: 34px;
  --vc: #191410;
  background:
    radial-gradient(circle var(--vr) at var(--vr) var(--vr), transparent calc(var(--vr) - 1px), var(--vc) var(--vr)) 0 0 / var(--vr) var(--vr) no-repeat,
    radial-gradient(circle var(--vr) at 0 var(--vr), transparent calc(var(--vr) - 1px), var(--vc) var(--vr)) 100% 0 / var(--vr) var(--vr) no-repeat,
    radial-gradient(circle var(--vr) at var(--vr) 0, transparent calc(var(--vr) - 1px), var(--vc) var(--vr)) 0 100% / var(--vr) var(--vr) no-repeat,
    radial-gradient(circle var(--vr) at 0 0, transparent calc(var(--vr) - 1px), var(--vc) var(--vr)) 100% 100% / var(--vr) var(--vr) no-repeat;
}

/* --- 5) PROCESO 01–04: RESTAURADO a la versión ORIGINAL (tarjetas glass
   alternadas + spine luminosa central sobre fondo oscuro). Se deshace el
   rediseño editorial del Round 6 y la rueda del Round 11 → mandan los
   estilos originales del tema (.act, .act-card.glass, .spine, #spineFill). */
.act-card .num { letter-spacing: 0.14em; }

/* En las secciones claras nada cambia (los actos viven sobre fondo oscuro) */

/* =========================================================================
   Enlaces a artículos sobre el footer (SEO interno home→artículos).
   Sin media query: se ve bien en todos los tamaños.
   ========================================================================= */
.dt-articles {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 9px 22px;
  max-width: 1000px;
  margin: 0 auto;
  padding: 40px 26px 4px;
}
.dt-articles a {
  color: var(--dim, #9a9aa6);
  font-size: 13px;
  letter-spacing: 0.01em;
  text-decoration: none;
  transition: color 0.25s ease;
}
.dt-articles a:hover { color: var(--snow, #f6f6f8); }

/* [nota conservada] La spine del journey está RESTAURADA (línea luminosa
   central, la anima el JS del tema con #spineFill): ya no se oculta.
   El fondo de la zona del proceso es #dbd8cf, idéntico a "Cómo trabajamos"
   (.intro), para que se lea como una sola sección. (La RUEDA VERTICAL que
   vivía aquí se eliminó del HTML; su CSS se ha borrado.) */
/* =====================================================================
   SHOWREEL del hero. La ANIMACIÓN del reel-float (el vídeo crece → sube →
   desaparece por arriba tras el menú) es del tema y queda INTACTA. Como el
   reel-float cuelga el vídeo del <body> en position:fixed (z-index 25) por
   encima del contenedor de contenido (.shell, z-index 2), no se puede tapar
   con z-index; para que no se asome sobre la sección gris (.intro) por abajo,
   premium.js (módulo 6) RECORTA con clip-path la parte del vídeo que caería
   sobre la .intro, en su borde superior. */

/* ═══════════ GRANO ANALÓGICO (mason-wong, medido en vivo) ═══════════
   Capa fija de ruido por ENCIMA de toda la página (z-index 125: sobre
   contenido, letterbox z91 y scrollbar z120; bajo el cursor z130). La
   textura 256×256 y el salto ~11fps los genera premium.js (módulo 8).
   El blanco lee ligeramente gris — es el efecto analógico buscado.
   NO se toca el resto del diseño: solo se AÑADE esta capa. */
.gh-grain {
  position: fixed;
  inset: -8% 0;
  z-index: 125;
  pointer-events: none;
  opacity: 0.09;
  transition: opacity 0.25s ease;
  /* EMULSIÓN: el grano deja de "flotar" encima y se integra en la química
     (overlay = grano de emulsión, no ruido plano). Subido (.05→.09) para más
     presencia de grano en toda la web, como la referencia. */
  mix-blend-mode: overlay;
  overflow: hidden;
}
.gh-grain > div {
  position: absolute;
  inset: -256px;
  background-repeat: repeat;
  opacity: 0.9;
  will-change: transform;
}

/* ═══════════ ATMÓSFERA DE SECCIÓN (solo comparador + contacto) ═══════════
   Réplica del fondo de "Cómo trabajamos": lavanda frío arriba → glow
   melocotón desde ABAJO que respira y SIGUE al cursor (muy sutil). La
   estructura la inyecta atmos.js en .cgrade y .contact. El resto de la
   web NO se toca (hero blanco hueso, etc.). Grano: el global .gh-grain. */
.gh-secatmos {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
  background:
    radial-gradient(64% 54% at 18% 36%, #d1d4e0, rgba(209,212,224,0) 60%),
    radial-gradient(96% 60% at 50% 0%,  #dfdde5, rgba(223,221,229,0) 70%),
    linear-gradient(to bottom, #dcdae2 0%, #d7d5dd 28%, #dcc4a8 62%, #e8c497 100%);
}
.gh-secatmos .gh-glow-wrap { position: absolute; inset: 0; will-change: transform; }
.gh-secatmos .gh-glow {
  position: absolute; inset: -6% -5% -12% -5%; will-change: transform, opacity;
  background:
    radial-gradient(70% 56% at 47% 106%, rgba(237,197,150,0.98), rgba(237,197,150,0) 68%),
    radial-gradient(56% 46% at 17% 102%, rgba(231,179,131,0.90), rgba(231,179,131,0) 64%),
    radial-gradient(54% 42% at 82% 104%, rgba(234,191,146,0.85), rgba(234,191,146,0) 66%);
  animation: ghGlowBreathe 17s ease-in-out infinite alternate;
}
@keyframes ghGlowBreathe {
  0%   { transform: translate3d(0,0,0) scale(1);        opacity: .92; }
  50%  { transform: translate3d(-1.2%,-2%,0) scale(1.05); opacity: 1; }
  100% { transform: translate3d(1.4%,-1%,0) scale(1.03);  opacity: .95; }
}
@media (prefers-reduced-motion: reduce) { .gh-secatmos .gh-glow { animation: none; } }
/* Secciones con atmósfera: contenido por encima; fuera el glow azul/rosa del tema */
.cgrade.gh-has-atmos, .contact.gh-has-atmos { position: relative; overflow: hidden; isolation: isolate; }
.cgrade.gh-has-atmos > *:not(.gh-secatmos),
.contact.gh-has-atmos > *:not(.gh-secatmos) { position: relative; z-index: 1; }
.cgrade.gh-has-atmos::before, .cgrade.gh-has-atmos::after,
.contact.gh-has-atmos::before, .contact.gh-has-atmos::after { content: none !important; background: none !important; }

/* ==========================================================================
   #8 — PROCESO ELEVADO: numeral gigante en contorno que sangra al margen
   exterior (editorial/adcker), teñido por paso FRÍO→CÁLIDO siguiendo el arco
   log→color. Sin tocar la spine (sigue intacta). El grade global (#5) además
   los desatura arriba y los satura al bajar → el proceso "gana color".
   ========================================================================== */
.journey .act { position: relative; }
.journey .act-card { position: relative; z-index: 1; }
.journey .act.left::before { left: clamp(-24px, 1vw, 20px); }
.journey .act.right::before { right: clamp(-24px, 1vw, 20px); }
/* Banda-puente en el TOP del about (el ::after del mist está bloqueado por la
   regla de "blanco puro" de la Round 9). El top del about arranca en el mismo
   papel cálido del portafolio y se disuelve al oscuro → sin corte seco. */
.about { position: relative; }
/* (Degradado-puente eliminado por petición de Ginder: el about ahora vive sobre
   la foto del mar, no sobre oscuro; el fundido a oscuro ya no aplica.) */

/* ==========================================================================
   #22 — PULIDO TIPOGRÁFICO (home): saltos de línea equilibrados + numeración
   tipo timecode (motivo de colorista). Solo home (no cargan en los artículos).
   ========================================================================== */
h2, h3, .about-txt h2 { text-wrap: balance; }
.sub, .about-txt p, .intro .fp, .cg-sub { text-wrap: pretty; }
.num, .eyebrow, .gh-st-eyebrow,
.gh-mist-btn i, .cg-tag, .gh-rail {
  font-variant-numeric: tabular-nums slashed-zero;
}

/* #24 — franja de artículos como ÍNDICE editorial numerado en mono (no una
   fila de enlaces suelta colgando entre contacto y footer). */
.dt-articles { counter-reset: gh-art; }
.dt-articles a::before {
  counter-increment: gh-art;
  content: "0" counter(gh-art) "  ";
  font-family: "Space Mono", ui-monospace, monospace;
  font-size: 0.82em;
  opacity: 0.5;
  letter-spacing: 0.04em;
}

/* ==========================================================================
   FONDO FOTO compartido para ABOUT + CONTACTO (mar al atardecer, tal cual,
   SIN scrim). La misma foto abarca las dos secciones (una imagen continua).
   ========================================================================== */
/* [1-ago-2026] La foto del mar (252 KB en AVIF) es el fondo de about+contacto,
   o sea EL FINAL de la página, pero al declararla aquí el navegador la pedía en
   la carga inicial compitiendo con el CSS y las fuentes. Ahora la imagen solo se
   pide cuando la zona se acerca (la clase .gh-sea-on la pone premium.js con un
   IntersectionObserver de 700px de margen: llega de sobra antes de verse).
   El color de fondo se queda desde el principio, así que nunca hay un hueco. */
.gh-photo-zone {
  position: relative;
  background-color: #14110d;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.gh-photo-zone.gh-sea-on {
  background-image: url(/img/sea.jpg);
  background-image: image-set(url(/img/sea.avif) type("image/avif"), url(/img/sea.jpg) type("image/jpeg"));
}
.gh-photo-zone .about,
.gh-photo-zone .contact { background: transparent; overflow: visible; }
.gh-photo-zone .about .wrap,
.gh-photo-zone .contact .wrap { position: relative; z-index: 2; }
/* Contacto: texto CÁLIDO claro (no blanco puro), sin scrim.
   [A2 · 19-jul-2026] ELIMINADA la `text-shadow: 0 2px 30px rgba(0,0,0,.5)`.
   Era una sombra NEGRA con desplazamiento vertical sobre el titular más grande
   de la sección — viola la regla absoluta del proyecto (cero sombras en texto).
   La legibilidad la da ahora el propio tono: hueso más luminoso sobre el agua
   profunda del atardecer, que en esa zona ya es oscura. Si algún día hiciera
   falta más contraste: densificar la FOTO bajo el bloque (contenedor), nunca
   volver a poner sombra al texto. */
.gh-photo-zone .contact h2 { color: #fff6ea; text-shadow: none; }
.gh-photo-zone .contact .link.glass { color: #f4ead9; }

/* =========================================================================
   [C1 · 19-jul-2026] EL FOTOGRAMA DEL PARÉNTESIS (escritorio ≥761px)
   -------------------------------------------------------------------------
   El tema dejaba el disparador en .28em × .37em (≈42×55px con el h1 a 150px):
   se leía como una mancha de carga fallida, no como un plano de cine.

   POR QUÉ .66em NO EMPUJA LA LÍNEA (medido en el woff2 con fontTools):
     Bricolage Grotesque · upm 1000 · asc .93 · desc .27 · x-height .528 · cap .66
     .hero.hero-light h1 → line-height .96 → medio interlineado (.96-1.20)/2 = -.12em
     La caja de línea llega a .81em POR ENCIMA y .15em POR DEBAJO de la base.
     .reel-thumb es inline-block con vertical-align:middle → su centro cae a
     x-height/2 = .264em sobre la base. De ahí:
         arriba: .264 + H/2 <= .81  → H <= 1.092em
         abajo:  H/2 - .264 <= .15  → H <= .828em   ← TECHO REAL
     .66em = cap-height exacto; sobresale .066em y deja .084em de margen. Y como
     el tema ya aplica translateY(-.06em), queda ópticamente a ras de base y a
     ras de caja. No hay que tocar el transform del tema.

   POR QUÉ 4:5 Y NO 3:2: el teaser es 720×1280 y .reel-video lleva
   object-fit:cover. Un recorte 3:2 conserva solo el 37,5% central de la altura
   → decapita 4 de los 7 planos del teaser. 4:5 conserva el 70%: las caras
   entran en 6 de 7. Y al hacer scroll, el flotante se reencuadra de 4:5 a 9:16
   de forma continua.
   ========================================================================= */

/* Devuelve el bloque contenedor al botón. Sin esto, con prefers-reduced-motion
   el bundle del tema hace return ANTES de sacar el <video> al <body>: el vídeo
   conserva position:absolute;inset:0 y, como .reel-thumb es static en
   escritorio, se estiraba sobre TODO el <h1>. Bug preexistente. */
.reel-thumb { position: relative; }



/* ==========================================================================
   [20-jul-2026] EL PARPADEO DEL VÍDEO DEL MODAL — la causa de verdad.
   --------------------------------------------------------------------------
   Síntoma (Ginder): con un reel reproduciéndose y el cursor QUIETO, primero
   parpadea una línea oscura a la altura de los controles y luego las esquinas,
   constantemente; al mover el cursor se detiene. Empezó al subir el grano.

   Causa: el modal vive en z-index 82 y el grano global (.gh-grain) en z-index
   125 — POR ENCIMA del vídeo — con mix-blend-mode:overlay y un salto cada
   90ms. El vídeo del iframe se compone en una overlay de HARDWARE, y un blend
   por encima no se puede componer contra ella: cada salto del grano obliga al
   compositor a re-decidir el camino de pintado del vídeo y alterna entre dos
   → parpadeo a ~11Hz. Mover el cursor genera repintados continuos que lo fijan
   en un solo camino → se detiene (exactamente el síntoma).

   Fix: mientras hay un modal abierto, el grano se apaga (opacity 0 + su
   setInterval se pausa en premium.js). Sobre el vídeo no pinta nada que valga
   la pena: el material ya lleva su propio grano de cámara.
   ========================================================================== */
body:has(.vmodal.open) .gh-grain,
body:has(.modal.open) .gh-grain{
  opacity: 0;
}

/* Las piezas de esquina, en su PROPIA capa de composición: así el compositor
   las ordena siempre por encima de la overlay del vídeo, pase lo que pase. */
.vm-frame::after{
  will-change: transform;
  transform: translateZ(0);
}

/* El vídeo no debe aparecer ANTES que el modal: la overlay ignora la opacidad
   del ancestro durante la animación de apertura (modalPop) y el vídeo
   "reventaba" a tamaño completo sobre el fundido. Con opacity<1 durante esa
   ventana se fuerza composición normal (que sí respeta el fundido); al llegar
   a 1, vuelve la overlay y el rendimiento de siempre. */
@keyframes ghIframeIn{
  0%{opacity:0}
  70%{opacity:0.35}
  99%{opacity:0.999}
  100%{opacity:1}
}
.vmodal.open .vm-frame iframe{
  animation: ghIframeIn 0.55s cubic-bezier(0.22,1,0.36,1) both;
}


/* ==========================================================================
   [20-jul-2026 · 2ª tanda contra el parpadeo] Los dos gatillos que quedaban.
   --------------------------------------------------------------------------
   1) EL "FONDO BLUR" DEL MODAL. El tema le pone al velo del modal un
      backdrop-filter:blur(16px) A PANTALLA COMPLETA: un desenfoque en vivo de
      todo lo que hay detrás, que se re-evalúa con cada actualización de la
      zona — incluida cada frame del vídeo. Fuera. Se compensa con un velo un
      punto más denso: detrás del modal apenas se veía nada de todos modos, y
      el halo del ambilight sigue dando la profundidad.
   2) LA OVERLAY DE HARDWARE, DESACTIVADA DE RAÍZ. Todo el historial de este
      bug (border-radius, clip-path, mask, piezas de esquina…) son intentos de
      RECORTAR un vídeo que vive en una overlay que ignora los recortes. Las
      overlays solo admiten rectángulos ALINEADOS CON LOS EJES: una rotación de
      0.03° (≈medio píxel en el borde, invisible) hace al iframe inelegible
      para la overlay → el vídeo se compone por el camino normal, que respeta
      el border-radius SIEMPRE. Un solo camino de pintado = nada que alternar
      = nada que parpadear. Es el único enfoque que ataca la causa y no el
      síntoma.
   ========================================================================== */
.vmodal{
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
  background: rgba(4, 4, 7, 0.90) !important;
}
.vm-frame iframe{
  transform: rotate(0.03deg);
}

/* ==========================================================================
   [HOJA · 20-jul-2026] MODO «HOJA DE CONTACTOS» DEL PORTAFOLIO
   Spec: docs/superpowers/specs/2026-07-20-portafolio-hoja-de-contactos-design.md
   Dos modos de la MISMA sección: niebla (defecto — nada de este bloque
   aplica sin .gh-sheet) y hoja de contactos (grid por categorías; metáfora
   EMULSIÓN: la hoja de contactos del rollo de 35mm). Botón y separadores
   los inyecta premium.js. Los !important pisan a propósito las reglas
   móviles de mobile-opt.css (carga entre medias con sus propios !important
   de ancho y botón); ganan por especificidad (.gh-mist.gh-sheet …).
   ========================================================================== */
.gh-mist-head { position: relative; }
.gh-view-toggle {
  position: absolute;
  right: clamp(18px, 4.5vw, 64px);
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-width: 44px;
  min-height: 44px;
  padding: 0 13px;
  border: 1px solid rgba(92, 92, 102, 0.28);
  border-radius: 999px;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.gh-view-toggle svg { width: 15px; height: 15px; display: block; }
/* [JURADO · 20-jul] etiqueta visible: el botón de solo-glifos casi no se
   descubría; una palabra en mono lo hace legible sin romper el minimalismo. */
.gh-view-toggle .gh-vt-tx {
  font: 500 10px/1 "Space Mono", ui-monospace, monospace;
  letter-spacing: 0.2em;
  color: #5c5c66;
}
.gh-view-toggle circle { fill: #b9b9c0; transition: fill 0.25s ease; }
.gh-view-toggle .gh-vt-mist circle { fill: #43434c; }
.gh-mist.gh-sheet .gh-view-toggle .gh-vt-mist circle { fill: #b9b9c0; }
.gh-mist.gh-sheet .gh-view-toggle .gh-vt-grid circle { fill: #43434c; }

/* Separadores de categoría: solo existen visualmente en modo hoja. */
.gh-sheet-sep { display: none; }
.gh-mist.gh-sheet .gh-sheet-sep {
  display: block;
  order: var(--sheet-order, 0);
  grid-column: 1 / -1;
  font-family: "Space Mono", ui-monospace, monospace;
  font-size: 12px;
  letter-spacing: 0.22em;
  color: #5c5c66;
  margin: 30px 0 2px;
}

/* La cuadrícula: el flow pasa a grid, las filas se disuelven
   (display:contents) y cada pieza toma su casilla por --sheet-order
   (lo asigna premium.js). El order NO aplica en modo niebla. */
.gh-mist.gh-sheet .gh-mist-flow {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 20px 14px;
  padding: 0 18px;
}
.gh-mist.gh-sheet .gh-mist-row { display: contents; }
.gh-mist.gh-sheet .gh-mist-item {
  order: var(--sheet-order, 99);
  width: auto !important;
  height: auto !important;
  aspect-ratio: 3 / 4 !important;
  margin: 0 !important;
  align-self: auto !important;
  opacity: 1 !important;
}
/* Fotograma nítido a celda completa: fuera la máscara de niebla; esquinas
   suaves pequeñas. overflow:hidden recorta también el iframe del
   hover-vivo (.gh-live). */
.gh-mist.gh-sheet .gh-mist-media {
  -webkit-mask-image: none !important;
  mask-image: none !important;
  border-radius: 8px;
  overflow: hidden;
}
/* Encuadres finos del recorte 3/4 (petición de Ginder: que se vea el
   rostro centrado). Solo afectan a la hoja; la niebla usa el center. */
.gh-mist.gh-sheet .gh-mist-item[data-vid="904843845"] .gh-mist-media { background-position: center 22%; }
.gh-mist.gh-sheet .gh-mist-item[data-vid="1207195205"] .gh-mist-media { background-position: center 18%; }
/* La píldora ORIGINAL (glass, centrada sobre el vídeo, nombre + número),
   compactada para las celdas de la hoja. Hereda todo del pill base
   (top 50%, glass, blur, imán del cursor en escritorio); aquí solo se
   reduce y se recorta el nombre largo con elipsis. Los !important pisan
   los ajustes móviles de mobile-opt.css (40px/12px), que son de la niebla. */
.gh-mist.gh-sheet .gh-mist-btn {
  height: 28px !important;
  margin: -14px 0 0 !important;
  padding: 0 11px !important;
  gap: 6px !important;
  font-size: 10px !important;
  max-width: calc(100% - 12px);
  /* [JURADO · 20-jul] contraste AA: el glass gris al 30% daba ~2,2-2,8:1
     sobre pósters claros en celdas pequeñas; en la HOJA la píldora se
     oscurece (≈5:1 con el texto #f6f6f8). La niebla conserva su glass. */
  background: rgba(28, 28, 33, 0.68) !important;
}
.gh-mist.gh-sheet .gh-mist-btn b {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gh-mist.gh-sheet .gh-mist-btn i { font-size: 9px !important; }

/* Fundido corto del cambio de modo en móvil (el FLIP es de escritorio). */
.gh-mist-flow { transition: opacity 0.28s ease; }
.gh-mist-flow.gh-sheet-fade { opacity: 0; }

/* [JURADO · 20-jul] MARCO EMULSIÓN DEL REPRODUCTOR: línea de metadata mono
   BAJO la tarjeta del modal — número y título del reel + firma de la casa,
   como el pie de una copia de laboratorio. Vive sobre el fondo oscuro del
   backdrop, NUNCA sobre el vídeo: el pipeline anti-parpadeo queda intacto.
   La posiciona premium.js al abrir (la tarjeta recorta con overflow:hidden,
   por eso la línea es hermana suya dentro del propio modal). */
.gh-vm-meta {
  position: absolute;
  z-index: 3;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 18px;
  font-family: "Space Mono", ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: 0.16em;
  color: rgba(214, 214, 222, 0.82);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.35s ease;
  pointer-events: none;
}
.gh-vm-meta.on { opacity: 1; }
.gh-vm-meta .gh-vm-tt { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.gh-vm-meta .gh-vm-num { opacity: 0.6; }
@media (max-width: 760px) {
  .gh-vm-meta { font-size: 10px; letter-spacing: 0.1em; }
  .gh-vm-meta .gh-vm-brand { display: none; }
}

/* Escritorio: 6 columnas, más respiración y píldora un punto mayor. */
@media (min-width: 761px) {
  .gh-mist.gh-sheet .gh-mist-flow {
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 26px 18px;
    padding: 0 clamp(20px, 4.5vw, 64px);
  }
  .gh-mist.gh-sheet .gh-sheet-sep { margin: 40px 0 4px; }
  .gh-mist.gh-sheet .gh-mist-btn {
    height: 30px !important;
    margin: -15px 0 0 !important;
    padding: 0 13px !important;
    font-size: 11px !important;
  }
  .gh-mist.gh-sheet .gh-mist-btn i { font-size: 10px !important; }
}

/* ==========================================================================
   [31-jul-2026] LA FRANJA NEGRA DE LA DERECHA — desbordamiento horizontal.
   --------------------------------------------------------------------------
   Síntoma (Ginder, en su móvil): una banda negra pegada al borde derecho en
   toda la web, como si el diseño no estuviera centrado.

   Causa medida: al hacer scroll, el tema le pone al hero las clases `exp gone`
   y DISPERSA las palabras del titular con transform (±175,5 px fijos). En
   escritorio sobra sitio; en un móvil de 390 px el «)» acaba en x=486 → el
   documento pasa a medir 486 px de ancho y esos 96 px de más muestran el fondo
   del body, que es casi negro (--ink #08090e). Exactamente la franja.

   El tema ya trae `body{overflow-x:hidden}`, pero el `html` se queda en
   `visible`: esa combinación depende de la propagación del overflow al
   viewport, que **Safari de iPhone aplica de forma inconsistente** — por eso
   se ve en el móvil y no en el Chrome de escritorio.

   Fix: `overflow-x: clip` en html y body. Se usa `clip` y NO `hidden` a
   propósito: `hidden` crea un contenedor de scroll y ROMPERÍA el
   `position:sticky` del hero (la animación del showreel que sube y se va);
   `clip` recorta sin crear contenedor de scroll, así que el sticky sigue
   intacto. La animación de dispersión se conserva tal cual: simplemente ya no
   ensancha el documento.
   ========================================================================== */
html{ overflow-x: clip; }
body{ overflow-x: clip; }
