/* ============================================================
   marcos gordillo — portfolio
   FASE 1: tema claro, web-juguete, cursor propio, loader.
   Filosofía (no copia) inspirada en bierman.io.
   ============================================================ */

:root {
  /* Paleta clara / crema */
  --bg:        #f8f8f4;   /* blanco cálido */
  --bg-2:      #f0f0e8;
  --ink:       #16160f;   /* casi negro cálido */
  --muted:     #565648;
  --line:      #e0e0d4;
  --accent:    #2e46ff;   /* azul eléctrico */
  --accent-2:  #6d7bff;

  /* Tipografía */
  --font-display: "Bricolage Grotesque", system-ui, sans-serif;
  --font-body:    "Inter", system-ui, sans-serif;
  --font-mono:    "JetBrains Mono", ui-monospace, monospace;

  --maxw: 1200px;
  --gutter: clamp(20px, 5vw, 60px);
  --radius: 12px;
}

/* ===== Reset ===== */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--bg);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
/* El bloqueo de scroll SOLO se aplica con JS activo (que es quien lo retira
   al entrar). Sin JS, el body nunca queda atrapado en overflow:hidden. */
.js body.is-loading { overflow: hidden; height: 100vh; }

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; padding: 0; }

.container {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.mono { font-family: var(--font-mono); font-weight: 400; }

.kicker {
  font-size: 0.76rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 22px;
}
.kicker::before { content: "( "; }
.kicker::after  { content: " )"; }

/* ============================================================
   CURSOR PERSONALIZADO
   Se activa por JS (body.has-cursor) solo en punteros finos.
   ============================================================ */
.cursor {
  position: fixed;
  top: 0; left: 0;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: #ffffff;
  mix-blend-mode: difference;   /* visible sobre cualquier fondo */
  pointer-events: none;
  z-index: 9999;
  transform: translate(-50%, -50%);
  transition: width 0.22s ease, height 0.22s ease;
  opacity: 0;
}
body.has-cursor { cursor: none; }
body.has-cursor a,
body.has-cursor button,
body.has-cursor [data-cursor] { cursor: none; }
.cursor.is-active { opacity: 1; }
.cursor.is-hover { width: 46px; height: 46px; }

/* ============================================================
   LOADER / PANTALLA DE ENTRADA
   Oculto por defecto; solo visible con JS (progressive enhancement).
   ============================================================ */
.loader { display: none; }

.js .loader {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 26px;
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: var(--bg);
  transition: opacity 0.7s ease, visibility 0.7s ease;
}
.js .loader.is-hidden { opacity: 0; visibility: hidden; }

.loader__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}
.loader__count {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(4rem, 18vw, 11rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* (El loader ya no tiene botón ni hint: la entrada es automática al 100%.) */

/* ============================================================
   MARCA MÍNIMA (la nav se eliminó; contacto vive en el footer)
   Monograma "mgb" fijo, discreto, vuelve al inicio.
   ============================================================ */
.mark {
  position: fixed;
  top: 18px;
  left: clamp(20px, 4vw, 44px);
  z-index: 40;
  font-weight: 500;
  font-size: 0.95rem;
  letter-spacing: 0.05em;
  color: var(--ink);
  background: none;
  border: none;
  padding: 6px 8px;
  cursor: pointer;
  transition: opacity 0.4s ease, color 0.2s ease;
}
.mark:hover { color: var(--accent); }
.mark:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* ============================================================
   HERO — el nombre ocupa casi todo el viewport
   ============================================================ */
.hero {
  position: relative;
  min-height: 92svh;   /* deja asomar la siguiente sección: hay más abajo */
  display: flex;
  align-items: center;
  padding-block: clamp(30px, 6vw, 70px);
}
.hero .container { width: 100%; }
/* Al entrar, el JS mueve el foco a este contenedor (tabindex="-1") para dejar
   el teclado dentro del contenido. El navegador dibujaría un recuadro de foco
   alrededor de TODO el hero: lo quitamos SOLO en este contenedor. Enlaces y
   botones conservan su :focus-visible intacto (ver .btn:focus-visible). */
.hero .container[tabindex="-1"]:focus,
.hero .container[tabindex="-1"]:focus-visible { outline: none; }
/* Invitación sutil al scroll: "scroll ↓" en mono, tinta al 50%, con un
   rebote mínimo y lento de la flecha. Se desvanece al primer scroll (JS)
   y no vuelve. */
.scroll-hint {
  position: absolute;
  left: var(--gutter);
  bottom: 26px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.76rem;
  letter-spacing: 0.14em;
  color: var(--ink);
  opacity: 0.5;
  transition: opacity 0.7s ease;
  pointer-events: none;
}
.scroll-hint__arrow {
  display: inline-block;
  animation: hint-bob 2.8s ease-in-out infinite;
}
@keyframes hint-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(4px); }
}
.scroll-hint.is-gone { opacity: 0; }

