:root {
  --door-open-duration: 1.5s;
  --door-open-angle: 100deg;
  --door-open-easing: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* Paleta cálida de la invitación (DISENO-03). El dorado oscuro está
     elegido para contrastar por encima de 4.5:1 sobre el papel crema, que
     es lo que lo hace legible como texto de cuerpo y no solo como adorno
     — si se ajusta a ojo, no aclararlo por encima de ese punto. */
  --color-night: #1a1310;
  --color-paper: #f4e7d3;
  --color-ink: #3b2a1c;
  --color-gold: #7a5a22;
  --color-gold-soft: #d9c37a;
  --color-wood: #b98a52;
  --color-glow: #ffd9a0;

  /* D-05: la puerta escala como una unidad. Se elige la unidad de ALTO, no
     vmin, a propósito: así el alto de la puerta vale 100 unidades tanto en
     vertical (100vh) como en apaisado (94vh, redeclarada más abajo), y las
     medidas de los adornos significan lo mismo en los dos encuadres. Dos
     declaraciones seguidas para el mismo nombre: 1vh de respaldo primero,
     1svh después para navegadores modernos — svh y no dvh para que los
     adornos no cambien de tamaño cuando la barra de direcciones del celular
     se muestra y se esconde. NO se usan unidades de contenedor: activarían
     contain: layout, que cambia el bloque de contención de los
     descendientes position: fixed (.door y .invitation dependen de estar
     fijos al viewport real) — Pitfall 1 de 03-RESEARCH.md. */
  --door-unit: 1vh;
  --door-unit: 1svh;

  /* Cuanto crece la escena al abrir. Es una VARIABLE y no un valor suelto
     dentro de .door.is-open a proposito: en apaisado .door ya usa transform
     para centrarse (translateX(-50%)), y transform es una sola propiedad —
     una regla que declarara "transform: scale(...)" ahi borraria el
     centrado y la puerta saltaria media puerta a la derecha al abrirse.
     Redefiniendo solo el factor, cada encuadre reescribe cuanto crece sin
     tocar la lista de transforms. */
  --door-open-zoom: 2.4;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  width: 100%;
  /* D-06: scroll horizontal nunca, en ninguna pantalla. Va aquí y no en
     .door: .door declara transform-style: preserve-3d, y cualquier valor de
     overflow distinto de visible ahí forzaría a sus hijos 3D al modo plano
     (mismo mecanismo que los comentarios del archivo ya documentan para
     clip-path, mask, filter y opacity < 1 — Pitfall 4 de 03-RESEARCH.md). */
  overflow-x: hidden;
}

body {
  background: var(--color-night);
  font-family: Georgia, 'Times New Roman', serif;
  color: #f5e9d8;
  /* Aclarado a papel crema (D-06): un color plano, sin degradado radial ni
     resplandor de ambiente (D-07). Cuelga de la misma duración/curva que el
     giro de la puerta y el resplandor de .door__glow para que los tres se
     lean como un solo movimiento continuo, no como tres cosas distintas. */
  transition: background-color var(--door-open-duration) var(--door-open-easing);
}

body.is-open {
  background-color: var(--color-paper);
}

/* Capa de invitación: ya existe en el DOM desde la carga, la hoja de la
   puerta la tapa por encima (z-index) hasta que se abre (D-01, D-12). */
