/* =====================================================================
   CENOTE-SELVA — sistema de diseño del portafolio
   Criterio: docs/superpowers/specs/2026-08-03-perfil-estetico-cenote.md
   Contrastes verificados con docs/herramientas/contrastes.py
   ===================================================================== */

/* ---- Fuentes autoalojadas ---- */
@font-face {
  font-family: "Alegreya";
  src: url("fonts/alegreya-var.woff2") format("woff2");
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Alegreya";
  src: url("fonts/alegreya-italic-var.woff2") format("woff2");
  font-weight: 400 900;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Archivo";
  src: url("fonts/archivo-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("fonts/jetbrains-mono-var.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

/* ---- Tokens (perfil §3 + spec §3) ---- */
:root {
  --fondo-selva: #103426;
  --superficie-selva: #16382a;
  --fondo-agua: #0a1c1d;
  --fondo-abismo: #04100f;
  --superficie: #12292a;
  --elevado: #1a3839;
  --cal: #ede6d4;
  --cal-suave: #c7c4b4;
  --bruma: #9aa8a3;
  --turquesa: #4fc7b8;
  --sol: #e9a23b;
  --achiote: #e8875a;
  --jade: #6fbf8e;
  --borde-sutil: #22484a;
  --borde-ui: #478a8c;
  --f-display: "Alegreya", Georgia, serif;
  --f-ui: "Archivo", system-ui, sans-serif;
  --f-mono: "JetBrains Mono", ui-monospace, Consolas, monospace;

  /* ---- Chukum: la cal deja de ser tinta y se vuelve muro ----
     --cal (#EDE6D4) y el papel del documento legal (#F3ECDD) están a 1.058
     de distancia: son el mismo valor. Promoverlo a superficie no agrega un
     color al sistema, reconoce uno que ya estaba. Es el estuco yucateco:
     la caliza acabada, el material del que están hechos los muros de aquí.

     La losa DESCIENDE con la página. En un cenote el muro está iluminado en
     la boca y el agua se lo va tragando: la losa se enfría y se apaga por
     estrato, y como la tinta nunca se invierte, la legibilidad no depende
     de dónde esté el lector. Ratios en docs/herramientas/contrastes.py. */
  --chukum: #f3ecdd; /* boca / estrato selva */
  --chukum-agua: #e6e3d2;
  --chukum-abismo: #d6d9cc;
  --chukum-hueco: #e7dfcd; /* recesión labrada en la losa */
  --chukum-hueco-agua: #dad7c5;
  --chukum-hueco-abismo: #c9ccbf;
  --chukum-filete: #d9cfb8; /* filete interior, decorativo (§6) */
  --chukum-filete-agua: #cbc9b8;
  --chukum-filete-abismo: #b8c0b3;
  --tinta: #12271f; /* verde muy oscuro, nunca #000 (§3.4) */
  --tinta-suave: #3f4a40;
  --tinta-meta: #4e5849;
  --acento-piedra: #0e5c55; /* el turquesa a la sombra */
  --acento-piedra-fuerte: #0b4f49;

  /* ---- Contrato de superficie ----
     Todo componente lee estas variables, nunca un token de color directo.
     Cambiar de material = re-ligar el contrato en el contenedor, y como la
     herencia de variables gana por profundidad en el árbol (no por
     especificidad ni por orden en el archivo), una losa dentro de un
     estrato siempre manda sobre el estrato. Sin esto, reglas como
     ".estrato-selva a" y ".losa a" — misma especificidad — se resolverían
     por orden de aparición, que es exactamente el error que se cuela al
     crecer la hoja. Por defecto: piedra oscura. */
  --sup: var(--superficie);
  --sup-hueco: var(--elevado);
  --sup-filete: var(--borde-sutil);
  --sup-borde-ui: var(--borde-ui);
  --sup-tinta: var(--cal);
  --sup-tinta-suave: var(--cal-suave);
  --sup-tinta-meta: var(--bruma);
  --sup-acento: var(--turquesa);
  /* Sobre piedra el hover solo subraya (el acento ya es lo más brillante que
     hay); sobre chukum sí profundiza, porque ahí oscurecer ES reforzar. Por
     eso el default iguala al acento en vez de inventar un tono más. */
  --sup-acento-fuerte: var(--sup-acento);
  --sup-peso: 380;

  /* Losa vigente. Las páginas de caso abren en agua y no marcan estrato en
     cada sección, así que este default es el suyo. */
  --losa: var(--chukum-agua);
  --losa-hueco: var(--chukum-hueco-agua);
  --losa-filete: var(--chukum-filete-agua);
}

/* ---- Base ---- */
*,
*::before,
*::after {
  box-sizing: border-box;
}
/* overflow-x: clip (no "hidden") — hidden crea un scroll container y eso
   apaga position:sticky en Chrome para CUALQUIER descendiente, en
   cualquier eje, aunque el overflow sea solo horizontal. clip recorta
   sin crear ese contexto de scroll. */
html {
  scroll-behavior: smooth;
  overflow-x: clip;
}
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}
body {
  margin: 0;
  background: var(--fondo-abismo); /* lo más profundo como base sin JS */
  color: var(--sup-tinta);
  font-family: var(--f-ui);
  font-weight: var(--sup-peso);
  font-size: 1rem;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}
img {
  max-width: 100%;
  display: block;
}
a {
  color: var(--sup-acento);
  text-decoration: none;
}
a:hover {
  color: var(--sup-acento-fuerte);
  text-decoration: underline;
  text-underline-offset: 3px;
}
:focus-visible {
  outline: 2px solid var(--sup-acento);
  outline-offset: 2px;
}

/* ---- Capas del descenso (activas solo donde existe .capas-fondo) ---- */
.capas-fondo {
  position: fixed;
  inset: 0;
  z-index: -1;
  display: none;
}
html.con-capas .capas-fondo {
  display: block;
}
.capa {
  position: absolute;
  inset: 0;
}
.capa-selva {
  background: var(--fondo-selva);
}
.capa-agua {
  background: var(--fondo-agua);
  opacity: 0;
}
.capa-abismo {
  background: var(--fondo-abismo);
  opacity: 0;
}
/* Base fija de las páginas de caso: ya "descendieron" al entrar desde la
   portada, así que abren directo en agua sin repetir selva→agua. Sin
   transición propia (opacity no declarada = 1, igual que .capa-selva en
   la portada) — lo único que anima ahí es capa-abismo, al acercarse al
   pie de página. */
.capa-base-agua {
  background: var(--fondo-agua);
}

/* ---- Escenografía del cenote (sprites) ----
   El descenso deja de ser solo color: dosel con lianas en la boca, paredes
   con raíces a los costados, agua que se repite en el cuerpo y rocas al
   llegar al fondo. Decisión del autor del 2026-08-06 — completa la estética
   sin tocar texto ni tarjetas, y extiende la puerta que ya abrió
   .dibujo-boca: ilustración sí, pero al servicio del descenso.

   Tres reglas hacen que esto no rompa el sistema:
   1. Los SVG son MÁSCARAS (negro = visible) y el color lo pone el
      background del ::before con un token — el sprite obedece a la paleta
      como todo lo demás, y ningún color nuevo entra sin pasar por
      contrastes.py (aquí no entra ninguno: son 4 tokens existentes).
   2. Van DESPUÉS de las capas de color en el DOM, porque las capas son
      opacas y se apilan encima de lo anterior: un sprite metido dentro de
      .capa-selva desaparecería en cuanto .capa-agua subiera a 1. Su
      opacidad la coreografía descenso.js con los mismos cruces del color.
   3. El sprite tiene DOS pieles: el div exterior recibe el parallax por JS
      (transform) y el ::before lleva la máscara y, en el agua, la deriva
      lenta por keyframes — si ambas vivieran en el mismo elemento, una
      pisaría a la otra. Solo transform y opacity se animan (§8.6).

   Sin JS no hay sprites: .capas-fondo entera vive detrás de con-capas, la
   misma degradación pasiva de siempre. */
.sprite {
  position: absolute;
  pointer-events: none;
}
.sprite::before {
  content: "";
  position: absolute;
  inset: 0;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* El dosel cuelga de la boca; se va (opacidad + parallax) al entrar al agua. */
.sprite-dosel {
  inset: -6vh -2vw auto;
  height: 62vh;
  opacity: 0.9;
}
.sprite-dosel::before {
  background: var(--fondo-agua);
  -webkit-mask-image: url(img/sprites/dosel.svg);
  mask-image: url(img/sprites/dosel.svg);
  -webkit-mask-size: cover;
  mask-size: cover;
  -webkit-mask-position: top center;
  mask-position: top center;
}

/* Las paredes acompañan todo el descenso. La derecha es la izquierda
   volteada — el flip va en el ::before para no pelear con el parallax
   del exterior. Desbordan 20vh por arriba y abajo para que el parallax
   nunca enseñe el canto. */
.sprite-pared {
  top: -20vh;
  bottom: -20vh;
  width: clamp(90px, 13vw, 230px);
  opacity: 0.4;
}
.sprite-pared-izq {
  left: 0;
}
.sprite-pared-der {
  right: 0;
}
.sprite-pared::before {
  background: var(--fondo-abismo);
  -webkit-mask-image: url(img/sprites/pared.svg);
  mask-image: url(img/sprites/pared.svg);
  -webkit-mask-repeat: repeat-y;
  mask-repeat: repeat-y;
  -webkit-mask-size: 100% auto;
  mask-size: 100% auto;
}
.sprite-pared-der::before {
  transform: scaleX(-1);
}

/* El agua se repite en loseta y deriva despacio hacia arriba: la página se
   hunde. El ::before se alarga una loseta (360px) por abajo para que el
   loop del translateY nunca enseñe el borde. */
.sprite-agua {
  inset: -30vh 0;
  opacity: 0;
}
.sprite-agua::before {
  inset: 0 0 -360px;
  background: var(--borde-sutil);
  -webkit-mask-image: url(img/sprites/agua.svg);
  mask-image: url(img/sprites/agua.svg);
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-size: 520px 360px;
  mask-size: 520px 360px;
  animation: deriva-agua 90s linear infinite;
}
@keyframes deriva-agua {
  to {
    transform: translateY(-360px);
  }
}

/* Las rocas asoman al llegar al abismo. */
.sprite-rocas {
  left: 0;
  right: 0;
  bottom: -6vh;
  height: 44vh;
  opacity: 0;
}
.sprite-rocas::before {
  background: var(--superficie);
  -webkit-mask-image: url(img/sprites/rocas.svg);
  mask-image: url(img/sprites/rocas.svg);
  -webkit-mask-size: cover;
  mask-size: cover;
  -webkit-mask-position: bottom center;
  mask-position: bottom center;
}

/* En angosto las paredes se comen la columna de lectura: fuera. El dosel
   se acorta para que las lianas no lleguen al primer título. */
@media (max-width: 760px) {
  .sprite-pared {
    display: none;
  }
  .sprite-dosel {
    height: 38vh;
  }
}
@media (prefers-reduced-motion: reduce) {
  .sprite-agua::before {
    animation: none;
  }
}

/* Sin capas fijas (sin JS) cada estrato pinta su propio fondo; con capas
   activas (con-capas, en las 3 páginas) se vuelven transparentes y el
   color lo dan las capas fijas. */
/* Cada estrato define, además de su fondo, la losa que le toca: el muro se
   apaga conforme el agua lo cubre. El acento de la selva es jade y del agua
   hacia abajo turquesa (§3.1, un acento por estrato) — se liga aquí, sobre
   el contenedor, no sobre el enlace. */
.estrato-selva {
  background: var(--fondo-selva);
  --sup-acento: var(--jade);
  /* --elevado (#1A3839) es azulado y se calculó contra el agua; sobre la
     selva verde desentona. --superficie-selva existe justo para esto
     ("tarjeta/chip en el tramo selva" en contrastes.py) y hasta ahora solo
     lo usaba el hover del desplegable. Importa desde que los chips de las
     obras salieron de la losa y se apoyan en la piedra: cal-suave sobre
     #16382A da 7.34. */
  --sup-hueco: var(--superficie-selva);
  --losa: var(--chukum);
  --losa-hueco: var(--chukum-hueco);
  --losa-filete: var(--chukum-filete);
}
.estrato-agua {
  background: var(--fondo-agua);
  --losa: var(--chukum-agua);
  --losa-hueco: var(--chukum-hueco-agua);
  --losa-filete: var(--chukum-filete-agua);
}
.estrato-abismo {
  background: var(--fondo-abismo);
  --losa: var(--chukum-abismo);
  --losa-hueco: var(--chukum-hueco-abismo);
  --losa-filete: var(--chukum-filete-abismo);
}
html.con-capas .estrato-selva,
html.con-capas .estrato-agua,
html.con-capas .estrato-abismo {
  background: transparent;
}

/* ---- Grano de caliza (perfil §7): 3.5%, sobre todo, sin capturar clics ---- */
.grano {
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---- Navegación ---- */
.nav-cenote {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 4px 16px;
  padding: 12px 24px;
  background: rgba(
    4,
    16,
    15,
    0.92
  ); /* abismo al 92%, sin blur (glassmorphism prohibido) */
  border-bottom: 1px solid var(--borde-sutil);
  font-family: var(--f-mono);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.nav-cenote a {
  color: var(--cal-suave);
  padding: 11px 6px;
} /* área táctil ≥44px real (medida: getBoundingClientRect) */
.nav-cenote a:hover {
  color: var(--cal);
  text-decoration: none;
}
.nav-cenote a[aria-current="page"] {
  color: var(--cal);
  font-weight: 600;
}
.nav-marca {
  color: var(--cal);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* La portada es la ÚNICA de las seis que no lleva marca en la barra: la
   carga el logotipo del hero, que empieza a setenta píxeles, y decirla dos
   veces en la misma pantalla resta a la grande. Las otras cinco sí la
   llevan — ahí es la única marca que hay.

   Sin este modificador el nav se queda con un solo hijo y `space-between`
   lo manda a la izquierda: la barra de la portada dejaría de cuadrar con
   las de las demás. Va con doble clase para ganar a `.nav-cenote` por
   especificidad y no por orden en el archivo. */
.nav-cenote.nav-sin-marca {
  justify-content: flex-end;
}
/* La marca va EN LÍNEA en los seis HTML, no como <img src="…svg">, por la
   misma razón que la infografía de "Auditorías": un documento SVG servido
   aparte no ve las variables ni el currentColor de la página, y este glifo
   tiene que heredar la tinta del nav. Los archivos de img/ existen para uso
   externo (redes, documentos), no para el sitio.

   Es la marca REDUCIDA (lente, mango y palomita). La completa lleva además
   los tres renglones de datos y no se usa aquí: a 17px se apelmaza. El SVG
   del nav debe seguir siendo idéntico a img/auditadata-isotipo.svg — si uno
   cambia, cambian los siete sitios (los 6 navs y el archivo). */
.glifo-marca {
  width: 1.35em;
  height: 1.35em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 3.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.nav-enlaces {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 16px;
}

/* ---- Desplegable de "Trabajo": NO navega, obliga a elegir un caso.
   hover/focus-within revela el panel — sin JS, solo opacity y transform
   (§8.6), :focus-within cubre el tabulado de teclado sin más. ---- */
.nav-desplegable {
  position: relative;
  display: flex;
  align-items: center;
}
/* El disparador es un <button>, no un <span>: el panel se revela con
   :focus-within y sus enlaces están visibility:hidden —fuera del tabulado—,
   así que sin un elemento enfocable delante el menú sería inalcanzable por
   teclado. Y no es un <a> porque no lleva a ningún sitio: mandaba a #proyectos,
   que no es lo que busca quien abre este menú.
   La fuente no se hereda en los controles de formulario (la fija el UA), de
   ahí `font: inherit`; lo demás iguala .nav-cenote a para que la fila se lea
   pareja. */
.nav-disparador {
  appearance: none;
  background: none;
  border: 0;
  margin: 0;
  padding: 11px 6px;
  font: inherit;
  /* No sobran: la hoja del navegador fija text-transform y letter-spacing en
     los controles de formulario y ahí se corta la herencia. Sin estas dos,
     "Trabajo" salía en caja baja y sin tracking, en medio de una fila de
     versalitas espaciadas. */
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--cal-suave);
  cursor: pointer;
}
.nav-disparador:hover,
.nav-desplegable:focus-within .nav-disparador {
  color: var(--cal);
}
/* Triángulo dibujado en CSS: hereda currentColor, no depende de que la fuente
   traiga el glifo ▾ (JetBrains Mono lo renderiza como un punto ilegible). */
.nav-caret {
  display: inline-block;
  margin-left: 6px;
  width: 0;
  height: 0;
  vertical-align: middle;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
}
.nav-desplegable-panel {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 4px;
  z-index: 1;
  /* width, no min-width: aunque visibility:hidden lo saca de la vista, un
     position:absolute sigue contando para el scrollWidth del documento
     (a diferencia de display:none) — en viewports angostos, si "Trabajo"
     no está pegado al borde, un ancho fijo se sale del viewport aunque
     nadie lo vea. min() lo acota siempre al espacio real disponible. */
  width: min(220px, calc(100vw - 48px));
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px;
  background: var(--elevado);
  border: 1px solid var(--borde-sutil);
  border-radius: 4px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition:
    opacity 220ms,
    transform 220ms,
    visibility 220ms;
}
.nav-desplegable:hover .nav-desplegable-panel,
.nav-desplegable:focus-within .nav-desplegable-panel {
  opacity: 1;
  visibility: visible;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .nav-desplegable-panel {
    transition: opacity 150ms;
    transform: none;
  }
}
.nav-desplegable-panel a {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 12px;
  border-radius: 2px;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  white-space: nowrap;
}
.nav-desplegable-panel a:hover,
.nav-desplegable-panel a:focus-visible {
  background: var(--superficie-selva);
  color: var(--cal);
}

/* ---- Panel de contacto: la ficha que acompaña el descenso ----
   El contacto dejó de ser el final del documento. Es una ficha con el rostro
   arriba y los canales debajo que sigue la pantalla todo el recorrido, en
   cualquier tamaño: columna a la izquierda cuando hay ancho, barra al pie
   cuando no. Las decisiones que lo mantienen dentro del sistema:

   · La COLUMNA no lleva fondo. La tinta cal aguanta los tres estratos
     (10.95 selva · 14.10 agua · 15.53 abismo), así que no hace falta una
     banda opaca — y sobre todo: un panel de chukum clavado a la pantalla
     sería una superficie clara CONSTANTE mientras todo lo demás oscurece,
     peleando contra R1 justo donde más se nota. Sin fondo, el descenso se
     ve pasar por detrás de la ficha.
   · La BARRA al pie sí lo lleva, porque cruza contenido: abismo al 92% y
     sin blur, el mismo trato que el nav (glassmorphism prohibido).
   · Sin acento en ninguna de las dos. Cruza los tres estratos y §3.1 pide
     un acento por sección: cualquiera sería el equivocado en dos tercios
     del recorrido. Los enlaces se re-ligan a la propia tinta — se cambia el
     contrato, no se pinta un color a mano.
   · Sin superponerse. Se le abre su calle con padding en <body>, así que el
     nav pegajoso también arranca después. .capas-fondo no se entera (es
     fixed, inset:0).
   La clase .con-panel va en el <body> que lo lleva (hoy solo la portada);
   sin ella ninguna otra página paga la calle, que además rompería la fuga a
   50vw de la vitrina del carrusel en el caso de LANI. */
.panel-contacto {
  position: fixed;
  z-index: 35;
  --sup-acento: var(--sup-tinta);
  --sup-acento-fuerte: var(--sup-tinta);
  font-family: var(--f-ui);
  font-size: 0.8125rem;
}
.panel-contacto a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.panel-rotulo {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--sup-tinta-meta);
  margin: 0 0 2px;
}
/* El correo es el dato, no un rótulo: va en mono y sin versalitas para que
   se pueda leer carácter a carácter y copiar sin dudas. */
.panel-correo {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.01em;
}
/* El retrato es documento, igual que las capturas: montado en la recesión de
   chukum, sin .foto-materia (ese duotono es para materia —caliza, dosel,
   agua—, no para una cara) y sin radio, porque la caliza se corta. */
.panel-retrato {
  flex: 0 0 auto;
}
/* El canal con glifo: inline-flex para que el ícono se asiente con la línea
   de texto en vez de empujarla, y `1em` para que escale con la tipografía en
   los dos formatos del panel (columna y barra) sin una medida por cada uno. */
.panel-canal {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.icono-canal {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  fill: currentColor;
}

/* --- Columna a la izquierda --- */
@media (min-width: 1100px) {
  .con-panel {
    padding-left: 216px;
  }
  .con-panel .panel-contacto {
    left: 0;
    top: 0;
    bottom: 0;
    width: 216px;
    display: flex;
    flex-direction: column;
    /* Pegada arriba, no centrada: la ficha arranca donde termina el nav y su
       canto superior continúa la línea inferior de la barra. Centrada dejaba
       el retrato flotando a media altura, sin nada con qué alinearse. */
    justify-content: flex-start;
    gap: 20px;
    /* 69px = alto del nav (12+44+12, una sola fila a este ancho). Si cambia la
       tipografía de la barra hay que volver a medirlo. */
    padding: 69px 20px 24px;
  }
  /* El hilo que separa la calle del panel de la columna de lectura. */
  .con-panel .panel-contacto::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    width: 1px;
    background: var(--sup-filete);
  }
  /* A todo el ancho de la calle: el retrato es lo primero que se ve del panel
     y a 124px competía en tamaño con los enlaces que tiene debajo. */
  .con-panel .panel-retrato {
    width: 100%;
    aspect-ratio: 1 / 1;
    padding: 8px;
  }
  .con-panel .panel-canales {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: flex-start;
  }
}

/* --- Barra al pie: lo único que "sigue la pantalla" sin comerse un teléfono --- */
@media (max-width: 1099px) {
  .con-panel {
    padding-bottom: 64px;
  }
  .con-panel .panel-contacto {
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 8px 16px;
    background: rgba(4, 16, 15, 0.92); /* abismo al 92%, sin blur */
    border-top: 1px solid var(--borde-sutil);
  }
  .con-panel .panel-retrato {
    width: 44px;
    aspect-ratio: 1 / 1;
    padding: 3px;
  }
  .con-panel .panel-canales {
    display: flex;
    align-items: baseline;
    gap: 16px;
    min-width: 0;
    flex-wrap: wrap;
  }
  /* En la barra el rótulo sobra: la ficha ya se lee entera de un vistazo y
     cada píxel de alto es de la lectura. */
  .con-panel .panel-rotulo {
    display: none;
  }
  .con-panel .panel-correo {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
/* Ventana muy angosta: el correo solo ya no cabe junto a los otros dos
   canales sin partirse. Se queda el rostro + los dos verbos; el correo sigue
   completo en el pie. */
@media (max-width: 460px) {
  .con-panel .panel-correo {
    display: none;
  }
}
@media print {
  .panel-contacto {
    display: none;
  }
  /* Sin panel no hay calle que respetar: si no, la hoja saldría con una
     sangría lateral sin nada dentro. */
  .con-panel {
    padding-left: 0;
    padding-bottom: 0;
  }
}

/* ---- Retícula y secciones ---- */
/* Ritmo entre secciones: 64px móvil / 96px escritorio — la mitad de los
   128/192 que pide §5, y bajado a propósito. El historial explica por qué
   el valor no se toca sin leer esto primero:

     4a85769 (04/08 10:17) colapsó el padding duplicado y dejó el total
             EXACTAMENTE en los 128/192 del perfil;
     46723be (04/08 10:32) lo recortó a 15/30 — quince minutos después, y
             dentro de un commit sobre el descenso que no lo menciona.

   O sea: 192 se probó y sobró (ocho secciones en columna única se leían
   como ocho páginas sueltas), pero 30px borraba la frontera y la portada
   se leía como un solo bloque apretado. 64/96 es el punto medio acordado
   el 2026-08-06: la frontera se ve, la sección siguiente no queda a una
   pantalla de distancia. */
.seccion {
  padding: 64px 24px;
}
@media (min-width: 900px) {
  .seccion {
    padding: 96px 24px;
  }
}
/* Dos .seccion contiguas suman su padding-bottom + padding-top, o sea el
   doble del ritmo: el perfil define su valor como el espacio TOTAL entre
   secciones, no como el de cada lado. Medido en index.html antes de esta
   regla: 384-424px de hueco real. Se excluye .pie-cenote: su padding-top
   propio (64px) ya es el valor deliberado para el cierre del sitio, no la
   duplicación. */
.seccion + .seccion:not(.pie-cenote) {
  padding-top: 0;
}
.contenido {
  max-width: 1280px;
  margin: 0 auto;
}
.lectura {
  max-width: 68ch;
}
.rejilla-12 {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 24px;
}
/* Dos piezas a mitades, apiladas en angosto (#caso-profundo). Empieza en una
   columna y sólo desde 900px vuelve a las 12: el reparto va aquí y no en un
   `style` en línea sobre cada tarjeta, porque una declaración en línea no la
   tumba ninguna media query — y así se quedaban las dos a 6 columnas en el
   teléfono, con el texto estrangulado.
   Doble clase por la misma razón que .rejilla-auto.rejilla-pares: contra
   .rejilla-12 a una sola clase tendrían igual especificidad y ganaría la que
   estuviera más abajo en el archivo. */
.rejilla-12.rejilla-mitades {
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .rejilla-12.rejilla-mitades {
    grid-template-columns: repeat(12, 1fr);
  }
  .rejilla-12.rejilla-mitades > :nth-child(1) {
    grid-column: 1 / span 6;
  }
  .rejilla-12.rejilla-mitades > :nth-child(2) {
    grid-column: 7 / span 6;
  }
}

/* ---- La boca: la abertura del cenote ----
   El hero NO lleva altura mínima: mide lo que mide su contenido.

   El aire de la primera pantalla lo da el `margin-bottom` de .marca-hero,
   que separa el logotipo del texto y deja el hueco por donde baja el haz
   —§7, el único elemento de luz del sitio, y aparece UNA sola vez; sin ese
   hueco vuelve a ser una franja de tres dedos que no se lee como luz—.
   **Esa es la perilla del hero.**

   Si alguna vez se le pone un min-height aquí, las dos medidas se SUMAN:
   hay que quitar una de las dos, no ajustar las dos a la vez. */
.boca {
  position: relative;
  /* clip y no hidden: hidden crea un contenedor de scroll (ver la nota de
     html arriba). Aquí sólo hay que recortar el haz contra el canto. */
  overflow: clip;
  display: flex;
  flex-direction: column;
  /* Sin altura mínima, esto no reparte nada: el hero mide lo que mide su
     contenido y no sobra espacio que repartir. Se conserva para que, si
     alguna vez vuelve una altura mínima, el texto quede abajo y no
     centrado — que es el reparto que este hero quiere. */
  justify-content: flex-end;
}
/* ---- El logotipo del hero ----
   El único sitio del sitio donde la marca aparece COMPLETA, con sus tres
   renglones de datos: aquí hay tamaño de sobra y es donde se presenta. El
   nav lleva la reducida, que a 17px es lo único que aguanta.

   Va en --sup-tinta, no en el acento: es la marca, no un elemento de la
   sección, y §3.1 reserva el acento para una cosa por sección.

   El margen inferior es lo que abre la altura del hero ahora que .boca no
   tiene min-height, y de paso le da al haz por dónde bajar: sin ese hueco
   el haz vuelve a ser una franja de tres dedos que no se lee como luz. Es
   la perilla del aire de la primera pantalla. */
/* El titular del hero se acota para dejarle su costado al dibujo, y NO es
   una preferencia de composición: es una medida de contraste. Con el
   titular a todo lo ancho, el dibujo le pasaba por detrás y el párrafo
   —que va en --cal-suave, más tenue que el titular— caía a **3.55:1**,
   por debajo del mínimo AA. Y no se arregla bajando la opacidad: al 0.20,
   donde el dibujo ya casi no se ve, cal-suave sigue en 4.46:1. La única
   solución es que no se toquen.
   De paso el titular gana: dos renglones se leen mejor que uno de 1150px,
   que se sale de cualquier medida sensata de lectura. */
.boca .display-xl {
  max-width: min(60%, 760px);
}
.marca-hero {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  margin: 0 0 clamp(52px, 9vh, 104px);
  color: var(--sup-tinta);
}
.glifo-hero {
  width: clamp(46px, 5.2vw, 62px);
  height: clamp(46px, 5.2vw, 62px);
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.palabra-hero {
  font-family: var(--f-ui);
  font-weight: 600;
  font-size: clamp(1.125rem, 1.8vw, 1.5rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1;
}
/* Un margen auto en el eje transversal desactiva el estirado del ítem
   flex: sin esto .contenido se encoge a su texto y los 1280px de la
   columna dejan de aplicarse justo en el hero. */
.boca > .contenido {
  width: 100%;
  /* Posicionado para que pinte POR ENCIMA del dibujo y del haz. Los dos son
     absolutos, y un elemento posicionado gana a uno estático sin importar
     el orden del DOM: sin esto el dibujo pasaría por delante del titular. */
  position: relative;
}

/* ---- El cenote dibujado, al costado del hero ----
   Un lápiz del interior del cenote mirando a la boca: exactamente lo que
   narra esta sección. Es una ILUSTRACIÓN, no una foto tratada — por eso va
   con `contain` y entera, no recortada a sangre: tiene composición propia y
   cortarla la estropea. Origen y procedencia en
   docs/herramientas/dibujo-cenote.py.

   **Va a la derecha y muere antes del texto, y eso no es composición sino
   norma:** §13 prohíbe texto sobre ornamento, así que un fondo a sangre
   detrás del titular está descartado. La máscara lo apaga hacia la
   izquierda mucho antes de la columna de lectura, y de paso ocupa el
   costado que estaba vacío.

   La tinta sale del contrato de superficie, no del archivo: el .webp lleva
   el dibujo en el canal alfa y el color lo pone el `background` de esta
   caja, así que si algún día se reutiliza en otro estrato se enfría con él
   como todo lo demás.

   Opacidad baja a propósito: es materia de la pared, no una ilustración que
   compita con el titular ni una segunda fuente de luz (R2 — la única es el
   haz). */
.dibujo-boca {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(46%, 700px);
  pointer-events: none;
  background: var(--sup-tinta);
  opacity: 0.42;
  -webkit-mask:
    url("img/dibujo-cenote.webp") no-repeat right center / contain,
    linear-gradient(90deg, transparent, #000 45%);
  mask:
    url("img/dibujo-cenote.webp") no-repeat right center / contain,
    linear-gradient(90deg, transparent, #000 45%);
  /* Las dos máscaras se INTERSECAN: el dibujo sólo existe donde además el
     degradado lo permite. Sin intersect se sumarían y el degradado pintaría
     un rectángulo sólido. */
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* Por debajo de 900px el hero es una columna estrecha y el dibujo quedaría
   detrás del titular pase lo que pase con la máscara. Se retira: no hay
   sitio donde ponerlo sin romper §13. */
@media (max-width: 899px) {
  .dibujo-boca {
    display: none;
  }
}

/* ---- Tipografía ---- */
.display-xl {
  font-family: var(--f-display);
  font-weight: 450;
  font-size: clamp(2.75rem, 7vw, 5rem);
  line-height: 1.02;
  margin: 0 0 24px;
  text-wrap: balance;
}
.titulo-seccion {
  font-family: var(--f-display);
  font-weight: 500;
  font-size: clamp(1.6rem, 3vw, 2.25rem);
  line-height: 1.15;
  margin: 0 0 12px;
}
/* Subtítulo de tarjeta/bloque. Sin esta regla, un <h3> suelto hereda el
   margen por defecto del navegador (~18.7px arriba y abajo, ligado a su
   propio tamaño) en vez de la escala de 8px del sitio — la causa real de
   los huecos entre bloques que .loseta h3 ya evitaba para sí misma. Una
   clase con más especificidad (.etiqueta, .loseta h3) sigue ganando sobre
   font/color/tamaño; esta regla solo fija lo que esas clases no tocan. */
h3 {
  font-family: var(--f-display);
  font-weight: 500;
  font-size: 1.1875rem;
  line-height: 1.25;
  color: var(--sup-tinta);
  margin: 0 0 8px;
}
/* ---- Cornisa de sección ----
   Abre cada sección con la línea que en la fachada Puuc separa un registro
   del siguiente (§5): un filete a todo lo ancho y, rematándolo, el nombre
   con capa que §10 le dio a la sección. Antes ese nombre colgaba suelto
   DEBAJO del título y se leía como un subtítulo tímido; arriba y al canto
   derecho encuadra, que es lo que hace una cornisa.

   Se probó meter también el estrato a la izquierda (SELVA · AGUA · ABISMO,
   la clase .estrato-* que gobierna el material de la sección) para hacer
   legible el descenso. Se quitó: colisiona de frente con los propios
   nombres con capa —#caso-profundo habría dicho «AGUA ─── El agua»— y §6
   cierra con "si dudas, quítalo". El descenso ya lo cargan el fondo y la
   losa que se enfría; no hace falta rotularlo.

   Un filete de 1px no cuenta contra el 15% de ornamento y no queda texto
   sobre ningún patrón. Sin acento (§3.1): la cornisa se repite en las siete
   secciones y el acento le toca al contenido. Sus dos tintas salen del
   contrato de superficie, así que no entra ningún par de contraste nuevo. */
.cornisa {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 16px;
  margin-bottom: 20px;
}
.cornisa-filete {
  height: 1px;
  background: var(--sup-filete);
}
.capa-poetica {
  font-family: var(--f-display);
  font-style: italic;
  font-weight: 400;
  font-size: 1.0625rem;
  color: var(--sup-tinta-meta);
  text-align: right;
}
/* En angosto los nombres largos ("De dónde viene el agua") dejan el filete
   en nada. Se parte en dos renglones: el filete a todo lo ancho arriba y el
   nombre debajo, alineado a la izquierda, que es por donde se lee. */
@media (max-width: 599px) {
  .cornisa {
    grid-template-columns: 1fr;
    gap: 8px;
  }
  .capa-poetica {
    text-align: left;
  }
}
.subtitulo {
  font-size: 1.375rem;
  line-height: 1.3;
  font-weight: 500;
  margin: 0 0 16px;
}
.cuerpo-grande {
  font-size: 1.125rem;
  line-height: 1.65;
  font-weight: 380;
}
.etiqueta {
  font-family: var(--f-mono);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sup-tinta-meta);
}
.dato {
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
}
/* Destacado del planteamiento de un caso — el problema del cliente, en su
   idioma, no la decisión técnica. Va sin comillas a propósito: describe una
   situación, no cita a nadie, y entrecomillarla la haría pasar por un
   testimonio que nadie dijo.
   Vive dentro de una losa, así que su tinta sale del contrato: iba en línea
   con var(--cal) —la tinta del fondo oscuro— y sobre chukum daba 1.06:1,
   texto invisible. Es la razón por la que el color nunca debe escribirse en
   el atributo style: un cambio de material no puede alcanzarlo. */
.cita {
  margin: 0 0 16px;
  font-family: var(--f-display);
  font-style: italic;
  font-size: 1.375rem;
  color: var(--sup-tinta);
}
/* La única aparición de --sol en todo el sitio (§3.3): el dato de Yucatán.
   Va sobre piedra desnuda, nunca dentro de una losa. */
.enlace-sol {
  color: var(--sol);
}
p {
  margin: 0 0 16px;
  color: var(--sup-tinta-suave);
}
p strong {
  color: var(--sup-tinta);
  font-weight: 600;
}

/* ---- El haz (una sola vez, en el hero de la portada) ---- */
.haz {
  position: absolute;
  top: 0;
  left: 16%;
  width: min(160px, 22vw);
  height: 100%;
  background: linear-gradient(
    180deg,
    rgba(237, 230, 212, 0.15),
    rgba(237, 230, 212, 0) 72%
  );
  /* El degradado sólo se apaga hacia abajo; los cantos izquierdo y derecho
     son rectos. Mientras el hero medía lo que su texto (~300px) no se
     notaba, pero al ocupar la ventana entera el haz pasó a leerse como un
     rectángulo gris con borde, no como luz. La máscara le da el segundo eje:
     nace y muere en nada a los lados. Va en mask y no en un tercer degradado
     dentro de background porque así el fundido multiplica al que ya existe
     en vez de pelearse con él.
     El #000 de la máscara NO es un color: en `mask` sólo se lee el canal
     alfa, y negro significa "opaco". No cuenta como hex fuera de los tokens
     ni contradice la prohibición del negro puro, que habla de fondos. */
  -webkit-mask: linear-gradient(
    90deg,
    transparent,
    #000 38%,
    #000 62%,
    transparent
  );
  mask: linear-gradient(90deg, transparent, #000 38%, #000 62%, transparent);
  pointer-events: none;
}

/* ---- Halo: una fuente de luz por pantalla (perfil §7) ---- */
.halo {
  position: relative;
}
.halo::before {
  content: "";
  position: absolute;
  inset: -18% -28%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    ellipse at center,
    rgba(79, 199, 184, 0.1),
    transparent 62%
  );
}
.estrato-selva .halo::before {
  background: radial-gradient(
    ellipse at center,
    rgba(111, 191, 142, 0.1),
    transparent 62%
  );
}

/* ---- Ornamento en franjas (perfil §6): greca escalonada y celosía ---- */
.franja-greca {
  height: 24px;
  opacity: 0.25;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='24'%3E%3Cpath d='M0 24 V16 H8 V8 H16 V0 H32 V8 H40 V16 H48 V24 Z' fill='%2322484A'/%3E%3C/svg%3E")
    repeat-x center / 48px 24px;
}
.franja-celosia {
  height: 48px;
  opacity: 0.25;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M0 12 L12 0 L24 12 L12 24 Z' fill='none' stroke='%2322484A' stroke-width='1.5'/%3E%3C/svg%3E")
    repeat center / 24px 24px;
}
/* La franja de greca no es .seccion, así que "'.seccion + .seccion" no
   toca esta frontera: el hero conservaba su padding-bottom completo
   (128/192) y #proyectos su padding-top completo, sumando ~400px de vacío
   alrededor de un adorno de 24px. Se acortan los dos lados a un valor
   propio de la escala (64px) — suficiente aire para que la franja respire
   sin repetir el ritmo completo de una sección. */
#la-boca {
  /* Sin banda muerta sobre el logotipo: la marca arranca donde termina el
     nav. El padding de .seccion (64/96px) tiene sentido cuando arriba hay
     otra sección de la que separarse; aquí arriba sólo está la barra, y ese
     hueco dejaba el hero empezando a media pantalla.
     Va por ID y no en .boca a propósito: contra .seccion tendría la misma
     especificidad y ganaría por orden en el archivo — el error que este
     repo evita en el contrato de superficie. Un ID gana siempre.
     El horizontal NO se toca: los 24px son los que alinean esta columna con
     las de todas las demás secciones. */
  padding-top: 0;
  padding-bottom: 64px;
}
.franja-greca + .seccion {
  padding-top: 64px;
}

/* ==== LOSA DE CHUKUM ==================================================
   Toda caja que el sitio presenta como objeto — tarjeta, loseta, panel,
   montura de captura — está labrada en chukum. Re-liga el contrato de
   superficie completo y a partir de ahí los componentes no saben de qué
   material están hechos: leen --sup-*.

   La losa se CORTA, no flota: sin sombra (§7 manda elevación por color) y
   radio 4px, "la caliza se corta, no se derrite". Lo que la separa del
   fondo es el propio salto de material, entre 11.6:1 y 13.6:1 según el
   estrato — no hace falta borde exterior.

   El peso del cuerpo sube de 380 a 420: §4.1 adelgaza el texto sobre fondo
   oscuro por irradiación óptica, y sobre fondo claro esa compensación se
   invierte. Sin esto la tipografía se ve anémica sobre el chukum. */
.losa,
.tarjeta,
.loseta,
.panel-decision,
.panel-elevado,
.marco-captura,
.carrusel-principal,
.carrusel-miniatura {
  --sup: var(--losa);
  --sup-hueco: var(--losa-hueco);
  --sup-filete: var(--losa-filete);
  --sup-borde-ui: var(--acento-piedra);
  --sup-tinta: var(--tinta);
  --sup-tinta-suave: var(--tinta-suave);
  --sup-tinta-meta: var(--tinta-meta);
  --sup-acento: var(--acento-piedra);
  --sup-acento-fuerte: var(--acento-piedra-fuerte);
  --sup-peso: 420;
  color: var(--sup-tinta);
  font-weight: var(--sup-peso);
}

/* ---- Losetas de trabajo (retícula de pasta hidráulica, asimetría 1-7 / 6-12) ---- */
.loseta {
  background: var(--sup);
  border-radius: 4px;
  padding: 32px;
  position: relative;
  /* clip, no hidden: hidden crearía un contenedor de scroll (ver la nota de
     html arriba). Aquí solo se recorta el friso contra el radio. */
  overflow: clip;
  transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (max-width: 899px) {
  .loseta {
    padding: 24px;
  }
}

/* ---- Obra de #proyectos: la losa lleva SOLO la captura; la prosa va a su
   costado, sobre la piedra ----
   Antes cada loseta era una tarjeta completa —captura arriba, texto debajo—
   metida en siete columnas, y dejaba las otras cinco vacías. Separar los dos
   materiales dice mejor lo que cada cosa es: el chukum es donde se apoya el
   DOCUMENTO (la captura, §"Capturas de proyecto conservan color"), mientras
   que la prosa es voz del sitio y le toca la piedra — que además es lo único
   que devuelve el jade al tramo selva, porque con todo labrado en chukum el
   acento del estrato no tenía dónde aparecer.

   La asimetría 1-7 / 6-12 no se pierde, cambia de sujeto: ya no es la
   posición de la tarjeta sino el lado por el que entra la captura, y alterna
   en cada obra. El texto ocupa el hueco que la asimetría deja libre — es la
   misma retícula, ahora sin aire muerto. */
/* La retícula es .banda y la usan dos secciones: las obras de #proyectos (donde
   además alterna el lado) y la ficha de #conoceme. El `grid-column` solo
   surte efecto cuando la banda vive dentro de .rejilla-12; suelta bajo
   .contenido, como en conoceme, el navegador lo ignora sin más. */
.banda {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 24px;
  align-items: center;
}
/* El gap de .rejilla-12 (24px) es separación entre columnas, no entre obras:
   dos bandas a 24px se leen como una sola pieza de tres pisos. */
.obra + .obra {
  margin-top: 56px;
}
/* grid-row explícito, y no es decorativo: en las obras pares la captura ocupa
   las columnas 6-12 y el texto pide las 1-5, o sea que hay que RETROCEDER en
   la fila. El auto-emplazamiento nunca va hacia atrás (modo "sparse"), así
   que sin esto el texto se caía a una segunda fila y la banda se partía en
   dos pisos con un hueco al lado de la captura. */
.banda-pieza {
  grid-column: 1 / span 7;
  grid-row: 1;
}
.banda-texto {
  grid-column: 8 / span 5;
  grid-row: 1;
}
.obra:nth-of-type(even) .banda-pieza {
  grid-column: 6 / span 7;
}
.obra:nth-of-type(even) .banda-texto {
  grid-column: 1 / span 5;
}
/* La losa se levanta desde cualquier punto de la obra: el enlace del título
   vive fuera de ella, así que engancharlo solo a .loseta:hover dejaba el
   gesto a medias. */
.obra:hover .banda-pieza,
.obra:focus-within .banda-pieza {
  transform: translateY(-3px);
}
@media (prefers-reduced-motion: reduce) {
  .obra:hover .banda-pieza,
  .obra:focus-within .banda-pieza {
    transform: none;
  }
}
@media (max-width: 899px) {
  .banda {
    grid-template-columns: 1fr;
    gap: 20px;
  }
  .banda-pieza,
  .banda-texto,
  .obra:nth-of-type(even) .banda-pieza,
  .obra:nth-of-type(even) .banda-texto {
    grid-column: 1 / -1;
    grid-row: auto;
  }
  .obra + .obra {
    margin-top: 40px;
  }
}
/* La etiqueta sube a antesala del título, como en la portada (#la-boca abre
   con "Mérida, Yucatán · México" antes del h1): el dato encuadra y el nombre
   remata. */
.banda-texto .etiqueta {
  margin: 0 0 10px;
}
.banda-texto h3 {
  font-family: var(--f-display);
  font-weight: 500;
  /* Por debajo de .titulo-seccion (máx. 2.25rem): la obra es un peldaño
     dentro de "Trabajo seleccionado", no otro encabezado de sección. */
  font-size: clamp(1.375rem, 1.9vw, 1.75rem);
  line-height: 1.15;
  margin: 0 0 12px;
}
.banda-texto p {
  margin-bottom: 20px;
}
.banda-texto > *:last-child {
  margin-bottom: 0;
}
/* El friso: lógica Puuc literal (§5) — el muro va liso y la ornamentación
   se concentra en una banda superior entre cornisas. Cada loseta es una
   fachada pequeña. Va con mask, no con la greca del data-URI de
   .franja-greca, porque un SVG incrustado no puede leer una variable CSS
   y el filete tiene que apagarse con la losa al descender. Solo en las 3
   losetas de #proyectos: es ornamento de fachada, no de tarjeta.
   El recorte del friso contra el radio y el position:relative que ancla al
   ::before ya los declara el bloque .loseta de arriba — este bloque volvía a
   declararlos con overflow:hidden y, por venir después con la misma
   especificidad, revertía en silencio el clip deliberado (hidden crea un
   contenedor de scroll y apaga position:sticky en los descendientes). */
.loseta::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 10px;
  background: var(--sup-filete);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='24'%3E%3Cpath d='M0 24 V16 H8 V8 H16 V0 H32 V8 H40 V16 H48 V24 Z' fill='%23000'/%3E%3C/svg%3E")
    repeat-x center / 48px 10px;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='24'%3E%3Cpath d='M0 24 V16 H8 V8 H16 V0 H32 V8 H40 V16 H48 V24 Z' fill='%23000'/%3E%3C/svg%3E")
    repeat-x center / 48px 10px;
}
.loseta > *:first-child {
  margin-top: 8px;
}
/* La losa se levanta de su encaje: transform, no sombra (§7 y §8.6). */
.loseta:hover,
.loseta:focus-within {
  transform: translateY(-3px);
}
@media (prefers-reduced-motion: reduce) {
  .loseta {
    transition: none;
  }
  .loseta:hover,
  .loseta:focus-within {
    transform: none;
  }
}

/* ---- Tarjetas y chips ---- */
.tarjeta {
  background: var(--sup);
  border-radius: 4px;
  padding: 32px;
}
/* ---- Icono de tarjeta de servicio ----
   Va EN LÍNEA, como la marca y por lo mismo: un SVG servido aparte no ve el
   contrato de superficie y no podría enfriarse con el estrato.

   Lleva --sup-acento, que dentro de una losa resuelve a --acento-piedra
   (4.57:1 sobre chukum, ya en la tabla; para un objeto gráfico WCAG pide
   3:1). No va en tinta plena porque competiría con el h3 que tiene debajo,
   ni en tinta meta porque a 34px de trazo fino se perdería. Es además el
   único acento de #servicios, que no tenía ninguno, así que §3.1 —un acento
   por sección— se sigue cumpliendo con él y no a pesar de él.

   El trazo (2.4 sobre lienzo de 48) es el del juego de iconos; si cambia,
   cambia en img/iconos/ y en docs/marca.html también. */
.icono-tarjeta {
  display: block;
  width: 34px;
  height: 34px;
  margin-bottom: 16px;
  color: var(--sup-acento);
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.chip {
  display: inline-block;
  padding: 6px 12px;
  border-radius: 2px;
  background: var(--sup-hueco);
  color: var(--sup-tinta-suave);
  font-family: var(--f-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
}
.grupo-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* Fila de acciones del pie: el canal de contacto primero, la navegación
   después. Son botones pares a propósito — no hay variante "primaria"
   porque el correo grande en Alegreya ya es el protagonista de la sección
   y un botón relleno competiría con él. */
.grupo-acciones {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  /* El botón no puede quedar pegado a la línea de metadatos que le sigue:
     un <p> aporta su margen, un contenedor flex no. */
  margin: 4px 0 24px;
}

.boton-enlace {
  display: inline-block;
  padding: 12px 20px;
  border: 1px solid var(--sup-borde-ui);
  border-radius: 2px;
  font-family: var(--f-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition:
    background-color 220ms,
    color 220ms;
}
.boton-enlace:hover {
  background: var(--sup-hueco);
  text-decoration: none;
}

/* ---- Capturas de proyecto (documento: conservan color, ver nota abajo) ----
   La firma del sistema. Las capturas de LANI son de una app de fondo CLARO:
   sobre piedra oscura cada una era un rectángulo blanco estrellándose contra
   el agua (blanco vs abismo = 19.3:1, el salto más violento del sitio). En
   chukum el salto baja a 1.2:1 — el papel queda apoyado sobre el estuco en
   vez de flotar sobre el vacío, y el ojo entra a la captura por el material,
   no por el borde.

   La montura es una RECESIÓN, no una caja: --sup-hueco es la losa labrada
   hacia dentro (1.12:1, perceptible sin ser informativa) y el filete marca
   el canto del corte. Dentro de una loseta ambos vienen del mismo estrato,
   así que la recesión se lee igual a cualquier profundidad. */
.marco-captura {
  border: 1px solid var(--sup-filete);
  background: var(--sup-hueco);
  position: relative;
  /* El passe-partout. Sin él la captura llena el marco hasta el canto y la
     montura queda reducida a 1px: el chukum deja de leerse como material y
     el efecto se pierde entero. Este margen es lo que convierte la imagen
     en documento apoyado sobre piedra. box-sizing:border-box (global) hace
     que el hueco se coma el padding sin alterar la caja exterior, así que
     los aspect-ratio declarados en el HTML siguen valiendo. */
  padding: 14px;
}
@media (max-width: 720px) {
  .marco-captura {
    padding: 8px;
  }
}
.marco-captura.sin-imagen::after {
  content: attr(data-pendiente);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--sup-tinta-meta);
  background: repeating-linear-gradient(
    -45deg,
    var(--sup-hueco) 0 14px,
    var(--sup-filete) 14px 28px
  );
  text-align: center;
  padding: 0 20px;
}
.captura {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
/* ---- Infografía de obra: diagrama, no captura ----
   Una obra cuyo trabajo son revisiones no tiene pantalla que enseñar, así que
   su lámina es un diagrama. Va en SVG en línea y NO como imagen justamente
   por esto: un .svg servido por <img> es un documento aparte y no ve las
   variables de la página, mientras que en línea lee el contrato de superficie
   y se enfría con el estrato igual que el resto de la losa. Ningún color
   escrito a mano — la regla que más fácil se rompe. */
.infografia {
  width: 100%;
  height: 100%;
}
.info-caja rect {
  fill: none;
  stroke: var(--sup-filete);
  stroke-width: 2;
}
.info-caja-salida {
  fill: none;
  stroke: var(--sup-acento);
  stroke-width: 3;
}
.info-linea path {
  fill: none;
  stroke: var(--sup-filete);
  stroke-width: 2;
}
.info-punta {
  fill: var(--sup-acento);
}
.info-titulo {
  font-family: var(--f-display);
  font-size: 56px;
  font-weight: 500;
  fill: var(--sup-tinta);
}
.info-titulo-salida {
  fill: var(--sup-acento);
}
.info-sub {
  font-family: var(--f-ui);
  font-size: 32px;
  fill: var(--sup-tinta-meta);
}
.pie-captura {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sup-tinta-meta);
  margin: 10px 0 40px;
}

/* ---- Carrusel de capturas: vitrina a todo lo ancho de la página.
   Perfil §13 prohíbe "carruseles automáticos" — este nunca avanza solo;
   el único movimiento es el que dispara el clic o el teclado del usuario
   (perfil §8.5). Rompe el ancho de .contenido (1280px) a propósito: la
   lámina activa necesita respirar grande, a diferencia del resto del
   texto que se queda en la columna de lectura. El truco de fuga es el
   estándar (margin negativo = mitad del contenedor menos mitad del
   viewport); no necesita position:relative porque los porcentajes de
   margin ya se resuelven contra el ancho de .contenido, su padre. Si
   sobra 1px por el hueco de la barra de scroll, overflow-x:clip en
   <html> ya lo absorbe (mismo mecanismo que protege el nav sticky). Las
   capturas de este proyecto tienen proporciones muy distintas (2.6:1 la
   más ancha, casi cuadrada la de detalle): un encuadre cinemascope +
   object-fit:contain deja que cada una respire sin deformarse ni recortar. */
.carrusel-vitrina {
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
/* `width: 100%` no es redundante y no se puede quitar: sin él el ancho
   queda "auto" y, en cuanto `max-height` recorta la altura, Chrome
   RE-DERIVA el ancho desde la relación de aspecto (altura × 21/9) en vez
   de mantener el del contenedor. En una ventana de 1894×950 eso daba un
   marco de 1552px pegado a la izquierda, con 340px de hueco a la derecha:
   los dos márgenes negativos quedan sobre-restringidos y el sobrante se
   va entero al lado derecho. Con el ancho definido, la relación sólo
   calcula la altura y `max-height` la recorta sin tocar el ancho. */
.carrusel-principal {
  position: relative;
  width: 100%;
  aspect-ratio: 21 / 9;
  max-height: 70vh;
  background: var(--sup);
  border-radius: 4px;
  overflow: clip;
}
@media (max-width: 720px) {
  .carrusel-principal {
    aspect-ratio: 4 / 3;
    max-height: 60vh;
  }
}
/* Solo transform/opacity (perfil §8.6): las tres láminas quedan apiladas
   una sobre otra y js/carrusel.js sólo alterna cuál tiene opacity:1 — un
   crossfade, el mismo mecanismo que ya usa el descenso entre capas de
   fondo. Sin JS la primera queda visible y las demás detrás, sin poder
   navegar — degradación aceptada. */
.carrusel-lamina {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 420ms cubic-bezier(0.22, 1, 0.36, 1);
  z-index: 1;
}
/* `:first-of-type`, no `:first-child`: los dos primeros hijos de
   .carrusel-principal son los botones ‹ ›, así que :first-child no casaba
   con ninguna lámina y sin JS el marco quedaba vacío. Y va condicionado a
   que el script NO haya arrancado (misma idea que .con-capas en el
   descenso); si no, la lámina 1 se quedaría opaca por debajo de la activa
   y asomaría en cada crossfade. */
.carrusel-vitrina:not(.con-carrusel) .carrusel-lamina:first-of-type {
  opacity: 1;
}
.carrusel-lamina.activa {
  opacity: 1;
  z-index: 2;
}
.carrusel-lamina .marco-captura {
  width: 100%;
  height: 100%;
  border: none;
  background: transparent;
}
@media (prefers-reduced-motion: reduce) {
  .carrusel-lamina {
    transition: opacity 150ms;
  }
}

/* Los controles viven SOBRE la lámina de chukum, así que se labran en la
   misma piedra en vez de flotar como pastillas oscuras: hueco + filete, y
   la punta de flecha en tinta. */
.carrusel-control {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--sup-hueco);
  border: 1px solid var(--sup-borde-ui);
  border-radius: 2px;
  color: var(--sup-tinta);
  cursor: pointer;
  transition: background-color 220ms;
}
.carrusel-control:hover {
  background: var(--sup-filete);
}
.carrusel-anterior {
  left: 16px;
}
.carrusel-siguiente {
  right: 16px;
}
.carrusel-control::before {
  content: "";
  width: 0;
  height: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}
.carrusel-anterior::before {
  border-right: 7px solid currentColor;
  margin-right: 2px;
}
.carrusel-siguiente::before {
  border-left: 7px solid currentColor;
  margin-left: 2px;
}

/* Debajo de la vitrina: miniaturas pequeñas a un lado, el pie de foto al
   otro — ambas ocupan la misma fuga a todo lo ancho, con un respiro
   interno (padding) para que el texto no quede pegado al borde real de
   la pantalla. En angosto se apilan: primero las miniaturas, luego el
   texto (mismo orden de lectura). */
.carrusel-tira {
  display: flex;
  align-items: center;
  gap: 32px;
  padding: 20px clamp(24px, 5vw, 64px) 0;
}
@media (max-width: 720px) {
  .carrusel-tira {
    flex-direction: column;
    align-items: stretch;
    gap: 16px;
  }
}
.carrusel-miniaturas {
  display: flex;
  gap: 10px;
  flex-shrink: 0;
}
.carrusel-miniatura {
  width: 84px;
  aspect-ratio: 16 / 10;
  padding: 0;
  border: 1px solid var(--sup-filete);
  background: var(--sup);
  cursor: pointer;
  opacity: 0.55;
  transition:
    opacity 220ms,
    border-color 220ms;
}
.carrusel-miniatura img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.carrusel-miniatura:hover {
  opacity: 0.8;
}
.carrusel-miniatura[aria-current="true"] {
  opacity: 1;
  border-color: var(--sup-acento);
}

.carrusel-info {
  min-width: 0;
}
/* El folio "01 — 03" retoma la numeración que ya usan "La apuesta" y "La
   decisión de diseño" en vez del punto genérico de un carrusel de plugin. */
.carrusel-folio {
  display: block;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sup-tinta-meta);
  margin-bottom: 6px;
}
.carrusel-pie-activo {
  margin: 0;
}

/* ---- Fotografía de materia (spec §7: definido hoy, contenido futuro) ----
   Solo para FOTOGRAFÍA (caliza, dosel, agua). Las capturas de proyecto son
   documento y conservan color: NO llevan esta clase. */
.foto-materia {
  filter: saturate(0.4);
  position: relative;
}
.foto-materia::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--fondo-agua);
  mix-blend-mode: multiply;
  opacity: 0.55;
}

/* ---- Rejilla utilitaria (grids auto-fit repetidos entre páginas de caso) ---- */
.rejilla-auto {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 28px;
}
/* Las dos rejillas de CUATRO de la portada (Servicios y Capacidades) se
   emparejan 2×2 en escritorio en vez de repartirse con auto-fit. Con el
   mínimo de 340px que ambas traían escrito en el atributo style, una columna
   de 1200px da tres pistas y deja la cuarta tarjeta sola en su renglón: una
   fila rota que se lee como un error de maquetado. Cuatro elementos piden un
   bloque par.

   Va con doble clase (.rejilla-auto.rejilla-pares) y no con una sola: contra
   .rejilla-auto tendría la misma especificidad y ganaría por orden de
   aparición en el archivo — exactamente el error que el contrato de
   superficie evita por herencia. Con dos clases el modificador gana siempre,
   lo muevan donde lo muevan. */
.rejilla-auto.rejilla-pares {
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .rejilla-auto.rejilla-pares {
    grid-template-columns: repeat(2, 1fr);
  }
}
/* El margen inferior de un <p> no escapa la caja de su celda de grid (a
   diferencia de un <div> sin padding/borde): sin este reset, la fila de
   ficha (Mi rol/Integración/…) y las tarjetas numeradas suman el margen
   del párrafo (16px) MÁS el gap de la rejilla (28px) = 44px, casi el
   triple del ritmo interno de 16px. El gap ya es la separación deseada. */
.rejilla-auto > div > p:last-child {
  margin-bottom: 0;
}

/* Tarjeta destacada de dos columnas dentro de .rejilla-auto: solo se aplica
   cuando la rejilla ya tiene ancho de sobra para dos pistas, si no
   fuerza una columna implícita y desborda a <530px. */
@media (min-width: 900px) {
  .tarjeta-doble {
    grid-column: span 2;
  }
}

/* ---- Panel elevado: elevación por color (perfil §7), no cambio de
   estrato — evita que el fondo se aclare después de oscurecer (R1).
   .panel-decision es el nombre original ("La decisión de diseño" / "La
   apuesta"); .panel-elevado es el mismo componente para secciones que
   no son una decisión pero también quieren leerse como bloque elevado
   ("Lo que sostiene el sistema", "Construcción"). Mismas reglas. */
.panel-decision,
.panel-elevado {
  background: var(--sup);
  border-radius: 4px;
  padding: 48px 32px;
}

/* ---- Caso K'íiwik: marco de hero (montaje de tres teléfonos) ----
   El montaje necesita ancho para leerse. En pantallas angostas se recorta
   a un encuadre más alto y "cover" deja al frente el teléfono central. */
.marco-hero {
  aspect-ratio: 1920 / 780;
}
.marco-hero .captura {
  object-fit: cover;
}
@media (max-width: 720px) {
  .marco-hero {
    aspect-ratio: 5 / 4;
  }
}

/* ---- Caso K'íiwik: hoja de identidad ----
   Es un artefacto exhibido, no una pieza del sitio: vive sobre PiedraCaliza,
   el mismo fondo que usa la app, enmarcada por la greca xicalcoliuhqui (el
   motivo de la serpiente emplumada). Por eso NO adopta --superficie ni
   --borde-sutil como el resto de los marcos — mostrarla con los tokens
   oscuros del sitio rompería la pieza que se está exhibiendo. El marco es
   un border-image: el navegador coloca las esquinas y repite los bordes. */
.marco-identidad {
  aspect-ratio: 680 / 590;
  box-sizing: border-box;
  background: #f5f0e8;
  border: 28px solid transparent;
  border-image: url(img/greca-kukulkan.svg) 50 round;
  padding: 20px;
}
@media (min-width: 900px) {
  .marco-identidad {
    aspect-ratio: 1080 / 700;
  }
}
/* En pantallas angostas la hoja bajaría a ~0.4x y su texto sería ilegible.
   Se le fija su ancho nativo y el contenedor se desplaza. */
.envoltura-identidad {
  overflow-x: auto;
}
.pista-desliza {
  display: none;
}
@media (max-width: 720px) {
  /* 680 de la hoja + 2×20 de padding + 2×24 del marco, para que la hoja
     conserve su tamaño nativo y siga siendo legible. */
  .marco-identidad {
    min-width: 768px;
    border-width: 24px;
  }
  .pista-desliza {
    display: block;
  }
}

/* ---- Caso K'íiwik: encabezado de fila (Reportes, Documentación) ---- */
.encabezado-fila {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 20px;
  flex-wrap: wrap;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--sup-tinta);
}
.etiqueta-acento {
  color: var(--sup-acento);
}
.fila-reporte {
  margin: 0;
  padding: 14px 0;
  border-bottom: 1px solid var(--sup-filete);
  color: var(--sup-tinta-suave);
}

/* ---- Caso K'íiwik: tabla de documentación de Google Play ---- */
.tabla-documentos {
  width: 100%;
  border-collapse: collapse;
  min-width: 640px;
}
.tabla-documentos th {
  text-align: left;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sup-tinta-meta);
  font-weight: 500;
  padding: 18px 24px 12px 0;
  border-bottom: 1px solid var(--sup-filete);
}
.tabla-documentos th:last-child {
  text-align: right;
  padding-right: 0;
}
.tabla-documentos td {
  padding: 16px 24px 16px 0;
  border-bottom: 1px solid var(--sup-filete);
}
.tabla-documentos td:last-child {
  text-align: right;
  padding-right: 0;
}
.tabla-documentos .doc-nombre {
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--sup-tinta);
  margin: 0;
}
.tabla-documentos .doc-requisito {
  font-size: 0.9375rem;
  color: var(--sup-tinta-meta);
  margin: 0;
}
.tabla-documentos a {
  font-family: var(--f-mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--sup-acento);
  padding-bottom: 2px;
  white-space: nowrap;
}

/* ---- Pie ---- */
/* El pie es lo más hondo de cualquier página, tenga estrato marcado o no:
   si algo se labra ahí, se labra en la losa del abismo. */
.pie-cenote {
  padding: 64px 24px 32px;
  --losa: var(--chukum-abismo);
  --losa-hueco: var(--chukum-hueco-abismo);
  --losa-filete: var(--chukum-filete-abismo);
}
.pie-cenote .correo-grande {
  font-family: var(--f-display);
  font-size: clamp(1.75rem, 4.5vw, 3rem);
  color: var(--sup-tinta);
}

/* ---- Entradas: ocultas solo cuando el JS va a animarlas ---- */
html.con-descenso [data-entrada] {
  opacity: 0;
}
html.con-descenso.listo [data-entrada] {
  opacity: initial;
} /* red de seguridad si GSAP no llega a animar */