/* Indicador "click ↓" sobre el portátil: hermano visual del scroll-hint.
   Vive DENTRO de #labShared (sigue su posición en cada lado), centrado sobre
   el borde superior de la tapa, apuntando a la pantalla. No intercepta el
   ratón. Visible solo en reposo (clase .is-on, la pone el JS). */
.click-hint {
  position: absolute;
  bottom: calc(100% + 16px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.76rem;
  letter-spacing: 0.14em;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.7s ease;
  pointer-events: none;
}
.click-hint.is-on { opacity: 0.5; }
.click-hint__arrow {
  display: inline-block;
  animation: hint-bob 2.8s ease-in-out infinite;
}

.hero__eyebrow {
  font-size: clamp(0.72rem, 1.6vw, 0.9rem);
  letter-spacing: 0.1em;
  color: var(--muted);
  margin-bottom: clamp(14px, 2.5vw, 30px);
}

.hero__name {
  font-family: var(--font-display);
  font-weight: 800;
  /* Acotado también por ALTO de ventana: sin el término svh, en viewports bajos
     (ventana corta, portátil pequeño o zoom del navegador) el nombre crece más
     que el sticky de 100svh y su base la recorta el overflow:hidden de .zoom-sticky.
     El svh solo entra por debajo de ~650px de alto; en pantallas normales manda el
     ancho (21vw/17rem) y el tamaño no cambia. Mismo patrón que usa el portátil. */
  font-size: clamp(3.6rem, min(21vw, 44svh), 17rem);
  line-height: 0.82;
  letter-spacing: -0.045em;
  color: var(--ink);
  text-transform: lowercase;
}
.hero__name .word { display: block; }

/* Cada letra es un bloque independiente (preparado para arrastrar en Fase 2) */
.letter {
  display: inline-block;
  transform-origin: center bottom;
}
/* Estado inicial de entrada SOLO con JS: las letras aparecen al entrar */
.js .letter {
  opacity: 0;
  transform: translateY(0.42em) rotate(3deg);
}
/* Revelado inicial (una sola vez). will-change y transition SOLO existen
   durante esta fase; luego .letters-done los elimina para siempre. */
.js .letters-in .letter {
  opacity: 1;
  transform: none;
  will-change: transform;
  transition:
    opacity 0.5s ease,
    transform 0.7s cubic-bezier(0.18, 1.1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 0.045s);
}
/* Estado FINAL permanente: al terminar el revelado, el JS añade .letters-done
   al .hero__name y las letras quedan fijas — sin transition, sin transform, sin
   will-change — de modo que NADA puede volver a animarlas al reentrar el hero
   en el viewport (esta era la causa real del glitch de letras). */
.js .hero__name.letters-done .letter {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
  transition-delay: 0s !important;
  will-change: auto !important;
}

.hero__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(16px, 4vw, 40px);
  margin-top: clamp(26px, 5vw, 54px);
}
.hero__cta { display: flex; flex-wrap: wrap; gap: 12px; }

/* ===== Botones ===== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 0.86rem;
  padding: 12px 22px;
  border-radius: 999px;
  border: 1.5px solid var(--ink);
  transition: transform 0.16s ease, background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.btn--primary { background: var(--ink); color: var(--bg); }
.btn--primary:hover { background: var(--accent); border-color: var(--accent); }
.btn--ghost { background: transparent; color: var(--ink); }
.btn--ghost:hover { border-color: var(--accent); color: var(--accent); }

/* ============================================================
   SECCIONES
   ============================================================ */
.section { padding-block: clamp(60px, 11vw, 130px); }

.lead {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.4rem, 4vw, 2.6rem);
  line-height: 1.18;
  letter-spacing: -0.015em;
  max-width: 20ch;
}
.section--about .lead,
.section--method .lead { max-width: 16ch; }

/* ===== Proyectos ===== */
.projects {
  display: flex;
  flex-direction: column;
  gap: clamp(56px, 9vw, 110px);
  margin-top: clamp(36px, 6vw, 70px);
}
.project {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(20px, 3vw, 40px);
  align-items: center;
}
@media (min-width: 860px) {
  .project { grid-template-columns: 1fr 1.1fr; }
  .project:nth-child(even) .project__text { order: 2; }
}
.project__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.2rem, 6vw, 4rem);
  line-height: 0.95;
  letter-spacing: -0.03em;
  text-transform: lowercase;
  margin-bottom: 18px;
  display: inline-block;
}
.project__desc { color: var(--muted); font-size: 1.05rem; max-width: 42ch; margin-bottom: 18px; }
.project__stack { font-size: 0.8rem; color: var(--ink); }