.invitation {
  position: fixed;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* safe center (D-06): cuando el contenido excede el alto disponible, un
     flex centrado a secas desborda por ARRIBA y esa parte queda fuera del
     alcance del scroll — el título se vuelve inalcanzable. Los navegadores
     que entienden "safe" alinean al inicio en cuanto habría desbordamiento;
     los que no la entienden descartan esta segunda declaración y se
     comportan como la línea de arriba. Degrada con dignidad, sin JS. */
  justify-content: safe center;
  /* D-06: el texto que no cabe se desplaza verticalmente, el horizontal
     nunca aparece. Seguro aquí: .invitation es una caja fija normal, no el
     elemento que declara transform-style: preserve-3d. */
  overflow-y: auto;
  overflow-x: hidden;
  gap: 1rem;
  text-align: center;
  padding: 2rem;
  /* Color de reposo legible sobre la noche cálida; pasa a var(--color-ink)
     cuando body.is-open, para que el texto se lea también a mitad del
     aclarado y no solo al final (D-06). Misma duración/curva que el fondo. */
  color: #f5e9d8;
  transition: color var(--door-open-duration) var(--door-open-easing);
  /* Con la puerta cerrada la invitación no se pinta. NO es redundante con que
     la puerta la tape: el arco usa radio horizontal porcentual (50%) y vertical
     fijo, así que en un viewport ancho y bajo se aplana en una elipse tendida y
     destapa las esquinas superiores — medido a 844x390, el texto de bienvenida
     se asomaba por encima del arco. Cubrirlo peleando con la geometría exigiría
     re-ajustar el radio para cada proporción de pantalla; esto lo cierra de raíz
     para cualquier aspect ratio, presente o futuro.
     Se usa visibility y no display para no recalcular el layout al abrir.
     No rompe D-12 (revelado continuo): script.js agrega body.is-open en el mismo
     tick que door.is-open, así que la invitación ya está visible en t=0 de la
     animación y la hoja la va descubriendo mientras gira — no aparece al final.
     Consecuencia asumida: sin JavaScript la invitación no se ve. Tampoco se veía
     antes, porque sin JS la puerta nunca se abre (D-16 solo protege contra el
     fallo del audio, no del script). */
  visibility: hidden;
}

body.is-open .invitation {
  color: var(--color-ink);
  visibility: visible;
}

.invitation__title {
  /* El minimo (1.8rem) esta puesto para que "OPEN HOUSE" quepa en UN renglon
     en un celular de 320px: con 2.1rem se partia en dos y el titulo quedaba
     en tres lineas contando la de los anfitriones. Medido, no estimado. */
  font-size: clamp(1.8rem, 8vw, 4rem);
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  /* En reposo (con la puerta aún cerrada) va en dorado suave; al abrir pasa
     a dorado oscuro (D-10). Transición propia sobre las mismas variables
     para que también forme parte del mismo movimiento y no cambie de
     golpe. */
  color: var(--color-gold-soft);
  transition: color var(--door-open-duration) var(--door-open-easing);
}

body.is-open .invitation__title {
  color: var(--color-gold);
}

/* Los anfitriones van en una segunda linea mas chica, no en la misma linea
   que "OPEN HOUSE". No es adorno: en mayusculas y con el letter-spacing del
   titulo, "OPEN HOUSE ALEXIS Y YADI" mide unos 20em, y en un celular de
   375px la letra tendria que bajar a ~15px para caber en un solo renglon —
   justo lo contrario del titulo grande que se pidio antes. Partido en dos
   niveles, "Open House" conserva su tamano y los nombres entran holgados. */
.invitation__hosts {
  display: block;
  font-size: 0.55em;
  letter-spacing: 0.08em;
  margin-top: 0.3em;
}

.invitation__welcome,
.invitation__invite {
  font-size: clamp(1rem, 2.6vw, 1.15rem);
  max-width: 34rem;
  margin: 0;
  line-height: 1.6;
  /* Justificado solo aqui. Son los unicos bloques con varias lineas: la
     hora, la direccion y el cierre son de una linea sola, y justificar una
     linea sola no hace nada (se queda a la izquierda) — quedarian
     descolgadas del resto de la composicion centrada. */
  text-align: justify;
  /* Sin esto, en un ancho de celular Georgia abre huecos enormes entre
     palabras para cuadrar los margenes. El documento ya declara lang="es",
     asi que el navegador corta con reglas de espanol. */
  hyphens: auto;
  /* La ultima linea vuelve al centro: en una composicion centrada, una
     ultima linea corta pegada a la izquierda se lee como un error de
     maquetado, no como una decision. */
  text-align-last: center;
}

/* Los filetes y el padding se declaran DESDE EL INICIO, con el borde
   transparente: bajo body.is-open solo cambia el COLOR del borde, no su
   grosor. Dos razones. Una, D-06/D-16 piden un solo movimiento continuo, y
   una propiedad que no está declarada en el estado inicial no puede
   transicionar: aparecería de golpe. Dos, animar border-width o padding es
   layout, no compositor — provocaría reflow en cada frame justo en los
   celulares lentos que la restricción de peso protege. Igual criterio en
   .invitation__map-link. */