.project__shot { margin: 0; }
.shot__ph {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  aspect-ratio: 1280 / 800;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    repeating-linear-gradient(45deg, rgba(0,0,0,0.02) 0 12px, transparent 12px 24px),
    var(--bg-2);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  text-align: center;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.shot__ph::after { content: attr(data-dim); font-size: 0.7rem; opacity: 0.6; }
.project__shot:hover .shot__ph {
  transform: translateY(-4px);
  box-shadow: 0 24px 50px -30px rgba(0,0,0,0.4);
}
.project__shot img { border-radius: var(--radius); width: 100%; aspect-ratio: 1280/800; object-fit: cover; }

/* ============================================================
   FOOTER / CONTACTO — cierre de página con marquee
   Dot grid ténue sobre crema (papel punteado) + correo gigante en
   marquee infinito sin costuras: dos copias idénticas en un track que
   se desplaza translateX(-50%) en loop lineal → cuando la primera sale
   por la izquierda, la segunda ya ocupa su lugar. Lento y sobrio.
   ============================================================ */
.footer {
  position: relative;
  min-height: 88svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(40px, 9vh, 90px);
  overflow: hidden;
  background-image: radial-gradient(rgba(22, 22, 15, 0.16) 1.2px, transparent 1.3px);
  background-size: 24px 24px;
  padding-block: 60px 44px;
}
.footer__mail {
  display: block;
  overflow: hidden;
  white-space: nowrap;
}
.footer__track {
  display: inline-flex;
  animation: mail-marquee 55s linear infinite;
  will-change: transform;
}
.footer__mail:hover .footer__track { animation-play-state: paused; }
.footer__chunk {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2rem, 6.8vw, 6.5rem);   /* ≈65% del tamaño anterior: respira */
  line-height: 1.05;
  letter-spacing: -0.04em;
  text-transform: lowercase;
  color: var(--ink);
  transition: color 0.25s ease;
}
/* Separador con aire generoso entre repeticiones: nunca se lee continuo. */
.footer__sep {
  display: inline-block;
  margin-inline: 1.6em;
  color: var(--muted);
}
.footer__mail:hover .footer__chunk { color: var(--accent); }
@keyframes mail-marquee {
  to { transform: translateX(-50%); }   /* exactamente una copia → sin costura */
}
/* Cierre clásico en las esquinas inferiores: © a la izquierda, LinkedIn a la derecha. */
.footer__meta {
  position: absolute;
  left: 0; right: 0; bottom: 26px;
  padding-inline: var(--gutter);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 14px;
  font-size: 0.78rem;
  color: var(--muted);
}
.footer__meta a { transition: color 0.2s ease; }
.footer__meta a:hover { color: var(--accent); }
/* Botón de icono LinkedIn: rectángulo horizontal, borde fino de tinta,
   logo (img/linkedin.png, recortado sin margen blanco) centrado sobre crema.
   Hover: invierte a tinta (el "in" blanco del logo sigue legible). */
.footer__in {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 28px;
  border: 1.5px solid var(--ink);
  border-radius: 6px;
  background: var(--bg);
  transition: background 0.2s ease;
}
.footer__in img { display: block; width: 18px; height: 18px; }
.footer__in:hover { background: var(--ink); }
.footer__in:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* Easter egg (FASE 3) — invisible hasta activarse */
.egg { position: absolute; width: 14px; height: 14px; opacity: 0; border: none; background: none; }

/* ============================================================
   FASE A — ZOOM A BLANCO (scroll-driven)
   El hero vive en una "escena" que escala hacia la primera "o"
   mientras un overlay blanco sube de opacidad. El "recorrido" de
   scroll lo genera un contenedor alto + position:sticky, y el JS
   escribe el progreso en --scale-p (0→1) y --white (0→1).
   Solo se animan transform (escena) y opacity (overlay): 0 reflow.
   ============================================================ */
.zoom-stage { position: relative; }

/* La mecánica alta + sticky SOLO con JS activo.
   Sin JS el hero es una sección normal (nada de scroll en vacío). */
.js .zoom-stage {
  height: calc(100svh + var(--zoom-travel, 170vh));
}
.js .zoom-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;   /* recorta la escena escalada: sin scroll horizontal */
}

.zoom-scene { position: relative; z-index: 1; }   /* el hero SIEMPRE sobre el peek */
.js .zoom-scene {
  position: absolute;
  inset: 0;
  /* --scale-p lo escribe el JS; transform-origin (centro de la "o") también */
  transform: scale(calc(1 + var(--scale-p, 0) * 16));
}

.zoom-white {
  position: absolute;
  inset: 0;
  z-index: 2;                 /* por encima de la escena */
  background: #ffffff;
  opacity: var(--white, 0);
  pointer-events: none;
}

/* Peek de la siguiente sección: franja blanca (el color de la escena del
   portátil) asomando bajo el hero de 92svh dentro del sticky. Solo con JS;
   sin JS el peek lo da el flujo normal. Bajo la escena (z0 vs z1): si el
   contenido del hero no cabe (ventanas bajas), nunca queda tapado. */
.zoom-peek { display: none; }
.js .zoom-peek {
  display: block;
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 8svh;
  z-index: 0;
  background: #ffffff;
  pointer-events: none;
}
/* Con JS, el hero reserva sitio para la franja: su contenido (y el hint)
   nunca caen dentro del peek, tampoco en ventanas bajas. */
.js .hero { padding-bottom: calc(8svh + 24px); }
.js .scroll-hint { bottom: calc(8svh + 14px); }

/* will-change de la escena lo gestiona el JS (setZoomAnimating), inline y solo
   mientras el zoom anima; en reposo lo pone en auto para no mantener la capa. */

/* Variante MÓVIL / puntero grueso: fundido a blanco SIN scale
   (el scale grande marea y va a tirones en táctil). Recorrido más corto. */
.zoom--lite { --zoom-travel: 110vh; }
.js .zoom--lite .zoom-scene { transform: none; }

/* La marca NO aparece sobre el hero: arranca oculta (el JS pone .nav-hidden
   en cuanto la escena está montada) y se revela con un fade justo cuando el
   fundido a blanco se completa; sigue visible en las secciones de abajo. */
.nav-hidden .mark { opacity: 0; pointer-events: none; }

/* ============================================================
   FASE B — EL PORTÁTIL QUE EMERGE DEL BLANCO
   Presenta los 3 proyectos. Dos "vidas" en el MISMO marcado:

   · DESKTOP (clase .lab--anim que pone el JS): escena sticky alta.
     Un ÚNICO portátil compartido (#labShared) emerge del blanco, se
     desliza de lado a lado y cruza-funde su captura entre proyectos;
     el texto de cada proyecto entra/sale por el lado opuesto. Todo
     ligado al scroll (progreso 0→1) y reversible con la rueda.

   · MÓVIL / reduce-motion / SIN JS: NO se añade .lab--anim → el mismo
     contenido queda en flujo normal, apilado (mini-portátil encima de
     su texto), scroll corriente. Sencillo y legible.

   El TEXTO de cada proyecto es contenido real y único en el DOM (no se
   duplica): en desktop es el panel que se desliza; en móvil, el texto
   bajo su mini-portátil. Los portátiles son decorativos (aria-hidden).
   Solo se animan transform y opacity.
   ============================================================ */

/* ---- Portátil 3D (compartido por el grande y los mini) ----
   UN ÚNICO objeto: .lap3d aporta la perspectiva (un solo punto de fuga);
   .lap3d__body es el grupo que rota entero (preserve-3d); dentro, la TAPA
   (.lap3d__lid, reclinada → apertura ~100°, con grosor vía panel trasero)
   y el DECK (.lap3d__deck, tumbado sobre la mesa) comparten la arista de
   la bisagra: borde inferior de la tapa = borde superior del deck, la
   MISMA línea en el espacio, y ambos giran desde ella.
   Intensidad de perspectiva definida en :root (variante SUTIL, por defecto). */
:root {
  --lap-persp: 1800px;  /* distancia de cámara (menor = más fuga) */
  --lap-rx:    6deg;    /* cámara ligeramente elevada (giro X del conjunto) */
  --lap-ry:    -6deg;   /* giro lateral del conjunto (.is-right invierte) */
  --lap-lid:   12deg;   /* reclinado de la tapa → apertura ~100° */
  --lap-deck:  70deg;   /* tumbado del deck (positivo = borde frontal hacia la cámara) */
  --lap-lid-z: 10px;    /* grosor de la tapa */
}

.lap3d {
  perspective: var(--lap-persp);
  perspective-origin: 50% 30%;   /* cámara algo por encima del centro */
}
/* Grupo: TODO el portátil rota junto. Su caja de layout es solo la tapa
   (16:10); el deck cuelga en absolute y no cuenta para centrados. */