.invitation__datetime {
  font-size: clamp(1.25rem, 4vw, 1.7rem);
  margin: 0;
  border-top: 1px solid transparent;
  border-bottom: 1px solid transparent;
  padding: 0.6rem 1.4rem;
  transition:
    color var(--door-open-duration) var(--door-open-easing),
    border-color var(--door-open-duration) var(--door-open-easing);
}

/* Bloque destacado (D-05): filetes finos color madera arriba y abajo en
   vez de una caja — lo cálido vive en los datos, no en el fondo (D-09). */
body.is-open .invitation__datetime {
  color: var(--color-gold);
  border-top-color: var(--color-wood);
  border-bottom-color: var(--color-wood);
}

.invitation__address {
  font-size: clamp(1rem, 3vw, 1.2rem);
  margin: 0;
  line-height: 1.45;
  transition: color var(--door-open-duration) var(--door-open-easing);
}

body.is-open .invitation__address {
  color: var(--color-gold);
}

.invitation__closing {
  font-size: clamp(0.95rem, 2.4vw, 1.05rem);
  margin: 0;
  line-height: 1.6;
}

.invitation__map-link {
  display: inline-block;
  padding: 0.6rem 1.2rem;
  border-radius: 999px;
  text-decoration: none;
  border: 1px solid transparent;
  background-color: transparent;
  transition:
    color var(--door-open-duration) var(--door-open-easing),
    background-color var(--door-open-duration) var(--door-open-easing),
    border-color var(--door-open-duration) var(--door-open-easing);
}

body.is-open .invitation__map-link {
  background-color: var(--color-gold);
  color: var(--color-paper);
  border-color: var(--color-wood);
}

/* Segundo elemento interactivo del sitio: foco visible por teclado, igual
   que .door ya lo tiene. :focus-visible y no :focus para que el anillo no
   aparezca al tocarlo con el dedo. No se usa :hover como única señal — en
   celular no existe (D-08 de la Fase 1); el botón ya se ve como botón por
   su fondo sólido (D-18). */
.invitation__map-link:focus-visible {
  outline: 3px solid var(--color-ink);
  outline-offset: 3px;
}

/* Escena de la puerta */
.door {
  position: fixed;
  inset: 0;
  z-index: 10;
  /* La hoja pasó de media pantalla (Fase 1) a pantalla completa: la
     profundidad que barre al girar se duplicó. Con una perspectiva fija en
     px, en un monitor ancho la hoja llegaría a cruzar el plano de
     perspectiva y la deformación se volvería absurda. Un valor relativo al
     viewport mantiene la misma relación profundidad/perspectiva en
     cualquier pantalla, y hace que el giro se sienta igual en celular y en
     escritorio. */
  perspective: 200vw;
  transform-style: preserve-3d;
  cursor: pointer;
  touch-action: manipulation;
  /* El acercamiento: toda la escena crece mientras la hoja gira, como si el
     visitante cruzara el umbral en vez de mirarlo desde afuera. Va sobre
     .door y no sobre .door__leaf a proposito — la hoja ya tiene su propio
     rotateY, y meter el scale ahi lo mezclaria con el giro. */
  transition: transform var(--door-open-duration) var(--door-open-easing);
}

/* SOLO transform. NO poner opacity aqui: .door declara transform-style:
   preserve-3d, y opacity < 1 lo fuerza a modo plano — la hoja dejaria de
   girar a mitad de la animacion. Es el mismo mecanismo que ya documentan
   los comentarios de overflow/clip-path/mask/filter de este archivo
   (Pitfall 4 de 03-RESEARCH.md). Al terminar, script.js pone display:none;
   a 2.4x la puerta ya salio del encuadre, asi que ese corte no se ve. */
.door.is-open {
  transform: scale(var(--door-open-zoom));
  /* Deja de interceptar clics apenas empieza a abrirse, sin esperar al
     display:none del final. */
  pointer-events: none;
}

/* Marco simple de madera sobre fondo liso (D-14), detrás de la hoja.
   Es hijo de .door, así que se retira junto con ella (D-06). */