.lap3d__body {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  transform-style: preserve-3d;
  transform: rotateX(var(--lap-rx)) rotateY(var(--lap-ry));
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* TAPA: reclinada hacia atrás girando sobre la bisagra (su borde inferior). */
.lap3d__lid {
  position: absolute;
  inset: 0;
  transform-origin: 50% 100%;
  transform: rotateX(var(--lap-lid));
  transform-style: preserve-3d;
}
/* Grosor de la tapa: panel trasero empujado en Z; asoma por los bordes que receden. */
.lap3d__lid-back {
  position: absolute;
  inset: -2px;
  border-radius: 16px 16px 5px 5px;
  background: linear-gradient(#34343f, #17171f 45%);
  transform: translateZ(calc(var(--lap-lid-z) * -1));
}
.lap3d__screen {
  position: absolute;
  inset: 0;
  background: #0b0b12;
  border: 2px solid #23232e;
  border-radius: 14px 14px 4px 4px;
  overflow: hidden;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
/* Captura del proyecto. Para usar una real, sustituye el contenido de
   .lap-shot por: <img class="lap-shot__img" src="…" alt="…"> (16:10). */
.lap-shot {
  position: absolute;
  inset: 10px;
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    repeating-linear-gradient(45deg, rgba(255,255,255,0.03) 0 12px, transparent 12px 24px),
    linear-gradient(160deg, #1b1b26, #0e0e16);
  opacity: 0;
}
.lap-shot.is-on { opacity: 1; }
.lap-shot__label { color: #8a8aa0; font-size: 0.8rem; letter-spacing: 0.02em; }
.lap-shot__img, .lap-shot img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; transform-origin: center; }
/* Interfaces muy densas: en vez de encajar toda la pantalla en miniatura,
   ampliamos y enfocamos la zona más representativa de cada app (el marco,
   con overflow:hidden, recorta el resto). Afecta al portátil grande y a los
   mini de móvil por igual. Ajusta transform-origin si quieres reencuadrar. */
.lap-shot[data-shot="1"] .lap-shot__img {   /* Cliente Memo: lista de clientes, anclado a la IZQUIERDA */
  object-position: left center;
  transform: scale(1.5);
  transform-origin: left center;
}
.lap-shot[data-shot="2"] .lap-shot__img {   /* Review Factory: reseña + botón "generar respuestas" (centro-arriba) */
  transform: scale(1.5);
  transform-origin: 50% 30%;
}
.lap-shot[data-shot="3"] .lap-shot__img {   /* PCPilot: cabecera + rejilla, anclado ARRIBA */
  object-position: center top;
  transform: scale(1.45);
  transform-origin: center top;
}

/* DECK: cuelga de la bisagra (top:100% de la tapa) y gira sobre esa MISMA
   arista hacia el espectador (rotateX positivo): el borde frontal queda más
   cerca de la cámara y el trapecio comparte punto de fuga con la tapa.
   Teclado insinuado como capa de fondo (sin dibujar teclas). */
.lap3d__deck {
  position: absolute;
  top: 100%; left: 0; right: 0;
  aspect-ratio: 16 / 5.6;                /* profundidad real antes de rotar */
  transform-origin: 50% 0%;
  transform: rotateX(var(--lap-deck));
  background: linear-gradient(#26262f, #16161f 60%, #0e0e15);
  border-radius: 0 0 16px 16px;
  /* Sombra de contacto: vive en el plano de la mesa → se proyecta apoyada. */
  box-shadow:
    8px 22px 34px -6px rgba(22, 22, 15, 0.30),
    2px 5px 10px rgba(22, 22, 15, 0.22);
}
/* Bisagra: banda de sombra donde la tapa se apoya sobre el deck. */
.lap3d__deck::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 16%;
  background: linear-gradient(rgba(0,0,0,0.5), transparent);
}
/* Trackpad, centrado bajo el teclado. */
.lap3d__deck::after {
  content: "";
  position: absolute;
  left: 50%; top: 64%;
  transform: translateX(-50%);
  width: 26%; max-width: 220px;
  height: 28%;
  border-radius: 8px;
  background: rgba(255,255,255,0.03);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
}

/* TECLADO: rejilla de teclas generada por JS (buildKeyboards) DENTRO del
   plano del deck — hereda su rotación y escorzo. Tonos grafito sobrios,
   sin brillos: se lee "teclado" sin robar atención a la pantalla. */
.lap3d__kb {
  position: absolute;
  left: 10%; right: 10%;
  top: 8%; height: 50%;
  display: flex;
  flex-direction: column;
  gap: 4%;
  padding: 1.2% 0.8%;
  border-radius: 5px;
  background: rgba(0, 0, 0, 0.28);      /* pocillo del teclado, ligeramente hundido */
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.35);
}
.lap3d__kb-row { display: flex; gap: 0.8%; flex: 1; min-height: 0; }
.lap3d__kb-row--fn { flex: 0.62; }      /* fila de función más baja */
.lap3d__key {
  flex: 1;
  min-width: 0;
  border-radius: 2.5px;
  background: linear-gradient(#24242e, #1b1b24);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.lap3d--mini .lap3d__key { border-radius: 1.5px; box-shadow: none; }

/* Canvas de la transición pixelada entre proyectos: vive DENTRO de
   .lap3d__screen (mismo hueco que .lap-shot) → hereda el escorzo 3D.
   Solo visible durante los ~320ms del cambio. */
.lap-pixel {
  position: absolute;
  inset: 10px;
  width: calc(100% - 20px);
  height: calc(100% - 20px);
  border-radius: 8px;
  z-index: 3;
  display: none;
  background: #0b0b12;
  pointer-events: none;
}

/* ============================================================
   INTERACCIÓN — cursor "ver proyecto" + overlay de detalle
   ============================================================ */
/* La bolita de tinta crece y muestra un texto contextual (data-label del JS:
   "ver proyecto" en el portátil, "escríbeme" en el correo). */
.cursor::after {
  content: attr(data-label);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.03em;
  color: var(--bg);
  opacity: 0;
  transition: opacity 0.2s ease 0.08s;
}
.cursor.is-project {
  width: 96px; height: 96px;
  mix-blend-mode: normal;      /* tinta sólida: el texto queda legible */
  background: var(--ink);
}
.cursor.is-project::after { opacity: 1; }

/* El portátil entero es clicable (pointer nativo sin cursor propio). */
.lab--anim .laptop, .proj-b__device { cursor: pointer; }
body.has-cursor .lab--anim .laptop,
body.has-cursor .proj-b__device { cursor: none; }

/* Overlay de detalle: crema, sobrio, scrolleable. Entrada/salida por fade
   + leve deslizamiento del contenido. */
.proj-detail {
  position: fixed;
  inset: 0;
  z-index: 8000;
  background: var(--bg);
  overflow-y: auto;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s ease, visibility 0.35s ease;
}
.proj-detail.is-open { opacity: 1; visibility: visible; }
body.detail-open { overflow: hidden; }   /* el scroll de la página espera debajo */
body.is-warping { overflow: hidden; }    /* bloqueo durante la transición warp */

/* Durante el warp el overlay entra/sale al instante BAJO la cortina pixelada
   (la animación la pone la propia cortina, no el fade CSS). */
.proj-detail--instant,
.proj-detail--instant .proj-detail__inner { transition: none !important; }

/* Cortina de pixelado del warp: cubre el viewport POR ENCIMA del detalle
   (z 8600 > 8000) e intercepta clicks mientras dura. */
.warp-pixel {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 8600;
  display: none;
  background: transparent;
}

.proj-detail__inner {
  position: relative;
  padding-block: clamp(70px, 10vh, 120px) 90px;
  transform: translateY(16px);
  transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.proj-detail.is-open .proj-detail__inner { transform: none; }

.proj-detail__close {
  position: fixed;
  top: 22px; right: clamp(20px, 4vw, 44px);
  width: 48px; height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--ink);
  background: transparent;
  border: 1.5px solid var(--ink);
  border-radius: 50%;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.16s ease;
}
.proj-detail__close:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); transform: translateY(-2px); }
.proj-detail__close:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

.proj-detail__panel { display: none; max-width: 900px; }
.proj-detail__panel.is-active { display: block; }

.proj-detail__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.4rem, 7vw, 4.6rem);
  line-height: 0.95;
  letter-spacing: -0.03em;
  margin-bottom: 22px;
}
.proj-detail__desc {
  color: var(--muted);
  font-size: clamp(1.02rem, 1.6vw, 1.15rem);
  max-width: 58ch;
  margin-bottom: 22px;
  /* Justificado a ambos márgenes SOLO en pantallas anchas (con hyphens:auto
     + lang="es" en <html> para partir sílabas sin ríos); en estrechas,
     alineado a la izquierda. */
}
@media (min-width: 700px) {
  .proj-detail__desc {
    text-align: justify;
    hyphens: auto;
    -webkit-hyphens: auto;
  }
}
/* Párrafo técnico "Bajo el capó:" — extra opcional: filete superior, cuerpo
   menor y tinta tenue; etiqueta en JetBrains Mono en negrita. */
.proj-detail__desc--tech {
  font-size: clamp(0.9rem, 1.3vw, 0.98rem);
  opacity: 0.6;
  border-top: 1px solid var(--line, currentColor);
  padding-top: 16px;
  margin-top: 26px;
}
.proj-detail__desc--tech strong.mono { font-weight: 700; font-size: 0.92em; }

.proj-detail__links { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 26px; }

.proj-detail__gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
  margin-top: clamp(30px, 5vw, 46px);
}
.proj-detail__shot { margin: 0; }
.proj-detail__shot img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: zoom-in;
}
body.has-cursor .proj-detail__shot img { cursor: none; }
/* Capturas verticales de móvil: respetar su proporción sin recorte. */
.proj-detail__shot--phone img {
  aspect-ratio: auto;
  object-fit: contain;
  max-height: 620px;
  background: var(--bg-2);
}
/* Placeholder de captura adicional: sustituir por <img> reales (16:10). */
.proj-detail__ph {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 10;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    repeating-linear-gradient(45deg, rgba(0,0,0,0.02) 0 12px, transparent 12px 24px),
    var(--bg-2);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-align: center;
  padding: 12px;
}