.door__frame {
  position: absolute;
  inset: 0;
  z-index: 1;
  /* Marco de BORDE, no de relleno. Antes esto era background:#5a3d22 sobre
     inset:0, o sea una pared opaca cubriendo toda la escena por detras de las
     hojas: al girar, las hojas descubrian esta pared en vez de la invitacion, y
     la invitacion aparecia de golpe en un solo frame cuando script.js pone
     display:none al terminar la transicion. Eso rompia D-12 (revelado continuo)
     y el human-check 4 del plan 01-01 ("se va descubriendo mientras las hojas
     giran, no aparece de golpe al terminar"). El tracer de la tarea 1 no tenia
     marco, asi que el revelado si funcionaba cuando se aprobo el checkpoint; la
     tarea 2 lo introdujo y no se volvio a mirar a mitad de animacion.
     El grosor del borde coincide con el inset de la hoja (top/bottom/left/right,
     ver .door__leaf abajo), asi que se ve igual con la puerta cerrada. El
     interior queda transparente: la invitacion se descubre progresivamente
     conforme la hoja rota. La puerta cerrada sigue tapando todo: la hoja es
     opaca. Grosor relativo a --door-unit (D-05): calc(2.4 * var(--door-unit))
     reproduce los 20px de referencia sobre una puerta de 390×844. */
  border: calc(2.4 * var(--door-unit)) solid #5a3d22;
  box-shadow: inset 0 0 0 6px #432c17;
  /* El arco. El radio del borde interior de un elemento con borde es su
     radio exterior menos el grosor del borde. En --door-unit esa misma resta
     es 21 − 2.4 = 18.6: el radio vertical de .door__frame de abajo (21) menos
     el grosor de borde de arriba (2.4) tiene que dar EXACTO el radio vertical
     del arco de .door__leaf (18.6). Si se desemparejan, quedan huecos
     transparentes en las esquinas del arco y la invitación se ve por ahí con
     la puerta cerrada — rompe la premisa de que nada asoma antes de abrir
     (D-11, D-14 de la Fase 1; D-05 de esta fase). */
  border-radius: 50% 50% 6px 6px / calc(21 * var(--door-unit)) calc(21 * var(--door-unit)) 6px 6px;
}

/* La luz que crece (DISENO-04, D-16). Hermano de .door__frame, mismo plano
   Z=0 ("dentro de la casa"), por detrás de la hoja que vive en
   translateZ(1px) — con la puerta cerrada la hoja lo tapa por completo, que
   es correcto: la única luz visible en reposo es la de .door__window (D-15).
   Como es hijo de .door, cuando script.js oculta la puerta al terminar la
   transición la luz se apaga en el mismo fotograma que la hoja y el marco
   (D-17 sale gratis, no hace falta código extra para apagarla). */
.door__glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Centrado al 35% horizontal y no al 50% a propósito: la bisagra está a
     la derecha, así que el hueco se abre por la izquierda y ahí es donde
     la luz tiene que parecer que entra (Open Question 2 de RESEARCH.md). */
  background: radial-gradient(ellipse at 35% 50%, var(--color-glow), rgba(255, 217, 160, 0) 70%);
  transform: translateZ(0) scale(0.25);
  opacity: 0;
  /* Se anima SOLO opacidad y escala: ambas las resuelve el compositor sin
     repintar. Animar la sombra o el tamaño del fondo repintaría cada
     fotograma y rompería la restricción de degradar con dignidad de
     CLAUDE.md (Pitfall 2 de RESEARCH.md). */
  transition: transform var(--door-open-duration) var(--door-open-easing),
              opacity var(--door-open-duration) var(--door-open-easing);
}

.door.is-open .door__glow {
  transform: translateZ(0) scale(1);
  opacity: 1;
}

.door__leaf {
  position: absolute;
  /* Mismo grosor relativo que el borde de .door__frame (D-05): el inset de
     la hoja tiene que seguir coincidiendo con el marco al escalar. */
  top: calc(2.4 * var(--door-unit));
  bottom: calc(2.4 * var(--door-unit));
  left: calc(2.4 * var(--door-unit));
  right: calc(2.4 * var(--door-unit));
  z-index: 3;
  background: repeating-linear-gradient(
    90deg,
    #c9925a 0px,
    #c9925a 6px,
    #b07c46 6px,
    #b07c46 12px
  );
  box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.45);
  transform-style: preserve-3d;
  backface-visibility: hidden;
  transition: transform var(--door-open-duration) var(--door-open-easing);
  /* Bisagra en el borde derecho (D-12): el borde derecho se queda quieto y
     el resto de la hoja es lo que gira. */
  transform-origin: 100% 50%;
  /* El arco de la hoja. Ver el comentario de .door__frame: estos dos radios
     están emparejados a propósito (radio exterior del marco menos su
     grosor de borde, 21 − 2.4 = 18.6 en --door-unit) y no son valores
     intercambiables. */
  border-radius: 50% 50% 4px 4px / calc(18.6 * var(--door-unit)) calc(18.6 * var(--door-unit)) 4px 4px;
  /* La hoja vive 1px delante del plano Z=0 donde está .door__frame. Sin
     esta separacion el borde interno de la hoja arranca el giro coplanar
     con el marco, y dentro de un contexto preserve-3d el renderer ordena
     por Z computado en vez de z-index: dos superficies coplanares producen
     z-fighting, visible como una linea parpadeando durante la apertura.
     Depende de la GPU, por eso se reproducia en movil y no en Chromium de
     escritorio (Pitfall 5 de 01-RESEARCH.md). Con perspective:200vw, 1px es
     imperceptible frente a la escala del viewport.
     El translateZ se repite en la regla .is-open de abajo. Si faltara ahi,
     la hoja cruzaria Z=0 a mitad de transicion y el parpadeo volveria. */
  transform: translateZ(1px);
}

.door.is-open .door__leaf {
  /* El signo NEGATIVO es el punto entero de esta regla: la hoja derecha de
     la Fase 1 tenia el mismo origen (100% 50%) con angulo POSITIVO y abria
     hacia AFUERA, hacia el visitante. D-12 pide lo contrario: la puerta se
     aleja del visitante, hacia el interior. Con la bisagra en el mismo
     lado, invertir el sentido del giro es invertir el signo. Copiar la
     regla de la hoja derecha de la Fase 1 tal cual es el error de copiar y
     pegar mas probable de toda esta fase (Pitfall 3 de RESEARCH.md) — no
     "arreglar" esto sin volver a leer esa nota. */
  transform: translateZ(1px) rotateY(calc(-1 * var(--door-open-angle)));
}

/* Ventana ovalada retroiluminada (D-11, D-15): calcomanía plana dentro de
   la hoja — position: absolute con porcentajes, SIN transform-style ni
   translateZ propios. No es descuido: la ventana es DESCENDIENTE de la hoja
   (no hermana dentro del contexto 3D de .door), y ya viaja con la única
   transformación de la hoja. Darle Z propio la convertiría en un plano rival
   y provocaría el parpadeo que costó dos intentos en la Fase 1 (Pitfall 5 de
   RESEARCH.md).
   Va vacía a propósito: el dueño pidió en el UAT quitar el adorno de en medio
   y dejar solo el círculo de luz (revierte la filigrana SVG de D-13). */
.door__window {
  position: absolute;
  top: 16%;
  left: 27%;
  width: 46%;
  height: 24%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 45%, #fff2d6, var(--color-glow) 55%, #e8a75a 100%);
  /* Sombra ESTÁTICA a propósito: se pinta una vez y nunca se anima —
     animar sombra repinta cada fotograma (Pitfall 2 de RESEARCH.md). */
  box-shadow: inset 0 0 0 4px #8a5f33, 0 0 26px 6px rgba(255, 217, 160, 0.55);
  pointer-events: none;
}

/* Los tres paneles hundidos de la mitad baja se quitaron a pedido del dueño
   en el UAT: la hoja queda con la sola veta de madera, sin relieve. Junto con
   ellos se fue .door__filigree (el adorno SVG dentro de la ventana). Ambos
   estaban en D-11/D-13 de 02-CONTEXT.md; se revierten a propósito, no por
   descuido. Con la filigrana fuera ya no queda ningún SVG en el proyecto —
   la ampliación de PUERTA-01 para permitir SVG en línea queda sin uso, pero
   se conserva porque sigue siendo cierta y no cuesta nada. */