/* ============================================================
   LIGHTBOX — por encima del overlay de detalle (z 8500 > 8000).
   Fondo: tinta al 85%. Apertura: fade + escala leve de la imagen.
   ============================================================ */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 8500;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(22, 22, 15, 0.85);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}
.lightbox.is-open { opacity: 1; visibility: visible; }
.lightbox__img {
  max-width: min(86vw, 1400px);
  max-height: 84vh;
  border-radius: var(--radius);
  transform: scale(0.96);
  transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.lightbox.is-open .lightbox__img { transform: none; }
.lightbox__close {
  position: absolute;
  top: 22px; right: clamp(20px, 4vw, 44px);
  width: 48px; height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--bg);
  background: transparent;
  border: 1.5px solid var(--bg);
  border-radius: 50%;
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.lightbox__close:hover { background: var(--accent); border-color: var(--accent); }
.lightbox__close:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.6rem;
  color: var(--bg);
  background: none;
  border: none;
  padding: 14px;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 0.2s ease, color 0.2s ease;
}
.lightbox__nav:hover { opacity: 1; color: var(--accent-2); }
.lightbox__nav:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.lightbox__nav--prev { left: clamp(12px, 3vw, 34px); }
.lightbox__nav--next { right: clamp(12px, 3vw, 34px); }
.lap3d--mini .lap3d__screen { border-radius: 12px 12px 4px 4px; }