/* Manija: decorativa respecto del clic — la zona clickeable sigue siendo
   el contenedor .door completo (D-09 de la Fase 1), por eso
   pointer-events: none. Late en bucle desde la carga sin depender del
   puntero (D-08 de la Fase 1): funciona igual en celular, donde :hover no
   existe. A la izquierda, como en la foto de referencia (D-11/D-12). */
.door__handle {
  position: absolute;
  top: 50%;
  left: 10%;
  /* Relativo a --door-unit (D-05): reproduce los 8×90px de referencia sobre
     una puerta de 390×844. */
  width: calc(1 * var(--door-unit));
  height: calc(10.7 * var(--door-unit));
  border-radius: 6px;
  background: linear-gradient(90deg, #d9c37a, #8a7434 55%, #d9c37a);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  transform: translateY(-50%);
  animation: handle-pulse 1.8s ease-in-out infinite;
}

@keyframes handle-pulse {
  0%,
  100% {
    transform: translateY(-50%) scale(1);
  }
  50% {
    transform: translateY(-50%) scale(1.08);
  }
}

/* Pista de texto, revelada tras un par de segundos (D-10 de la Fase 1) para
   darle al visitante un respiro antes de pedirle algo. El retardo vive en
   CSS, sin temporizador en JS que haya que limpiar. */
.door__hint {
  position: absolute;
  left: 50%;
  /* Relativo a --door-unit (D-05): reproduce los 40px de referencia sobre
     una puerta de 390×844. */
  bottom: calc(4.7 * var(--door-unit));
  z-index: 4;
  margin: 0;
  /* translateZ(2px) mantiene la pista DELANTE de la hoja, que vive en 1px.
     La pila de .door (marco 0 -> hoja 1 -> pista 2) se ordena por Z
     computado, no por z-index, una vez que hay hermanos en distintos
     planos — si no se sube tambien, la pista queda tapada por la hoja. */
  transform: translateX(-50%) translateZ(2px);
  /* Escala con la puerta pero nunca baja de lo legible ni crece sin
     control (D-05). */
  font-size: clamp(0.9rem, calc(1.9 * var(--door-unit)), 1.4rem);
  letter-spacing: 0.05em;
  color: #fff3df;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  pointer-events: none;
  opacity: 0;
  animation-name: hint-reveal;
  animation-duration: 0.6s;
  animation-timing-function: ease;
  animation-delay: 2s;
  animation-fill-mode: forwards;
}

@keyframes hint-reveal {
  to {
    opacity: 1;
  }
}

/* Reencuadre por orientación (D-01, D-02, D-03). En pantalla más alta que
   ancha (celular vertical) manda la regla base de arriba: .door en
   inset: 0, borde a borde, exactamente como hoy — el dueño lo pidió así
   ("en celular no se ve mal, así que eso lo puedes dejar así"). En pantalla
   más ancha que alta (monitor, celular acostado) el alto manda: 3% de
   sangría arriba y abajo (literal del dueño) y el ancho se deriva de la
   proporción de puerta 3:5. Con top/bottom fijos y width:auto queda un eje
   en auto, que es justo lo que aspect-ratio necesita para derivar el otro
   — en 1920×1080 da ~609×1015 y ~34% de margen por lado: decisión tomada
   por el dueño con los números en la mano (D-02), no un defecto a corregir
   hacia el 10% que se pidió al principio. El fondo alrededor sigue siendo
   el --color-night plano de body: no hace falta nada nuevo aquí (D-04). */
@media (orientation: landscape) {
  /* 94% = 100% − 2×3% de sangría (D-02): el alto de la puerta vuelve a
     valer 100 unidades en este encuadre también. */
  :root {
    --door-unit: 0.94vh;
    --door-unit: 0.94svh;
  }

  .door {
    top: 3%;
    bottom: 3%;
    left: 50%;
    right: auto;
    width: auto;
    aspect-ratio: 3 / 5;
    transform: translateX(-50%);
    /* Pitfall 3 de 03-RESEARCH.md: 200vw (regla base de .door, sin tocar)
       medía contra el viewport completo porque .door ocupaba el viewport
       completo. Aquí la puerta mide 60 unidades de ancho (3:5 de 100
       unidades de alto), así que la perspectiva correcta es el mismo doble
       del ancho de la puerta, expresado en --door-unit en vez de vw — sin
       esto el giro 3D se vería plano en escritorio. */
    perspective: calc(120 * var(--door-unit));
  }

  /* Aqui la puerta mide 60 unidades de ancho (3:5) dentro de un viewport de
     ~189 unidades en 16:9 y hasta ~247 en 21:9. Con el 2.4 de vertical
     terminaria midiendo 144 y los costados del marco NUNCA saldrian de
     cuadro: se quedan cruzando la invitacion hasta el corte final. 4.5 la
     lleva a 270 unidades, asi que los costados salen antes de terminar y el
     acercamiento se lee como cruzar el umbral, no como una barra que se
     desliza. En vertical no hace falta porque la puerta ya ocupa todo el
     ancho. */
  :root {
    --door-open-zoom: 4.5;
  }

  /* El translateX(-50%) del centrado se REPITE aqui. No es redundante: sin
     el, esta regla dejaria .door.is-open con un transform de un solo item y
     la puerta perderia el centrado justo al abrirse. */
  .door.is-open {
    transform: translateX(-50%) scale(var(--door-open-zoom));
  }
}

/* Movimiento reducido (D-08–D-11): quien pide menos movimiento en su sistema
   conserva la puerta cerrada, la pista y el clic como acto propio — lo único
   que pierde es la rotación 3D de pantalla completa. Ninguna regla base de
   arriba se toca, así que la apertura normal queda intacta por construcción. */
@media (prefers-reduced-motion: reduce) {
  /* La hoja se disuelve en vez de girar (D-08): transform deja de estar en
     la lista de propiedades con transición, así que translateZ(1px) se
     repite sin cambiar — el mismo valor exacto que en reposo — y solo
     transiciona la opacidad. Conservar el translateZ(1px) (no "none") es
     obligatorio: es lo que separa la hoja del plano Z=0 del marco, y
     quitarlo reintroduce el parpadeo por z-fighting que costó dos intentos
     en la Fase 1. */
  .door__leaf {
    transition-property: opacity;
    transition-duration: var(--door-open-duration);
    transition-timing-function: var(--door-open-easing);
  }

  .door.is-open .door__leaf {
    transform: translateZ(1px);
    opacity: 0;
  }

  /* El fundido dura lo mismo que el giro normal (D-09), a propósito: el
     audio de apertura y la animación tienen que seguir emparejados, y el
     respaldo por setTimeout de script.js está escrito a mano en 1500ms —
     cambiar la duración aquí lo desincronizaría en silencio. */

  /* El latido se apaga (D-10): ninguna animación infinita queda corriendo
     en pantalla. @keyframes handle-pulse se queda definida sin cambios,
     el modo normal la sigue usando. */
  .door__handle {
    animation: none;
  }

  /* La pista pierde su retardo de 2s SOLO aquí (D-10): la señal doble de la
     Fase 1 queda sencilla —el texto solo, visible desde el primer
     instante— porque la otra mitad de la señal (el latido) acaba de
     apagarse arriba. La regla base fuera de este bloque no se toca. */
  .door__hint {
    animation: none;
    opacity: 1;
  }

  /* El resplandor entra sin escalar (D-11): mismo transform en los dos
     estados (translateZ(0), no "none", para no sacarlo del plano Z=0 de la
     pila 3D) — el crecimiento de un cuarto a tamaño completo desaparece y
     solo queda el fundido de opacidad. DISENO-04 se conserva. */
  .door__glow {
    transform: translateZ(0);
  }

  .door.is-open .door__glow {
    transform: translateZ(0);
    opacity: 1;
  }

  /* El acercamiento se apaga (D-08): crecer a pantalla completa es justo el
     tipo de movimiento que esta preferencia pide evitar. Se apaga por la
     VARIABLE y no con "transform: none": none tambien borraria el
     translateX(-50%) que centra la puerta en apaisado, y la dejaria
     descentrada justo al abrirse. Con el factor en 1, scale(1) no mueve
     nada y la lista de transforms queda intacta. Este bloque va despues del
     de apaisado en el archivo, asi que gana cuando los dos aplican.
     pointer-events: none se conserva — no es movimiento. */
  :root {
    --door-open-zoom: 1;
  }
}