/* ---- POR DEFECTO (móvil / reduce-motion / sin JS): flujo apilado ---- */
.laptop-stage { position: relative; }
.lab-inner { padding-block: clamp(60px, 11vw, 130px); }
.projects-b {
  display: flex;
  flex-direction: column;
  gap: clamp(48px, 8vw, 96px);
  margin-top: clamp(30px, 5vw, 60px);
}
.proj-b { display: flex; flex-direction: column; gap: 22px; align-items: flex-start; }
/* padding-bottom reserva el alto visual del deck (absolute, fuera de flujo):
   ~10% del ancho una vez rotado. Evita que pise el texto de debajo. */
.proj-b__device { width: 100%; max-width: 440px; padding-bottom: 10%; }
.laptop { display: none; }               /* el portátil compartido solo vive en .lab--anim */

.proj-b__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2rem, 5.5vw, 3.4rem);
  line-height: 0.95;
  letter-spacing: -0.03em;
  text-transform: none;   /* respeta la capitalización del HTML: "Clientmaxxing", "ReviewFactory", "PCPilot" */
  margin-bottom: 14px;
  display: inline-block;
}
.proj-b__desc { color: var(--muted); font-size: 1.02rem; max-width: 40ch; }
.proj-b__stack { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.chip {
  font-size: 0.72rem;
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  background: var(--bg-2);
}

/* Apilado más presentable en pantallas anchas SIN animación (reduce-motion). */
@media (min-width: 860px) {
  .proj-b {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: clamp(24px, 4vw, 56px);
    align-items: center;
  }
  .proj-b:nth-child(even) .proj-b__text { order: 2; }
  .proj-b__device { max-width: none; }
}

/* ---- DESKTOP ANIMADO (.lab--anim, lo pone el JS) ---- */
.lab--anim { height: calc(100svh + var(--lab-travel, 300vh)); }
.lab--anim .laptop-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;                      /* recorta paneles fuera de pantalla */
  background: #ffffff;                   /* continuidad con el blanco de la fase A */
  display: block;
}
.lab--anim .lab-inner { max-width: none; padding: 0; height: 100%; }
.lab--anim .lab-inner > .kicker { display: none; }

.lab--anim .projects-b { position: absolute; inset: 0; margin: 0; display: block; }
.lab--anim .proj-b { position: absolute; inset: 0; display: block; }
.lab--anim .proj-b__device { display: none; }
.lab--anim .proj-b__text {
  position: absolute;
  top: 50%;
  transform: translate(0, -50%);         /* base; el JS le suma el deslizamiento en X */
  opacity: 0;                            /* el JS lo revela según scroll */
  max-width: min(32vw, 400px);           /* estrecho: deja sitio al portátil grande */
}
.lab--anim .proj-b[data-proj="1"] .proj-b__text,
.lab--anim .proj-b[data-proj="3"] .proj-b__text { left: var(--gutter); }
.lab--anim .proj-b[data-proj="2"] .proj-b__text { right: var(--gutter); text-align: right; }
.lab--anim .proj-b[data-proj="2"] .proj-b__stack { justify-content: flex-end; }

.lab--anim .laptop {
  display: block;
  position: absolute;
  left: 50%; top: 50%;                   /* centrado en el hueco bajo el nav */
  /* Algo más pequeño y acotado también por ALTO de ventana (tapa≈0.625×ancho
     + deck visible): cabe entero, con aire arriba y abajo, en cualquier alto. */
  width: min(56vw, 780px, calc((100svh - 230px) / var(--lap-hfactor, 0.73)));
  transform: translate(-50%, -50%);      /* base; el JS le suma translate3d */
  opacity: 0;
}
/* El giro cambia de lado según hacia dónde mire el portátil (.is-left /
   .is-right del JS) y se endereza parcialmente al pasar el ratón para leer
   la captura de frente. La intensidad viene de las variables --lap-*. */
.lab--anim .laptop.is-right .lap3d__body { transform: rotateX(var(--lap-rx)) rotateY(calc(var(--lap-ry) * -1)); }
.lab--anim .laptop.is-left  .lap3d__body { transform: rotateX(var(--lap-rx)) rotateY(var(--lap-ry)); }
.lab--anim .laptop .lap3d__body:hover   { transform: rotateX(calc(var(--lap-rx) * 0.5)) rotateY(0deg); }

/* will-change SOLO mientras la escena está (cerca de) pantalla — lo enciende el IO. */
.lab-active .laptop,
.lab-active .proj-b__text { will-change: transform, opacity; }
.lab-active .lap-shot { will-change: opacity; }

/* ============================================================
   REVELADO AL SCROLL
   ============================================================ */
.reveal {
  transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.16, 0.8, 0.2, 1);
  transition-delay: var(--delay, 0s);
}
/* Estado oculto SOLO con JS: es el IntersectionObserver quien revela.
   Sin JS (o si el JS falla), el contenido se muestra siempre. */
.js .reveal { opacity: 0; transform: translateY(20px); }
.reveal.is-visible { opacity: 1; transform: none; }

/* ============================================================
   ACCESIBILIDAD — reduce movimiento
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* Lightbox: solo fade, sin escala. */
  .lightbox__img { transform: none; transition: none; }
  /* Indicador de scroll estático, sin rebote; peek vía flujo normal. */
  .scroll-hint__arrow { animation: none; }
  .click-hint__arrow { animation: none; }
  .zoom-peek { display: none !important; }
  /* Marquee del footer desactivado: correo estático centrado, una sola copia. */
  .footer__track { animation: none; }
  .footer__chunk + .footer__chunk { display: none; }
  .footer__mail { white-space: normal; overflow: visible; text-align: center; }
  .footer__chunk { overflow-wrap: anywhere; font-size: clamp(1.6rem, 5vw, 4rem); }
  .reveal, .js .letter, .js .letters-in .letter {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  .btn:hover, .project__shot:hover .shot__ph { transform: none; }

  /* Fase A desactivada: el hero vuelve a ser una sección normal,
     sin recorrido alto, sin sticky, sin zoom ni fundido. */
  .js .zoom-stage { height: auto; }
  .js .zoom-sticky { position: static; height: auto; overflow: visible; }
  .js .zoom-scene { position: static; transform: none !important; }
  .zoom-white { display: none; }
  /* Sin cinemática de zoom, la marca es visible con normalidad. */
  .nav-hidden .mark { opacity: 1; pointer-events: auto; }

  /* Fase B aplanada: aunque no se añade .lab--anim con reduce-motion, forzamos
     el estado apilado por si acaso, y anulamos deslizamientos/perspectiva.
     Los 3 proyectos quedan estáticos, visibles y legibles. */
  .lab--anim { height: auto; }
  .lab--anim .laptop-sticky { position: static; height: auto; overflow: visible; }
  .lab--anim .laptop { display: none; }
  .lab--anim .lab-inner { padding-block: clamp(60px, 11vw, 130px); }
  .lab--anim .lab-inner > .kicker { display: block; }
  .lab--anim .projects-b { position: static; inset: auto; }
  .lab--anim .proj-b { position: static; inset: auto; display: flex; flex-direction: column; }
  .lab--anim .proj-b__device { display: block; }
  .lab--anim .proj-b__text { position: static; transform: none !important; opacity: 1 !important; text-align: left; }
}
