/* ═══════════════════════════════════════════════════════════
   Bárbara & Adriel — 12 · 12 · 2026
   Invitación de boda · hoja de estilos única
   ═══════════════════════════════════════════════════════════ */

/* Los valores viven en tokens.css. Acá no se define ninguno: solo se usan.
   Las únicas declaraciones de variables que quedan son overrides por bloque
   —una superficie que da vuelta el par de colores del botón, por ejemplo—. */

/* ─── Base ───────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html, body {
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }

body {
  margin: 0;
  background: var(--color-ink);
  color: var(--color-paper);
  font-family: var(--type-label);
}

a { color: var(--color-paper); text-decoration: none; }

img { max-width: 100%; }

button {
  font: inherit; color: inherit;
  margin: 0; border: 0; background: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

input::placeholder { color: var(--color-paper-faint); letter-spacing: normal; }

/* Anillo de foco. No había ninguno: quien navega con teclado no veía dónde
   estaba parado, y el input encima llevaba outline:none. Va en :focus-visible
   para que no aparezca al tocar con el dedo, solo al tabular. El color se
   hereda del contexto (currentColor), así funciona sobre marrón y sobre papel
   sin definirlo dos veces. */
:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
.gate:focus-visible { outline-offset: -6px; }

[hidden] { display: none !important; }

.page {
  max-width: var(--shell);
  margin: 0 auto;
  background: var(--color-ink);
  /* clip y no hidden: hidden convierte a .page en contenedor de scroll —aunque
     nunca scrollee— y las animaciones con animation-timeline: view() miden
     contra él, que está quieto, así que se congelan. clip recorta igual sin
     crear contenedor. */
  overflow-x: clip;
}

/* ─── Animaciones ────────────────────────────────────────── */
@keyframes fadeIn   { from { opacity: 0 } to { opacity: 1 } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0ms !important;
    transition-duration: .001ms !important;
  }
}

/* ═══════════════ 0 · APERTURA ═══════════════ */
.gate {
  position: fixed; inset: 0; z-index: 999;
  background: var(--color-ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transform-origin: center center;
  will-change: transform, filter, opacity;
}
.gate--gone { visibility: hidden; pointer-events: none; }

.gate__frame {
  position: relative;
  width: 100%; max-width: var(--shell); height: 100%;
  margin: 0 auto; overflow: hidden;
}
.gate__photo {
  position: absolute; inset: 0;
  background: url('assets/sobre-lacre.webp') center / cover no-repeat;
}
/* El sobre ya es oscuro: apenas una viñeta para que el texto despegue. */
.gate__veil {
  position: absolute; inset: 0;
  background: radial-gradient(120% 75% at 50% 45%, transparent 0%, var(--color-glow-on-dark) 62%, var(--color-scrim-soft) 100%);
}
.gate__veil--bottom {
  background: linear-gradient(to top, var(--color-scrim-soft) 0%, transparent 42%);
}

/* ═══════════════ I · PORTADA ═══════════════ */
.header {
  position: relative;
  background: var(--color-ink);
  opacity: 0; transform: scale(1.06); filter: blur(8px);
  will-change: transform, filter, opacity;
}
/* La foto va a sangre y baja 180px por debajo de la sección: ese borde queda
   tapado por la tarjeta de la mano de Save the Date, así que no se ve dónde
   termina. La tarjeta arranca a 78px del inicio de esa sección y llega hasta
   0,66×ancho, o sea 78–289 a 320px y 78–369 a 440: 180 cae adentro siempre.
 */
.header__foto {
  position: absolute; top: 0; bottom: -180px; left: 0; right: 0;
  /* Debajo de la url va la mediana medida del cuadro (39,47,36): si el archivo
     llegara a faltar, la portada se ve como un verde de penumbra y no como un
     rectángulo vacío. Los dos radiales ámbar que había acá eran para fingir un
     salón con arañas encendidas; con un paisaje no vienen al caso. */
  background:
    var(--img-grove) center / cover no-repeat,
    var(--color-surface-grove);
}
.header__scrim {
  position: absolute; top: 0; bottom: -180px; left: 0; right: 0;
  pointer-events: none;
  /* Ver --veil-hero en tokens.css: los tramos están medidos contra el cuadro. */
  background: var(--veil-hero);
}

.hero {
  position: relative; z-index: 2;
  min-height: var(--vh);
  display: flex; flex-direction: column; align-items: center;
  /* Centrado y no repartido: con solo dos elementos, space-between los pegaba
     a los extremos y la portada quedaba hueca en el medio. */
  justify-content: center; gap: var(--space-5);
  /* El relleno de abajo despeja el contador fijo. */
  text-align: center; padding: var(--space-5) var(--gutter-display) calc(var(--timer-bottom) + var(--space-6));
}


/* El monograma reemplaza a los nombres tipografiados. Con él se fueron el
   centrado óptico de "Bárbara" y "Adriel" —medido por barrido de píxeles— y el
   ampersand prestado de la Playfair itálica: eran la solución a componer dos
   nombres con tipografía, y ya no hay dos nombres que componer.
   El h1 queda como caja de la imagen y nada más. */
/* El margen negativo anula el relleno ancho del hero, nada más: --gutter-display
   existe para darle aire a los swashes de una script tipografiada, y este
   archivo no lo necesita porque su tinta ya viene calzada al borde.
   El ancho lo pone --marca-ancho, que sale de la legibilidad de la frase; ver
   tokens.css. En una pantalla de 375 quedan 12px de aire a cada lado y en una
   de 320 la marca llega justa al borde. */
.hero__names {
  margin-inline: calc(-1 * var(--gutter-display));
  display: flex; justify-content: center;
}
/* El ancho manda y el alto sale de la proporción del archivo, así que el hero
   no salta mientras carga.
   El drop-shadow y no text-shadow: la marca es una imagen con transparencia, y
   el filtro sigue el contorno del trazo en vez de la caja. Mismos valores que
   --shadow-text-strong, que es lo que llevaban los nombres sobre el cuadro. */
.hero__marca {
  display: block;
  /* Hasta --marca-ancho, y menos si la pantalla no da. */
  width: 100%; max-width: var(--marca-ancho); height: auto;
  filter: drop-shadow(0 2px 22px rgb(var(--rgb-shade) / .80));
}

.hero__kicker {
  margin: 0;
  /* Era versalitas con tracking amplio, como los overlines, los botones y la
     fecha. Cinco elementos con el mismo recurso y el recurso deja de
     jerarquizar. Esto es una frase, así que se lee como frase. */
  /* Sube de 16 a 24. No era contraste —medido contra los píxeles reales del
     cuadro con el velo encima da 6,44:1 en el peor punto, contra los 4,5 que
     pide la norma— ni trazo fino: era tamaño, una serif delicada de 16px
     debajo de nombres de 84. A 24 la línea larga mide 246px y entra en los
     256 que quedan a 320.
     Una salvedad: eso vale con la Baskerville puesta. En el fallback a Georgia
     la misma línea mide 265,5 y a 320 se parte en tres renglones. No se
     arregla acá —pasa solo si el kit de Typekit no entrega la fuente, o sea
     con la tipografía ya equivocada en toda la página—, pero es un motivo más
     para no olvidarse de cargar el dominio de producción en el kit. */
  font: 400 var(--text-lg)/var(--leading-loose) var(--type-body);
  color: var(--color-paper);
  text-shadow: var(--shadow-text);
}

/* ═══════════════ Temporizador fijo ═══════════════ */
.countdown {
  position: fixed; bottom: var(--timer-bottom); left: 50%;
  transform: translateX(-50%); z-index: 10;
  pointer-events: none;
  width: 100%; max-width: var(--shell);
  padding: var(--space-2) 0;
  display: flex; align-items: baseline; justify-content: center; gap: var(--space-2);
  white-space: nowrap;
  color: var(--color-paper); background: var(--color-ink);
  /* Sin sombra: la única elevación del sistema es la del pop-up. */
  animation: fadeIn var(--duration-slow) var(--ease-out) both;
}
/* Las palabras en la caligráfica, las cifras en la Playfair. Cada tipografía
   hace lo que sabe: la script pone el tono y la serif aguanta los números, que
   son lo único que hay que poder leer de un vistazo.
   "Faltan" y no "FALTAN": las mayúsculas de una script son capitales de rasgo,
   pensadas para abrir palabra, no para formar una. En caja alta y con el
   tracking de overline que tenía antes, se leían como un adorno roto.
   24 y no 18: la altura de x de Playfair es 1,347 veces la de Pinyon —medido—
   así que la script necesita ese cuerpo para verse del mismo tamaño que las
   cifras. El cálculo da 24,2 y el escalón de la escala es 24. */
.countdown__label {
  font: 400 var(--text-lg)/var(--leading-none) var(--type-display);
}
/* El ancho fijo no es capricho. Acá decía que `tabular-nums` evitaba que el
   reloj temblara en cada tic; medido, es falso: ni Playfair ni Pinyon traen la
   característica `tnum`, sus cifras varían 4,5 y 5,5px de ancho, y el contador
   venía moviéndose 9px por segundo desde siempre. Como está centrado, eso son
   4,5px de vaivén a cada lado.
   Fijar sólo la caja no alcanzaba: probado, el texto centrado adentro seguía
   moviéndose 8,85px, porque lo que cambia de ancho son las cifras mismas. Cada
   una va en su propia celda de ancho fijo —app.js las envuelve— y ahí sí el
   vaivén es cero.
   La caja lleva además un piso, que es lo que ocupa el contador con dos cifras
   de día: así el día que pase de 10 a 9 días la barra tampoco se mueve. Los
   valores están medidos sobre estas fuentes a estos cuerpos; si cambia alguna
   de las dos cosas, hay que rehacerlos. */
.countdown__value {
  font: 400 var(--text-md)/var(--leading-none) var(--type-heading);
  min-width: 8.17em; text-align: center;   /* el ancho medido con dos cifras de día */
}
/* La celda mide .6em, el ancho del 0, que en Playfair es la cifra más ancha; la
   más angosta, el 1, mide .37. */
.countdown__value .cifra { display: inline-block; width: .6em; text-align: center; }
/* "días" vuelve a la script, al mismo cuerpo que "Faltan". */
.countdown__value .palabra { font: 400 var(--text-lg)/var(--leading-none) var(--type-display); }

/* ═══════════════ Piezas compartidas ═══════════════ */

/* Monograma recortado por máscara — hereda el color del contenedor */
.ornament { display: flex; align-items: center; gap: var(--space-3); }
.ornament--center { justify-content: center; width: 100%; max-width: 236px; margin: 0 auto var(--space-5); }
/* Solo el monograma, centrado y sin reglas a los lados */
.ornament__line { flex: 1; height: 1px; }
.ornament--center .ornament__line:first-child { background: linear-gradient(to right, transparent, var(--color-line-on-light)); }
.ornament--center .ornament__line:last-child  { background: linear-gradient(to left,  transparent, var(--color-line-on-light)); }

/* Mismo ornamento, sobre fondo oscuro */
.ornament--light.ornament--center .ornament__line:first-child { background: linear-gradient(to right, transparent, var(--color-line-on-dark)); }
.ornament--light.ornament--center .ornament__line:last-child  { background: linear-gradient(to left,  transparent, var(--color-line-on-dark)); }

.ornament__mark {
  flex: none; display: block;
  -webkit-mask: var(--img-mono) center / contain no-repeat;
  mask: var(--img-mono) center / contain no-repeat;
}
/* 36 y no 18. El monograma es dibujo de línea: en el original de 264px sus
   trazos más finos miden 3px, o sea el 1,25% del ancho. A 18px eso daba 0,22px
   de CSS —0,67 en un teléfono 3x—, por debajo de un píxel de pantalla, así que
   el antialiasing lo disolvía y el ornamento se veía como dos líneas y un hueco.
   A 36 el trazo fino queda en 1,35px de pantalla y el dibujo aparece. */
.ornament__mark--ivory { width: 36px; height: 41px; opacity: .65; background: var(--color-paper); }

/* Botones */
/* ═══════════════ Botón ═══════════════ */
/* Un solo componente para todos los CTA de la pieza. Antes había tres lenguajes
   distintos: el del hero escrito aparte, el sólido a sangre de las secciones y
   el del pop-up. Ahora hay dos variantes y la superficie decide los colores. */
.btn {
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-1);
  /* Ni a sangre ni diminuto. A sangre se lee como una barra de la sección y no
     como un control; el piso de 220px lo mantiene con presencia de botón.
     El min() es para que adentro del pop-up, donde la caja son 192px a 320,
     se achique en vez de desbordar. */
  width: fit-content; min-width: min(var(--btn-ancho-min), 100%); max-width: 100%;
  min-height: 52px;
  margin-inline: auto;
  padding: 0 var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  /* 400 y no 500: en versalitas de 12px el medio punto de peso de más las
     engorda y las vuelve pesadas. */
  font: 400 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-btn); text-indent: var(--tracking-btn);
  text-transform: uppercase;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}

/* Relleno en el color contrario a la superficie, texto en el de la superficie. */
.btn--primary {
  background: var(--btn-contrast);
  border-color: var(--btn-contrast);
  color: var(--btn-surface);
}
/* Contorno de 1px y fondo transparente. */
.btn--secondary {
  background: transparent;
  border-color: var(--btn-contrast);
  color: var(--btn-contrast);
}


.btn:active { transform: scale(.98); }
.btn:disabled {
  background: transparent;
  border-color: var(--color-disabled-border-on-dark);
  color: var(--color-disabled-on-dark);
  cursor: default;
}
.btn:disabled:active { transform: none; }
.btn.is-sending { opacity: .7; cursor: progress; }

@media (hover: hover) {
  .btn--primary:hover:not(:disabled)   { background: transparent; color: var(--btn-contrast); }
  .btn--secondary:hover:not(:disabled) { background: var(--btn-contrast); color: var(--btn-surface); }
}
/* El pulso de lacre al tocar un botón. Es el único uso del bordó fuera de los
   momentos ceremoniales, y se justifica porque ES el lacre: el sello de la
   portada reaparece un instante bajo el dedo. */
@keyframes sealPulse {
  from { opacity: .5;  transform: scale(.25); }
  to   { opacity: 0;   transform: scale(5); }
}
.btn::after {
  content: '';
  position: absolute; left: 50%; top: 50%;
  width: 18px; height: 18px; margin: var(--nudge-neg) 0 0 var(--nudge-neg);
  border: 1px solid var(--color-seal);
  border-radius: var(--radius-round);
  opacity: 0;
  pointer-events: none;
}
.btn.is-sealing::after { animation: sealPulse var(--duration-base) var(--ease-out); }
/* Sobre el relleno del primary el bordó no se vería: ahí el anillo toma el
   color de la superficie. */
.btn--primary::after { border-color: var(--btn-surface); }

/* ═══════════════ III · SAVE THE DATE ═══════════════ */
/* Plano, sin grano ni radiales: el paso desde la portada tiene que leerse como
   cambio de tono, no de material. Comparte el marrón con el header, así que
   entre las dos secciones no hay corte de color. */
.save {
  --save-pad: var(--gutter-display);
  padding: var(--space-7) var(--save-pad) var(--space-8);
  background: var(--color-ink);
  color: var(--color-paper);
  display: flex; flex-direction: column; align-items: center; text-align: center;
}
/* Mismo ancho y mismo radio que el marco de la portada, para que los márgenes
   laterales sigan la misma línea al bajar. La foto es un recorte con
   transparencia, así que el marco aporta el fondo.
   El relleno superior está en % para acompañar a la cúpula: la curva escala
   con el ancho y el aire que necesita la tarjeta, también. */
/* A sangre: cancela el padding lateral de la sección y ocupa todo el ancho de
   la pieza. El recorte está cortado justo en los bordes de la tarjeta, así que
   la tarjeta llega de un borde al otro sea cual sea el ancho de pantalla. */
.save__photo {
  align-self: stretch;
  margin: 0 calc(-1 * var(--save-pad)) var(--space-6);
  position: relative;
  container-type: inline-size; /* la fecha escala con el ancho del bloque */
  line-height: var(--leading-flush);
}
.save__photo img { display: block; width: 100%; height: auto; }

/* Centrada sobre la tarjeta, que ocupa del 0,3% al 41,3% del alto del recorte */
.save__date {
  position: absolute;
  top: 4%; bottom: 62.5%; left: 12%; right: 12%;
  display: flex; align-items: center; justify-content: center;
  margin: 0; text-align: center;
  font-family: var(--type-heading); font-weight: 400;
  font-size: var(--text-xl);   /* respaldo si no hay unidades de contenedor */
  font-size: 8.6cqw;
  line-height: var(--leading-tight);
  color: var(--color-ink);
}

.save__cal { width: 100%; max-width: 300px; margin: var(--space-7) auto 0; }
.save__cal-head {
  font: 400 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-label); text-indent: var(--tracking-label);
  margin-bottom: var(--space-4); color: var(--color-paper-faint);
}
.save__grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--space-1);
  font: 400 var(--text-md)/var(--leading-none) var(--type-heading); color: var(--color-paper);
  /* Lining acá también: es una grilla y las elzevirianas dejan los días
     desalineados entre filas, además de dejar el 12 bajo dentro de su círculo. */
  font-variant-numeric: tabular-nums lining-nums;
}
.save__dow { font: 400 var(--text-xs)/var(--leading-none) var(--type-label); letter-spacing: var(--tracking-label); color: var(--color-paper-faint); }
.save__day { padding: var(--space-2) 0; }
.save__day--on { background: var(--color-paper); color: var(--color-ink); border-radius: var(--radius-round); }
.save__cal .btn { margin-top: var(--space-6); }

/* ═══════════════ IV · UBICACIÓN ═══════════════ */
/* El relleno de arriba tiene que dejar pasar al festón, que es alto y escala
   con el ancho: 104px sobre una pieza de 375 (27,73%) y 122px sobre una de 440.
   Con un valor fijo el monograma quedaba tapado por el corte en pantallas
   anchas, así que el despeje va en % —que resuelve contra el ancho de la
   pieza, igual que el festón— más el aire propio de la sección. */
.venue {
  padding: calc(var(--feston-alto) + var(--space-6)) var(--gutter) var(--space-8);
  /* Sin grano: era un enrejado de líneas de 1px cada 3 y 4px que se leía como
     retícula, y además no se podía reproducir dentro del festón —el corte se
     pinta con un color plano— así que dibujaba un escalón justo en la junta. */
  background:
    radial-gradient(115% 55% at 50% 0%, var(--color-glow-on-dark), transparent 68%),
    var(--color-ink);
  color: var(--color-paper); text-align: center;
}
/* La pregunta es el título de la sección y el h2: la dirección es la respuesta
   y va más grande, pero como dato, no como encabezado. Mismo cuerpo y misma
   serif que "¿Nos acompañás?", que es la otra pregunta de la pieza. */
.save__title,
.venue__title {
  margin: 0;
  font: 400 var(--text-xl)/var(--leading-tight) var(--type-heading);
  color: var(--color-paper);
}
.venue__place { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-5); }
/* El CTA respira contra la dirección. */
.venue .btn { margin-top: var(--space-6); }
/* Tres niveles: el lugar, la calle y la localidad. Antes la calle hacía de
   nombre del lugar y "Almte." terminaba a 40px, donde una abreviatura se lee
   como un error de tipeo. El display ahora es el nombre del salón; la calle
   baja a cuerpo de dirección. */
.venue__name {
  margin: 0;
  /* Piso en --text-xl para que nunca quede por debajo del título de la
     sección: la pregunta y la respuesta empatan a 320 y la respuesta gana a
     partir de ahí. */
  font: 400 clamp(var(--text-xl), 8.6vw, var(--text-2xl))/var(--leading-tight) var(--type-heading);
  color: var(--color-paper);
}
.venue__street {
  margin: 0;
  font: 400 var(--text-md)/var(--leading-snug) var(--type-body);
  color: var(--color-paper);
}
/* La localidad sube de cuerpo y deja de ser lo más tenue del bloque: hay varias
   calles "Almte. Brown" en el conurbano sur, así que es información crítica y
   no un pie de firma. */
.venue__city {
  margin: 0;
  font: italic 400 var(--text-base)/var(--leading-snug) var(--type-heading);
  color: var(--color-paper-muted);
}
/* Sin reglas horizontales: la hora se apila debajo de la dirección y el aire
   es el que separa los bloques. */
/* ── El circuito de la noche ──
   Un riel vertical con tres hitos. Va debajo del CTA porque primero se resuelve
   cómo llegar y después qué pasa cuando llegás.
   La línea crece con el scroll usando animation-timeline: view(), o sea sin una
   línea de JavaScript. Donde el navegador no lo soporta —iOS viejo— el bloque
   se ve entero y quieto, que es exactamente lo que tiene que pasar: la
   animación es el adorno, el horario es la información. */
.circuito {
  /* Diámetro del punto. Todo el eje del circuito se deriva de acá, así el riel
     y los puntos no se pueden desincronizar por una cuenta hecha a mano. */
  --punto: 7px;
  position: relative;
  width: fit-content;
  margin: var(--space-6) auto 0;
  padding-left: var(--space-4);
  list-style: none;
  text-align: left;
}
/* El riel. Nace arriba para poder crecer hacia abajo.
   Mide 1px, así que para que su eje caiga en el centro del punto tiene que
   arrancar medio punto menos medio píxel: (7 − 1) / 2 = 3, eje en 3,5.
   Arriba y abajo tiene que morir en el centro del primer y del último punto.
   Arriba sale solo: el punto está a .5em de su hito y el riel también.
   Abajo hay que descontar lo que le sobra al último hito por debajo de su
   punto, y ese alto es el de la hora —el hijo más alto, que es quien manda la
   línea de base—: --text-lg menos el .5em al que cuelga el punto. */
.circuito::before {
  content: '';
  position: absolute;
  top: .5em; bottom: calc(var(--text-lg) - .5em);
  left: calc((var(--punto) - 1px) / 2);
  width: 1px;
  background: var(--color-paper-faint);
  transform-origin: top;
}
.circuito__hito { position: relative; display: flex; align-items: baseline; gap: var(--space-2); }
.circuito__hito + .circuito__hito { margin-top: var(--space-5); }
/* El punto se posiciona contra el hito, que arranca después del padding de la
   lista: para volver al borde izquierdo de .circuito hay que descontar ese
   padding exacto. Antes acá había un −26px suelto que no correspondía a ningún
   lado y dejaba el punto 2px a la izquierda del riel. */
.circuito__hito::before {
  content: '';
  position: absolute; left: calc(-1 * var(--space-4)); top: .5em;
  width: var(--punto); height: var(--punto);
  margin-top: calc(-1 * var(--punto) / 2);
  border-radius: var(--radius-round);
  background: var(--color-paper);
}
.circuito__hora {
  font: 400 var(--text-lg)/var(--leading-none) var(--type-heading);
  font-variant-numeric: tabular-nums;
  color: var(--color-paper);
}
.circuito__que {
  font: 400 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-paper-faint);
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .circuito::before {
      animation: circuitoCrece linear both;
      animation-timeline: view();
      /* En `cover` y no en `entry`: el bloque mide 115px, así que su fase de
         entrada dura 115px de scroll y la línea terminaba de crecer apenas
         asomaba. `cover` se extiende sobre el alto de la ventana más el del
         bloque —unos 930px— y el 5-45% de eso son ~370px de recorrido, que es
         donde se siente que la línea acompaña al dedo. */
      animation-range: cover 5% cover 45%;
    }
    .circuito__hito {
      animation: hitoAparece linear both;
      animation-timeline: view();
    }
    /* Cada hito se enciende cuando el riel ya lo pasó. */
    .circuito__hito:nth-child(1) { animation-range: cover 10% cover 20%; }
    .circuito__hito:nth-child(2) { animation-range: cover 20% cover 30%; }
    .circuito__hito:nth-child(3) { animation-range: cover 30% cover 40%; }
  }
}
@keyframes circuitoCrece { from { transform: scaleY(0); } to { transform: scaleY(1); } }

/* El mismo movimiento para los navegadores sin animation-timeline: Safari en
   iPhone recién lo trae desde la 26, así que hasta ahí el bloque de arriba no
   se aplicaba y la sección quedaba quieta. Lo mueve un IntersectionObserver
   desde app.js.
   El estado inicial cuelga de .js-anim, una clase que ese mismo script le pone
   al <html>: si el JS no corre, nada se esconde y todo se ve como siempre. */
.js-anim .circuito::before { transform: scaleY(0); transform-origin: top; transition: transform 1s var(--ease-out); }
.js-anim .circuito.dentro::before { transform: scaleY(1); }
.js-anim .circuito__hito { opacity: 0; transform: translateY(10px); transition: opacity .5s var(--ease-out), transform .5s var(--ease-out); }
.js-anim .circuito__hito.dentro { opacity: 1; transform: none; }
/* El escalonado: cada hito entra después del anterior, como el riel que los va
   pasando. */
.js-anim .circuito__hito:nth-child(1).dentro { transition-delay: .15s; }
.js-anim .circuito__hito:nth-child(2).dentro { transition-delay: .35s; }
.js-anim .circuito__hito:nth-child(3).dentro { transition-delay: .55s; }
@keyframes hitoAparece  { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ═══════════════ V · DRESS CODE ═══════════════ */
/* Al espejar el corte, el punto más hondo pasó del borde al centro —justo
   donde va el contenido, que está centrado—, así que el despeje tiene que
   cubrir el festón entero. Mismo criterio que Ubicación: el 27,73% es la
   proporción del corte (104/375) y resuelve contra el ancho de la pieza, así
   que los rombos quedan a 30px del corte a cualquier ancho. */
/* El papel grabado reemplaza al marfil plano. Va anclado al borde de ABAJO y
   repetido, de modo que un límite de baldosa cae exactamente en el pie de la
   sección; el festón de Confirmación arranca ahí anclado arriba, con la misma
   imagen y la misma escala, así que la textura sigue de largo por debajo del
   corte y no queda la línea recta que se veía antes.
   Para que eso funcione la baldosa tiene que ser continua consigo misma: el
   archivo está fundido en vertical (ver README).
   Los dos radiales que tenía el marfil se van: no se pueden continuar dentro
   del festón —están anclados a las esquinas de esta sección— y eran justo lo
   que dibujaba el escalón de tono en el borde. */
.terciopelo {
  /* El color de abajo es la media medida del archivo (63,25,24): mientras la
     imagen carga se ve terciopelo y no un marfil que ya no existe acá. */
  background: var(--img-velvet) center bottom / 100% auto repeat, var(--color-surface-velvet);
}
.dress {
  /* Sin relleno arriba: la foto arranca pegada al borde de la sección para que
     el festón le caiga encima y la enmarque. Antes el relleno reservaba el alto
     del festón y dejaba una banda de terciopelo entre el corte y la imagen; esa
     banda era justamente lo que hacía que el marco enmarcara el fondo en vez de
     la foto.
     Abajo sí necesita respiro propio: la sección cerraba a ras porque ahí iba
     una hilada de azulejos que la terminaba, y ya no está. */
  padding: 0 var(--gutter) var(--space-7);
  /* Fondo propio, y negro. El terciopelo lo pinta .terciopelo para las dos
     secciones que envuelve; acá se tapa, así que el bordó queda sólo para
     Confirmación y el corte entre las dos pasa a verse.
     El negro es el canto medido de la foto: la mirilla no se recorta contra el
     fondo, se disuelve en él. */
  background: var(--color-surface-mirilla);
  color: var(--color-paper); text-align: center;
  /* La medida de lectura de la sección. La comparten la bajada, los dos
     remates y —desde ahora— la fila de HOMBRES/MUJERES: con los specs más
     angostos que la frase de abajo, el bloque se leía como dos anchos
     distintos apilados. Estaba escrita tres veces a mano; ahora una sola. */
  --medida: 284px;
}
/* Los rombos ya no abren la sección —eso lo hace la foto— ni separan la foto
   de los datos: ahora van al medio de HOMBRES y MUJERES, apilados, haciendo de
   eje entre los dos. En vertical porque separan una fila, no dos bloques.
   Y más chicos: al lado de dos palabras sueltas, a 5 y 7px pesaban más que los
   datos que separan. */
.diamonds { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-1); }
.diamond { display: block; transform: rotate(45deg); }
.diamond--sm { width: 3px; height: 3px; border: 1px solid var(--color-paper-faint); }
.diamond--lg { width: 5px; height: 5px; background: var(--color-paper); }

/* ── La mirilla ──────────────────────────────────────────────────────────
   La foto que antes cerraba la página ahora abre esta sección, y el título va
   encima de la carta que se ve por el hueco.
   Va a sangre y no con el margen de la sección: el marco lo pone la foto
   —el óvalo oscuro de la mirilla— y meterla dentro del margen le agregaría un
   segundo marco de terciopelo que compite con el primero.
   Las coordenadas de la carta están medidas sobre el archivo, no estimadas:
   centro en 50,77% del ancho y 51,30% del alto; la carta mide 18,55% × 7,21%.
   Al ir en porcentaje, el título sigue clavado sobre ella en cualquier ancho. */
.mirilla {
  position: relative;
  margin: 0 calc(-1 * var(--gutter)) var(--space-5);
}
.mirilla__foto { display: block; width: 100%; height: auto; }

/* El título va arriba de la carta, en el arco del círculo de la mirilla.
   El 28% no es a ojo: el hueco abre en 7,2% del alto y su punto más ancho cae
   en 30,3%, así que el 28 pone la frase justo en la curva superior. Y es el
   primer valor que da: por debajo del 28 el título arranca antes del 15,6%,
   que es hasta donde baja el festón, y el corte se le vendría encima.
   Sigue anclado en x al eje de la carta (50,77%) para que la frase, la carta y
   la mesa compartan un mismo eje vertical.
   Acá ya no pisa el rosa de la carta —donde el marfil daba 1,62:1 y
   desaparecía— pero la sombra se queda igual: abajo hay cortina verde con
   pliegues iluminados, y es el mismo recurso con el que los nombres de la
   portada sobreviven al cielo claro del cuadro. */
.mirilla__title {
  position: absolute;
  left: 50.77%; top: 28%;
  transform: translate(-50%, -50%);
  width: 100%;
  text-shadow: var(--shadow-text-strong);
}

/* Dos líneas: "vestimenta" sola ya ocupa casi lo mismo que decía "Dress code"
   entera, así que en una sola línea habría que bajar a ~40px y el título
   dejaría de ser el título. El interlineado sube de .92 a 1.02 porque la
   Pinyon tiene ascendentes y descendentes largas y a .92 se tocaban. */
.dress__title {
  font-family: var(--type-display); font-weight: 400;
  font-size: var(--text-display-md); line-height: var(--leading-tight); color: var(--color-paper);
  margin: 0;
}
/* Hombres y Mujeres son dos datos del mismo tipo: van en fila, y cada uno con
   su etiqueta arriba en versalitas. Es el único lugar de la pieza donde el
   recurso de overline sirve para rotular un dato y no para encabezar una
   sección, y funciona porque son dos palabras sueltas.
   El marfil al 58% de la etiqueta da 6,27:1 sobre el negro de la sección, con
   los 4,5 que pide un texto de 12px de exigencia. Sobre el terciopelo, que era
   el fondo anterior, daba 4,92 y pasaba raspando; el negro le dio aire. Si
   alguna vez se aclara este fondo, sigue siendo el primer texto que se cae. */
/* Grilla 1fr auto 1fr y no un flex con gap: así el rombo cae en el centro
   exacto del bloque aunque HOMBRES mida 77 y MUJERES 73,8. Con space-between
   quedaba 1,6px corrido, que en un rombo de 3px se nota.
   Y los dos datos se van a los extremos —justify-self— para que el bloque
   ocupe la misma medida que la frase de abajo. Antes su tinta llegaba a 203,7
   contra los 269 de la frase: 32,6px de menos por lado. */
.dress__specs {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  width: var(--medida); max-width: 100%;
  margin: var(--space-5) auto 0;
}
.dress__spec:first-of-type { justify-self: start; }
.dress__spec:last-of-type  { justify-self: end; }
.dress__spec { margin: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.dress__spec dt {
  font: 400 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-label); color: var(--color-paper-faint);
}
.dress__spec dd {
  margin: 0;
  font: 400 var(--text-lg)/var(--leading-none) var(--type-heading);
  color: var(--color-paper);
}

.dress__copy {
  font: 400 var(--text-lectura)/var(--leading-loose) var(--type-body);
  color: var(--color-paper-muted);
  max-width: var(--medida); margin: var(--space-5) auto var(--space-4);
  text-wrap: pretty;
}
/* Una fila por color, con el nombre al costado. Antes era una grilla 2×2 y
   los nombres iban debajo; al costado la lectura es directa —pieza, nombre— y
   la zona respira, que era lo que le faltaba.
   El contenedor es fit-content y las filas se estiran a él, así que las cuatro
   miden lo mismo: las piezas quedan alineadas en una columna y los nombres
   arrancan todos en el mismo x, sin escribir un ancho a mano. */
.palette {
  display: flex; flex-direction: column; gap: var(--space-5);
  width: fit-content; max-width: 100%;
  margin: var(--space-6) auto 0;
  padding: 0; list-style: none;
}
.palette__item { display: flex; align-items: center; gap: var(--space-4); }
/* 14 y no 12: al lado de una pieza de 128px el cuerpo de overline quedaba
   perdido. A la izquierda y no centrado: es la etiqueta de la pieza que tiene
   al lado, y los cuatro nombres arrancan en el mismo x. */
.palette__name {
  text-align: left;
  font: 400 var(--text-sm)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-btn); color: var(--color-paper-muted);
}
/* La muestra es una baldosa, no un disco de color plano.
   Cuadrada y con el radio de los controles, no redonda: el dibujo de estas
   piezas vive en las esquinas —las palmetas del bordó, los cuartos de hoja del
   celeste— y recortarlas en círculo se come justo eso.
   El marco no es decoración, es lo que hace que la pieza exista. Las cuatro
   baldosas son oscuras y el fondo también: contra el negro de la sección el
   canto de cada una da entre 1,64 y 3,53:1, o sea que ninguna se separa sola.
   Va en --color-control-on-dark —marfil al 54%— porque un objeto gráfico
   necesita 3:1, y el aro compuesto sobre el canto de cada pieza da entre 7,32
   y 9,66 contra el fondo. Sobre el terciopelo, que era el fondo anterior, daba
   entre 5,45 y 7,19: el cambio a negro lo mejoró.
   El color plano queda de respaldo debajo de la imagen: si la baldosa no carga,
   la sección sigue comunicando el color, que es el dato que importa. */
/* Girada 45° para que la pieza se lea como rombo y no como azulejo.
   El scale(.7071) es 1/√2 y no es decorativo: un cuadrado girado 45° ocupa su
   diagonal, o sea 1,414 veces su lado, y sin compensar se comería el hueco
   entre filas y se metería debajo del nombre. Con ese factor la caja de
   maquetado vuelve a medir exactamente --space-8 y el rombo entra clavado
   adentro. Lado visible: 128 / √2 = 90,5px.
   El tamaño sale de la escala de espaciado a propósito: no hay una escala de
   tamaños de imagen y no valía la pena inventar una para un solo elemento. */
.swatch {
  flex: none;
  width: var(--space-8); aspect-ratio: 1;
  border-radius: var(--radius-sm);
  background-position: center; background-size: cover; background-repeat: no-repeat;
  box-shadow: inset 0 0 0 2px var(--color-control-on-dark);
  transform: rotate(45deg) scale(.7071);
}
.swatch--marron  { background-color: var(--swatch-marron);  background-image: var(--img-mosaico-marron); }
.swatch--verde   { background-color: var(--swatch-verde);   background-image: var(--img-mosaico-verde); }
.swatch--bordo   { background-color: var(--swatch-bordo);   background-image: var(--img-mosaico-bordo); }
.swatch--celeste { background-color: var(--swatch-celeste); background-image: var(--img-mosaico-celeste); }


/* Era una frase corta de tres golpes y podía darse el lujo de la itálica de
   display. Ahora es la única indicación práctica de la sección —el calzado
   cómodo cambia lo que el invitado mete en el bolso—, así que va en la de
   cuerpo: en itálica de display un dato útil se lee como floritura. */
/* Las piezas van apareciendo con el scroll y el nombre entra después, desde el
   costado. Es CSS puro: animation-timeline: view() ata la animación al paso del
   elemento por la ventana, sin un listener ni una librería. El mismo recurso ya
   mueve el riel del circuito de horarios en Ubicación.
   Dos resguardos, los dos deliberados. El @supports deja fuera a los
   navegadores que no lo tienen —hoy Safari y Firefox—, y ahí las filas se ven
   completas y quietas, que es el estado por defecto: nunca hay contenido
   escondido esperando un scroll que quizá no llegue. Y el prefers-reduced-motion
   apaga todo para quien pidió que la pantalla no se mueva.
   El nombre arranca 7 puntos después que su pieza: primero aparece el mosaico y
   recién entonces entra la etiqueta, que es el orden en que se lee. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .palette__item {
      animation: mosaicoEntra linear both;
      animation-timeline: view();
      animation-range: cover 12% cover 38%;
    }
    .palette__name {
      animation: nombreEntra linear both;
      animation-timeline: view();
      animation-range: cover 19% cover 45%;
    }
  }
}
@keyframes mosaicoEntra { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes nombreEntra  { from { opacity: 0; transform: translateX(-14px); } to { opacity: 1; transform: none; } }

/* Mismo fallback que el circuito, por lo mismo. El nombre entra 150ms después
   que su mosaico, que es el orden en que se leen. */
.js-anim .palette__item { opacity: 0; transform: translateY(18px); transition: opacity .55s var(--ease-out), transform .55s var(--ease-out); }
.js-anim .palette__item.dentro { opacity: 1; transform: none; }
.js-anim .palette__name { opacity: 0; transform: translateX(-14px); transition: opacity .55s var(--ease-out) .15s, transform .55s var(--ease-out) .15s; }
.js-anim .palette__item.dentro .palette__name { opacity: 1; transform: none; }

/* Dos remates, dos pesos. El primero cierra lo que la sección viene diciendo y
   va en marfil pleno; el segundo es el dato práctico —el calzado cómodo cambia
   lo que el invitado mete en el bolso— y va apagado. Separarlos en dos frases
   de dos colores da la jerarquía sin necesidad de leerlos.
   Los dos en la de cuerpo y no en la itálica de display: ahí un dato útil se
   lee como floritura. */
.dress__note {
  font: 400 var(--text-lectura)/var(--leading-loose) var(--type-body);
  color: var(--color-paper);
  max-width: var(--medida); margin: var(--space-6) auto 0;
  text-wrap: pretty;
}
.dress__note--dato {
  color: var(--color-paper-muted);
  margin-top: var(--space-3);
}

/* ═══════════════ VI · CONFIRMACIÓN ═══════════════ */
/* Sin foto, sin velo y sin corte: la sección es el mismo papel que la anterior,
   lo pinta .papel, que envuelve a las dos. */
.rsvp {
  /* Sin el marco, --aire-marco perdió sentido: existía para que el aire de
     arriba y el de abajo del óvalo salieran del mismo valor y no se pudieran
     desincronizar. Ahora esto es simplemente el aire entre Vestimenta y el
     título de esta sección. Ahora además tiene que dejar pasar el festón, que
     mide 27,73% del ancho: sin eso el corte se comería el título.
     Abajo va lo mismo que arriba y no --space-7. El marfil del pie no empieza
     donde termina la sección: trepa 104px por encima —su festón sale hacia
     arriba— así que con 96px de relleno quedaban MENOS SIETE de bordó libre
     debajo de los botones, o sea que el corte les pasaba por encima, contra 40
     de aire arriba del título. Repitiendo el mismo cálculo, abajo quedan
     también 40 y la sección respira parejo. */
  padding: calc(var(--feston-alto) + var(--space-5)) var(--gutter);
  color: var(--color-paper);
  text-align: center;
  scroll-margin-top: 0;
}

/* 340 y no 300: las filas de alias y CVU necesitan lugar para el monoespaciado
   más el botón de copiar. El texto corrido se limita aparte, que a 340 leería
   con renglones largos. */
.rsvp__body { width: 100%; max-width: 340px; margin: 0 auto; }
.rsvp__title { font: 400 var(--text-xl)/var(--leading-tight) var(--type-heading); color: var(--color-paper); margin: 0; }
.rsvp__title em { font-style: italic; }
.rsvp__copy {
  font: 400 var(--text-lectura)/var(--leading-loose) var(--type-body);
  color: var(--color-paper-muted);
  margin: var(--space-3) auto var(--space-4); max-width: 292px;
}

/* ─── Formulario en pasos ───────────────────────────────────
   Una pregunta por vez. Con dos campos cortos no haría falta, pero en una
   invitación el ritmo de "una cosa a la vez" acompaña mejor que un bloque de
   controles, y en celular cada objetivo queda más grande.
   Del componente de referencia se toman el indicador de pasos, el contador
   1/2, el botón que cambia de etiqueta y el volver. Su barra de progreso no:
   con dos pasos repite lo que ya dicen los círculos. */
.pasos {
  display: flex; align-items: center; justify-content: center;
  list-style: none; margin: 0 0 var(--space-4); padding: 0;
}
.pasos__item { display: flex; align-items: center; }
.paso {
  position: relative; flex: none;
  width: 34px; height: 34px; border-radius: var(--radius-round);
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--color-control-on-dark);
  color: var(--color-paper-muted); background: transparent;
  font: 500 var(--text-sm)/var(--leading-none) var(--type-label);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background var(--duration-base) var(--ease-out), color var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out);
}
/* El área táctil llega a 44px sin agrandar el círculo: el punto se dibuja de
   34 y la zona sensible se estira con un pseudo-elemento.
   -6 y no -5: el pseudo se posiciona contra la caja de relleno, que descuenta
   el borde de 1px de cada lado, así que arranca de 32 y no de 34. */
.paso::before { content: ''; position: absolute; inset: -6px; border-radius: var(--radius-round); }
.paso:disabled { cursor: default; opacity: .45; }
.paso--activo {
  background: var(--color-paper); border-color: var(--color-paper); color: var(--color-surface-velvet);
}
.paso--hecho { background: var(--color-fill-on-dark); border-color: var(--color-paper); color: var(--color-paper); }
.paso__n, .paso__ok { display: block; }
.paso__ok {
  width: 15px; height: 15px; fill: none;
  stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.paso .paso__ok, .paso--hecho .paso__n { display: none; }
.paso--hecho .paso__ok { display: block; }

.pasos__linea { display: block; width: 44px; height: 1px; margin: 0 var(--space-2); background: var(--color-line-on-dark); }
.pasos__linea i {
  display: block; height: 100%; background: var(--color-paper);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--duration-base) var(--ease-out);
}
.pasos__item--hecho .pasos__linea i { transform: scaleX(1); }

/* La etiqueta y el control eran hermanos de bloque sin nada en medio, así que
   "¿VAS A VENIR?" quedaba apoyado sobre el borde del toggle. */
/* El paso que se va se apaga primero y recién entonces entra el que sigue: sin
   eso el cambio era un corte seco, un campo desaparecía y otro aparecía en el
   mismo cuadro. Los dos pasos difieren 8px de alto, medido, así que el salto
   queda tapado por el cruce y no hace falta animar la altura. */
.campo { display: flex; flex-direction: column; gap: var(--space-2); animation: pasoIn var(--duration-base) var(--ease-out) both; }
.campo--sale { animation: none; opacity: 0; transform: translateY(-8px);
               transition: opacity var(--duration-fast) var(--ease-out),
                           transform var(--duration-fast) var(--ease-out); }
@keyframes pasoIn { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }

/* Lo mismo para el paso de la invitación al formulario. */
.rsvp__sale { opacity: 0; transform: translateY(-8px);
              transition: opacity var(--duration-fast) var(--ease-out),
                          transform var(--duration-fast) var(--ease-out); }
.campo__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.campo__n {
  font: 400 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-label); color: var(--color-paper-muted);
  font-variant-numeric: tabular-nums;
}

/* El botón y "Volver" en fila. La caja de "Volver" es una columna de grilla que
   va de 0fr a 1fr: `fr` sí interpola, mientras que de 0 a `auto` no, así que es
   la forma de que la caja se abra desde cero hasta el ancho natural de su
   contenido sin escribir ese ancho a mano. Con overflow oculto, en el primer
   paso el botón queda solo y centrado, y al pasar al segundo "Volver" se abre
   corriéndolo, en lugar de aparecer de golpe.
   Simétrico no entra: dejarle lugar reservado a los dos lados para que el botón
   quede en el centro exacto pide 176 + 2×86 = 348px y a 320 hay 272. Así, en
   fila, el par mide 262,3 y entra. */
.rsvp__acciones {
  /* "Volver" primero: es la salida, y en el orden de lectura la salida va antes
     que la acción que empuja hacia adelante. La columna que se abre es la
     primera, así que el par arranca cerrado en `0fr auto`. */
  display: grid; grid-template-columns: 0fr auto;
  align-items: center;
  /* fit-content y no ancho completo: en una grilla que ocupa todo el ancho el
     `1fr` se queda con el espacio libre —medido, la caja se iba a 220px y el
     botón se pegaba al borde izquierdo—. Ajustada al contenido, el `fr` resuelve
     al ancho natural de "Volver" y el par queda centrado. */
  width: fit-content; max-width: 100%; margin-inline: auto;
  column-gap: 0;
  transition: grid-template-columns var(--duration-base) var(--ease-out),
              column-gap var(--duration-base) var(--ease-out);
}
.rsvp__acciones.tiene-volver { grid-template-columns: 1fr auto; column-gap: var(--space-2); }
.rsvp__volver-caja {
  overflow: hidden;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
}
.tiene-volver .rsvp__volver-caja { opacity: 1; transition-delay: var(--duration-fast); }

.rsvp__volver {
  white-space: nowrap;
  min-height: 44px; padding: 0 var(--space-3); cursor: pointer;
  font: italic 400 var(--text-sm)/var(--leading-none) var(--type-heading);
  color: var(--color-paper-muted);
  text-decoration: underline; text-underline-offset: 4px;
  text-decoration-color: var(--color-paper-faint);
  transition: color var(--duration-fast) var(--ease-out), text-decoration-color var(--duration-fast) var(--ease-out);
}
@media (hover: hover) { .rsvp__volver:hover { color: var(--color-paper); text-decoration-color: var(--color-paper); } }


/* Solo para lectores de pantalla: anuncia el cambio de paso */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.rsvp__form {
  display: flex; flex-direction: column; gap: var(--space-3);
  margin-top: var(--space-4); text-align: left;
  /* base y no slow: el alto de la caja crece en --duration-base, y con el
     desvanecido al doble el formulario seguía apareciendo cuando el despliegue
     ya había terminado. */
  animation: fadeIn var(--duration-base) var(--ease-out) both;
}
.field__label {
  font: 400 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-label); color: var(--color-paper-muted);
}
.field__input {
  border: none; border-bottom: 1px solid var(--color-control-on-dark);
  /* min-height 44: la regla táctil vale también para los campos, y con
     16px de texto y 7px de relleno el alto daba 36. */
  /* 9px no sale de la escala: es lo que hace falta para que el campo llegue
     a los 44px de área táctil con 16px de texto. */
  background: transparent; padding: 9px 0; min-height: 44px;
  /* 16px o iOS hace zoom al enfocar el campo */
  font: 400 var(--text-base)/var(--leading-snug) var(--type-heading); color: var(--color-paper);
  width: 100%; border-radius: 0;
  transition: border-color var(--duration-fast) var(--ease-out);
}
/* .60 y no .45: al 45% el placeholder daba 3,62:1 sobre el punto más claro
   del terciopelo, por debajo del 4,5 que pide un texto chico. Sigue estando
   claramente por debajo del valor escrito, que va en marfil pleno. */
.field__input::placeholder { color: var(--color-paper-faint); letter-spacing: normal; }
/* El campo no lleva el anillo genérico. Un input de texto activa
   :focus-visible también al tocarlo —no solo al tabular—, así que en un
   teléfono aparecía un rectángulo duro apenas ibas a escribir, y encima el
   offset de 3px lo dibujaba por encima de la etiqueta.
   El foco lo marca el subrayado, que cambia grosor y color a la vez: de 1px al
   54% a 2px en marfil pleno. Sigue siendo un indicador visible —2.4.7 pide que
   se vea, no que sea una caja— y no mete un marco que la composición no tiene. */
.field__input:focus-visible { outline: none; }
.field__input:focus {
  border-bottom-width: 2px;
  border-bottom-color: var(--color-paper);
  /* el px que gana el borde se descuenta del relleno, así el campo no salta */
  padding-bottom: 8px;
}

.toggle {
  /* 3px es el grosor del riel alrededor de la opción marcada, no un espacio
     de la escala. */
  display: flex; gap: var(--space-1); padding: 3px;
  border: 1px solid var(--color-control-on-dark); border-radius: var(--radius-sm);
}
.toggle__opt {
  flex: 1; display: flex; align-items: center; justify-content: center;
  min-height: 44px; border-radius: var(--radius-sm); cursor: pointer;
  font: 500 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-btn); text-indent: var(--tracking-btn);
  background: transparent; color: var(--color-paper);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.toggle__opt[aria-checked='true'] { background: var(--color-paper); color: var(--color-surface-velvet); }

/* Modificador: el submit del formulario es un .btn .btn--primary. Los estados
   deshabilitado y "enviando" los da el componente. */
.btn--submit { margin-top: var(--space-1); }
.rsvp__acciones .btn--submit { margin-inline: 0; min-width: var(--btn-ancho-min); }

.rsvp__error {
  font: 400 var(--text-sm)/var(--leading-loose) var(--type-body);
  color: var(--color-paper); margin: 0; text-align: center;
}
#rsvpDone { animation: fadeIn var(--duration-slow) var(--ease-out) both; }

/* ═══════════════ Estado final ═══════════════ */
/* Lo que queda en la página es solo lo ceremonial: centrado, serif, sobre el
   papel. Lo transaccional —que es un formulario de pago y se comporta como
   tal— vive en el pop-up de más abajo. */
/* Centrado, como todo el resto del sitio. Probado alineado a la izquierda: el
   bloque quedaba siendo el único con eje propio en una página que centra de
   punta a punta, y esa excepción se lee como un error de maquetado antes que
   como una jerarquía. */
.exito { text-align: center; padding-top: var(--space-2); }
/* Los 40 de abajo los traía .exito__cabeza, el envoltorio que apilaba el visto
   sobre el título. Sacado el visto, el envoltorio no tenía nada que apilar y la
   separación pasa a vivir acá: lo que sigue es otro bloque, y entre bloques se
   abre. */
.exito__title {
  margin: 0 0 var(--space-5);
  font: 400 var(--text-xl)/var(--leading-tight) var(--type-heading);
  letter-spacing: var(--tracking-tight); color: var(--color-paper);
}
.exito__title em { font-style: italic; }
/* Un renglón por span, con el corte decidido en app.js y no por el ancho. */
.exito__title span { display: block; }
/* El eco le devuelve al invitado lo que el formulario entendió. Sin esto no
   sabe si su nombre entró bien y termina escribiendo por WhatsApp. Los 40 de
   abajo los traía la fecha larga, que se sacó por repetida; sin ellos el eco
   quedaba pegado al primer botón. */
.exito__eco {
  margin: 0 auto var(--space-5); max-width: 30ch;
  font: 400 var(--text-md)/var(--leading-loose) var(--type-body); color: var(--color-paper-muted);
}
/* El rótulo del par. Va en 14 y en su propio renglón: bajarlo de cuerpo es la
   única señal que hace falta para que el nombre sea el dato, y evita que el
   renglón compita con el título de arriba. Sin él, el eco tenía el mismo cuerpo
   que un párrafo y el nombre parecía una palabra gritada en el medio. */
.exito__rotulo {
  display: block;
  /* --text-md y no --text-sm: va en Baskerville, y a 16px su altura de x caía a
     6,7 —el texto más chico que quedaba en toda la pieza—. A 20 da 8,4. */
  font-size: var(--text-md); line-height: var(--leading-snug);
}
/* El nombre del invitado, en la caligráfica de la portada: es el único dato de
   la pieza que le pertenece a quien está mirando la pantalla, y verlo escrito a
   mano es lo que convierte el acuse en un gesto.
   24 y no 19,5, que sería el cuerpo ópticamente neutro —la altura de x de la
   Baskerville es 1,083 veces la de Pinyon, medido—. Dos motivos: a 19,5 el
   trazo más fino de la script queda en 0,68 píxeles de pantalla en un teléfono
   2×, por debajo de uno; y esto no busca ser neutro sino destacar.
   Y en marfil pleno, no apagado como el resto del renglón: sobre el terciopelo
   pasa de 7,39:1 a 13,78, que es lo que un trazo tan fino necesita. */
.exito__nombre {
  display: block;
  margin-top: var(--space-1);
  font-family: var(--type-display);
  font-size: var(--text-lg); line-height: var(--leading-tight);
  color: var(--color-paper);
}

/* Una sola acción. Con ella se fueron .exito__nota —la aclaración del botón del
   calendario—, #rsvpDoneCal y toda la regla de .enlace, que existía nada más
   que para "Ver ubicación".
   Ya no hace falta que la pila sea fit-content ni que reparta gap entre hijos:
   tiene uno solo, y el botón se arregla con el ancho que le da .btn. */
.exito__acciones { display: flex; justify-content: center; }

/* ═══════════════ Luna de miel — pop-up ═══════════════ */
/* El pedido de plata salió del flujo y pasó a ser una capa: aparece cuando el
   formulario terminó y se cierra sin dejar rastro. La jerarquía viene de la
   referencia —display serif a dos líneas, bajada corta pegada al título, un
   bloque gris más chico, y los datos adentro de una caja de borde fino con las
   etiquetas en versalitas muy espaciadas—. Todo alineado a la izquierda: el
   invitado tiene que verificar caracteres, no contemplar una composición. */
.luna {
  /* Superficie clara: el botón se da vuelta. Primary pasa a relleno de tinta
     con texto crema, secondary a contorno de tinta. */
  --btn-contrast: var(--color-ink);
  --btn-surface:  var(--color-paper);
  position: fixed; inset: 0; z-index: 210;
  display: flex; align-items: center; justify-content: center;
  /* 34px a los costados y no 16: el panel tiene que leerse como una pieza
     apoyada sobre la página, no como una pantalla nueva pegada a los bordes. */
  padding: calc(var(--space-4) + env(safe-area-inset-top)) var(--gutter-display) calc(var(--space-4) + env(safe-area-inset-bottom));
  /* Sobre el terciopelo la tinta se invierte. Redefiniendo estos dos tokens acá
     adentro, todo lo que los hereda se da vuelta solo: son los mismos valores
     que ya usa la sección donde el pop-up se despliega. */
  /* El texto secundario sube al 90% SOLO acá. Sobre el fondo lavado del mural
     el punto más claro queda en (164,158,144) y --color-ink-muted, que es el
     65%, da 2,95:1: por debajo del 4,5 que pide un texto chico. El mínimo
     medido es .88. Es la única excepción del sistema y está acotada a esta
     superficie. */
  --color-ink-muted: rgb(var(--rgb-ink) / .90);
}
.luna__velo {
  position: absolute; inset: 0;
  /* Con el velo solo, arriba a la izquierda se seguía leyendo el visto y el
     texto cortado de la pantalla de atrás. El desenfoque los desarma sin
     oscurecer más. */
  background: var(--color-scrim);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  animation: fadeIn var(--duration-fast) var(--ease-out) both;
}
/* El marco no dibuja nada: lleva el scroll y el ancho máximo. Antes también
   existía para que el canto festoneado, que vivía fuera de la caja del panel,
   no lo recortara un overflow. Ahora la onda está adentro del panel —es su
   máscara— y ese motivo ya no corre. */
.luna__marco {
  position: relative;
  width: 100%; max-width: var(--shell);
  max-height: 100%; overflow-y: auto; -webkit-overflow-scrolling: touch;
  animation: lunaEntra var(--duration-base) var(--ease-out) both;
}
.luna__panel {
  position: relative;
  /* La onda ya no es un canto pegado arriba: recorre los cuatro bordes y el
     panel entero se recorta con ella. Seis capas de máscara que se suman —la
     suma es el modo por defecto—: las cuatro tiras de los bordes y dos
     rectángulos cruzados que rellenan el medio.
     El rectángulo del medio va metido para adentro por los CUATRO lados, no
     cruzado. Probé con dos rectángulos en cruz —uno a todo el ancho, otro a
     todo el alto— y no cortaba nada: entre los dos ya tapaban las tiras de los
     bordes y las ondas quedaban debajo de un rectángulo lleno.
     El paso va en píxeles y no en porcentaje, que es lo que antes obligaba a
     sacar el canto afuera de la caja. Los dos tamaños van escritos y no en
     `auto`: con preserveAspectRatio='none' el SVG se queda sin proporción
     propia, así que no hay de dónde deducir el lado que falta. */
  /* `round` y no `repeat`: estira apenas el paso para que en cada borde entre un
     número entero de ondas. Con `repeat` centrado, los extremos cortaban una
     onda por la mitad y en las cuatro esquinas quedaba una escuadra maciza.
     Redondeando, cada borde arranca y termina en el valle del dibujo y las
     esquinas cierran limpias. `round` sólo toca el eje que se repite, así que el
     espesor de la tira no se mueve. */
  --mascara-onda:
    var(--onda-arriba) 0    0    / var(--onda-paso) var(--onda-alto) round no-repeat,
    var(--onda-abajo)  0    100% / var(--onda-paso) var(--onda-alto) round no-repeat,
    var(--onda-izq)    0    0    / var(--onda-alto) var(--onda-paso) no-repeat round,
    var(--onda-der)    100% 0    / var(--onda-alto) var(--onda-paso) no-repeat round,
    /* +1px por lado: el rectángulo pisa un pelo las tiras en vez de tocarlas
       justo. Al ras quedaba una costura visible —el espesor de la onda da
       11,09px, o sea que el límite cae en medio de un píxel y el antialias de
       los dos lados no llega a sumar opacidad entera. */
    linear-gradient(#000 0 0) 50% 50% /
      calc(100% - 2 * var(--onda-alto) + 2px)
      calc(100% - 2 * var(--onda-alto) + 2px) no-repeat;
  -webkit-mask: var(--mascara-onda);
  mask: var(--mascara-onda);
  /* 20 y no 26 a los costados: al angostar el panel, el alias monoespaciado
     dejó de entrar en la caja de datos y hay que devolverle ancho.
     Arriba 64 y no 8: al centrar el título dejó de estar corrido a la izquierda
     y a 320px la segunda línea —"de luna de miel", 170px de itálica— entraba
     debajo del área táctil del cerrar, que ocupa 44px de esquina. Tocar el
     final de la palabra cerraba el pop-up. Con el título abajo del botón el
     problema no puede volver, y de paso el titular deja de estar pegado al
     festón: antes arrancaba a 5px del canto. */
  /* Arriba y abajo el mismo valor, para que el panel cierre con el mismo aire
     con el que abre. Antes abajo iba space-4 más el espesor de la onda: contra
     el borde quedaban 35,1px, de los cuales la onda se comía 11,1, o sea 24
     libres. Arriba, en cambio, quedaban 49,9 hasta la tinta del título —casi 26
     de diferencia— y el sobre se veía apretado contra el canto.
     A los costados sí se le suma el espesor de la onda, porque ahí el texto
     corre a todo el ancho y sin ese margen queda contra los picos. Arriba y
     abajo no hace falta sumarlo: 64 ya es holgado y la onda se descuenta sola.
     El 64 de arriba, además, no se puede bajar: el aspa de cerrar ocupa 44px de
     esquina y el título tiene que arrancar por debajo. */
  padding: var(--space-6) calc(var(--space-3) + var(--onda-alto)) var(--space-6);
  /* Centrado como el resto del sitio. La referencia del afiche apilaba a la
     izquierda, pero acá el pop-up aparece encima de una página que centra de
     punta a punta y el cambio de eje se notaba más que la jerarquía. */
  text-align: center;
  /* El mismo terciopelo de la sección donde se despliega. La baldosa va anclada
     ARRIBA acá y ABAJO en el canto de arriba, así que un límite de baldosa cae
     justo en la junta y el dibujo sigue de largo. */
  /* El cuadro, con un velo marfil en degradado. Medido: el cielo de arriba está
     en L=0,65-0,67 y el follaje de abajo en L=0,010, así que un velo plano
     tendría que ir al 55% en todo el alto y borraría la imagen. En degradado el
     cielo queda casi limpio —6%— y sólo el pie se lava. Peor caso con la tinta
     encima: 5,51:1. */
  background:
    var(--veil-mural),
    /* auto 240% y no cover: con cover entra el cuadro entero en el panel y el
       horizonte queda a un tercio, así que el texto caía sobre el follaje. Al
       240% se ve solo el 42% de arriba —el cielo— y el texto lee sobre él.
       La fracción visible no depende del alto del panel, así que es estable. */
    var(--img-mural) center top / auto 240% no-repeat,
    var(--color-surface-sky);
}
.luna__panel:focus { outline: none; }   /* recibe el foco al abrir, no es un control */

.luna__cerrar {
  /* Corrido por el espesor de la onda: en la esquina el recorte se come parte
     del área y el aspa quedaba mordida. */
  position: absolute; top: calc(4px + var(--onda-alto)); right: calc(4px + var(--onda-alto));
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--radius-round);
  color: var(--color-ink-muted); cursor: pointer;
}
.luna__cerrar svg {
  width: 14px; height: 14px; fill: none;
  stroke: currentColor; stroke-width: 1.4; stroke-linecap: round;
}

/* El display escala con el ancho porque a 33px fijos "de luna de miel" en
   itálica mide más que los 236px de contenido que quedan en un 320. */
.luna__title {
  margin: 0;
  font: 400 clamp(var(--text-lg), 8.4vw, var(--text-xl))/var(--leading-tight) var(--type-heading);
  letter-spacing: var(--tracking-tight); color: var(--color-ink);
}
.luna__title-a, .luna__title-b { display: block; }
/* 700 y no 500. Comparados los tres pesos con la variable puesta: a 500 la
   primera línea casi no se distingue de la itálica de abajo y el contraste que
   arma la referencia se pierde. 600 funciona; 700 es el que iguala la
   contundencia del titular de la referencia. */
.luna__title-a { font-weight: 700; }
.luna__title-b { font-style: italic; }

/* La bajada queda pegada al título —14px— y el bloque gris recién abre a 22:
   título y bajada son una unidad, como en la referencia. */
/* Un solo párrafo. Antes eran dos —una bajada corta y el pedido— y los dos
   cambiaban de tono según la respuesta del formulario. */
.luna__copy {
  margin: var(--space-4) auto; max-width: 34ch;
  font: 400 var(--text-lectura)/var(--leading-loose) var(--type-body); color: var(--color-ink-muted);
}

/* La caja de la referencia. Ya no tiene divisiones internas: adentro quedó solo
   el alias, así que es una caja y punto. */
.luna__caja {
  /* Solo el dato, y debajo del sobre: primero el gesto, después la referencia
     de lo que se copió. El botón salió afuera hace tiempo: tenerlo adentro de
     la caja con borde, y otro botón con otro borde pegado, eran tres marcos
     anidados. */
  display: flex; flex-direction: column; align-items: center; gap: var(--space-1);
  /* Del ancho del pedido de arriba, no del panel: los dos bloques de texto de
     la tarjeta comparten caja de lectura y el sobre queda adentro, centrado.
     Las 34ch son las mismas de .luna__copy, y la familia está declarada acá
     solo para que la unidad ch mida contra la misma tipografía: sin esto la
     caja hereda la de interfaz y las mismas 34ch daban 312 en vez de 272. No
     cambia nada visible —el rótulo y el alias declaran la suya— y si la
     licenciada no carga, las dos caen al mismo fallback y siguen coincidiendo. */
  max-width: 34ch; margin-inline: auto; font-family: var(--type-body);
  border: 1px solid var(--color-line-on-light);
  border-radius: var(--radius-md);
  /* Con relleno y no transparente: el dato es lo único que hay que copiar bien,
     y sobre la imagen el monoespaciado se ensuciaba con lo que pasara detrás.
     El crema al 72% lo despega sin tapar el cuadro. */
  background: rgb(var(--rgb-paper) / .72);
  padding: var(--space-3);
}



@keyframes lunaEntra {
  from { opacity: 0; transform: translateY(16px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .luna__marco { animation: fadeIn var(--duration-fast) var(--ease-out) both; } }

/* La fila envuelve sola cuando el dato no entra al lado del botón: el alias
   son 19 caracteres monoespaciados y a 375px quedan 159px libres, así que se
   partía en dos. Un alias cortado al medio no se puede verificar. Con
   flex-wrap y un min-width igual al largo del dato, arriba de ~400px van uno
   al lado del otro y abajo el botón baja a su propia línea. */
/* Versalitas chicas y muy abiertas, como el bloque de micro-texto de la
   referencia: la etiqueta no compite con el dato, lo rotula. */
.dato__label {
  font: 500 var(--text-xs)/var(--leading-none) var(--type-label);
  letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--color-ink-muted);
}
/* Los datos nunca se centran: el ojo necesita un margen izquierdo fijo para
   verificar carácter por carácter. Tracking corto —.3px— porque el ancho de
   la versión anterior lo hacía leer decorativo en vez de copiable. */
.dato__valor {
  /* El cuerpo escala con el ancho: con el panel angostado, el alias de 19
     caracteres monoespaciados medía 200px contra los 166 de la caja y se salía
     del borde. A 4,6vw entra a cualquier ancho, y el tope sigue siendo 17px. */
  font: 500 clamp(var(--text-sm), 4.6vw, var(--text-md))/var(--leading-snug) var(--type-data);
  letter-spacing: var(--tracking-btn); color: var(--color-ink);
  font-variant-numeric: tabular-nums;
  /* Último recurso: si el alias real es más largo que el de ejemplo, preferimos
     que corte a que se salga de la caja y quede invisible. */
  overflow-wrap: break-word;
  /* Sin nowrap: a 320px el CVU agrupado mide 265px contra 210 disponibles y se
     salía de la tarjeta. Al dejarlo cortar, corta por los espacios de los
     grupos de 4 —que es justo donde se puede cortar sin perder la lectura— y
     nunca por el medio de un bloque. El alias no tiene espacios y entra
     entero: 194px contra los mismos 210. */
}

/* El botón de copiar con relleno y no solo un borde fino: sin el relleno no se
   lee como tocable. El borde va al 51% y no al 24 que sería lo mínimo visible,
   porque un contorno de control necesita 3:1 y al 24% daba 1,55.
   El botón va centrado: es la única acción de la caja, y pegado a un costado
   quedaba desbalanceado contra el dato de arriba. */
/* Modificador: el botón de copiar es un .btn .btn--primary y solo agrega su
   estado propio. El tamaño lo maneja el componente. */
/* Con forma de sobre. Es a lo que apunta el texto de arriba —"dejanos tu regalo
   en el siguiente sobre"— y cierra el gesto con el que la invitación abre.
   La proporción 1,62 es la de un sobre real y es lo que hace que se lea como
   tal: a los 48px de alto que tenía, sobre 257 de ancho, la relación era 5,4 a
   1 y ninguna solapa dibujada encima habría alcanzado. El tope de 220 es para
   que no se estire en el panel ancho.
   El texto va abajo, en el cuerpo del sobre, no centrado en la caja: arriba
   está la solapa y una palabra ahí la parte al medio. */
/* El sobre y su pista, apilados. La columna mide lo que mide el contenido más
   ancho —la frase— y el stretch de fábrica le pasa ese ancho al sobre. De ahí
   sale el tamaño del sobre: ningún número, la frase.
   Antes el sobre iba al 100% y era del ancho de la caja del alias. */
.copiar__grupo {
  display: flex; flex-direction: column;
  width: fit-content; max-width: 100%;
  margin: 0 auto var(--space-3);
}
.copiar {
  /* Lo estira la columna. */
  width: auto;
  aspect-ratio: 1.62;
  min-height: 0;                       /* manda la proporción, no el piso del botón */
  /* 8 abajo: la pista explica este botón y va pegada a él. Los 16 que separan
     del alias los pone .copiar__grupo, porque ahí sí son dos cosas distintas. */
  margin: 0 0 var(--space-2);
  /* El relleno de abajo sigue en 12%: ahí iba la palabra y ahí queda el visto
     del acuse, en el cuerpo del sobre y no partido por la solapa. */
  padding: 0 var(--space-3) 12%;
  align-items: flex-end;
  font-size: var(--text-sm);
  overflow: hidden;                    /* la solapa se recorta contra el radio */
}
/* La pista. En el serif del cuerpo y no en la label de los botones: es una
   frase que se lee, no un rótulo. Va más chica que .luna__copy —14 contra 16—
   porque es una instrucción de servicio y no puede competir con el pedido. */
.copiar__pista {
  /* Los dos spans en la misma celda: el fantasma sostiene el ancho, el vivo se
     dibuja encima. Sin esto el sobre se encogía al cambiar la frase por
     "¡Copiado!", que es más corta. */
  display: grid; justify-items: center;
  margin: 0;
  /* 16 y no 14: a 14 la frase se leía como una nota al pie y arrastraba al
     sobre a 145 de ancho. Mismo cuerpo que el pedido de arriba; la jerarquía
     entre los dos la marca la extensión —tres renglones contra uno— y que
     aquel es el pedido y esta una instrucción de servicio. Más de 16 la pondría
     por encima del pedido, que es lo que no puede pasar. */
  font: 400 var(--text-md)/var(--leading-snug) var(--type-body);
  /* Tinta plena y no el gris del panel —que acá ya viene reforzado a .90—
     porque la frase cae justo donde el follaje del mural asoma por la derecha.
     Medido componiendo el papel con PIL: con .90, el 3,3% de la franja bajo la
     frase queda por debajo de 4,5:1 y el peor píxel da 3,57; con tinta plena
     baja al 0,2% y el peor da 4,09, con la mediana en 11,8. La jerarquía la
     sigue marcando el cuerpo —14 contra los 16 del pedido—, no el tono: 10% de
     alfa no se ve, un contraste de 3,57 sí. */
  color: var(--color-ink);
}
.copiar__pista > * { grid-area: 1 / 1; white-space: nowrap; }
/* Un velo de crema detrás de la frase, no una caja: el gradiente llega a alfa 0
   en el borde, así que no tiene contorno ni se lee como un segundo recuadro
   pegado al del alias. Está porque el árbol del mural sube justo detrás de la
   última palabra —"copiar", la que importa— y ahí el papel deja de ser papel.
   Medido componiendo el fondo con PIL sobre la caja de tinta real: sin el velo
   el 1,6% de la franja queda bajo 4,5:1 con el peor píxel en 3,89; con él, el
   peor da 6,75 y no queda ninguno por debajo. En un teléfono de 375 pasa lo
   mismo: de 3,80 a 6,97. Donde el papel ya es papel no se nota, porque está
   aclarando crema sobre crema. */
.copiar__pista { position: relative; isolation: isolate; }
.copiar__pista::before {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
  inset: -12px -24px;
  /* farthest-side y no el tamaño de fábrica: sin eso el 100% del degradado cae
     en la esquina, los lados quedan más cerca que la esquina y el velo llega al
     borde todavía con color. Se veía el rectángulo. Con farthest-side cada eje
     termina en su propio lado y el alfa llega a 0 justo en el borde. */
  background: radial-gradient(ellipse farthest-side at center,
    rgb(var(--rgb-paper) / .93)  0%,
    rgb(var(--rgb-paper) / .86) 55%,
    rgb(var(--rgb-paper) / 0)  100%);
}
.copiar__pista-eco { visibility: hidden; }
/* La solapa: dos líneas finas que bajan de las esquinas de arriba al centro.
   Se dibujan con degradados y no con un SVG porque así heredan currentColor y
   siguen al botón cuando cambia de color al copiar o en hover. */
.copiar__solapa {
  position: absolute; left: 0; right: 0; top: 0; height: 52%;
  pointer-events: none;
  /* `to top right` y no `to bottom right`: en un degradado la franja de color
     va PERPENDICULAR al eje, así que apuntando abajo-derecha la línea sale
     subiendo y las dos juntas dibujaban un pico, como un techo. Con el eje
     invertido bajan desde las esquinas al centro, que es una solapa cerrada. */
  background:
    linear-gradient(to top right,
      transparent calc(50% - .6px), currentColor calc(50% - .6px),
      currentColor calc(50% + .6px), transparent calc(50% + .6px)) left / 50% 100% no-repeat,
    linear-gradient(to top left,
      transparent calc(50% - .6px), currentColor calc(50% - .6px),
      currentColor calc(50% + .6px), transparent calc(50% + .6px)) right / 50% 100% no-repeat;
  opacity: .55;
}

.copiar__ok { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round; display: none; }
/* Ya relleno de base, el estado copiado no puede distinguirse por el relleno.
   Lo hacen el ícono y la palabra —que es la forma que igual hay que tener,
   porque el color solo no alcanza— y el marfil baja al 72% para que se sienta
   el acuse sin que parezca deshabilitado. */
.copiar.copiado { background: var(--color-control-on-light); border-color: var(--color-control-on-light); }
.copiar.copiado .copiar__ok { display: block; }
@media (hover: hover) { .copiar:hover { background: var(--color-ink); border-color: var(--color-ink); } }


/* ── Movimiento ── */
@keyframes subeYAparece { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
#rsvpExito  { animation: subeYAparece var(--duration-base) var(--ease-out) both; }

@media (prefers-reduced-motion: reduce) {
  #rsvpExito { animation: fadeIn var(--duration-fast) var(--ease-out) both; }
}

/* ═══════════════ Cierre ═══════════════ */
/* La foto ocupa el ancho entero de la pieza y cierra la invitación. El festón
   del papel le cae encima: arriba la foto es casi negra, así que el corte se
   lee limpio contra ella. */

/* ═══════════════ Coda — el resumen ═══════════════ */
/* Franja de marfil al pie, con el monograma al medio y los datos repartidos a
   los lados. Lleva festón como todos los pasos de sección.
   Antes acá arriba iba la foto de la mirilla y el festón se dibujaba sobre
   ella. La foto pasó a abrir Código de vestimenta, así que ahora el marfil
   trepa directamente sobre el terciopelo de Confirmación. El efecto es el
   mismo —el pie sube festoneado sobre lo que tenga encima— y de paso la página
   deja de terminar con una imagen que ya no aportaba nada nuevo.
   El relleno de arriba sigue al alto del festón (27,73% del ancho) más el aire;
   el de abajo (--coda-bottom) es el que despeja el contador fijo. */
/* El festón del pie no usa la clase .festón porque su geometría es la inversa:
   la banda no se apoya adentro del pie tapando el arranque del marfil, sino que
   sale hacia ARRIBA —bottom: 100%— y se dibuja encima de la sección anterior.
   El marfil trepa sobre el terciopelo, sin franja de color plano en el medio.
   Por eso también el relleno de arriba vuelve a 30px: la banda ya no ocupa
   lugar adentro del pie. */
.coda { position: relative; }
.coda::before {
  content: '';
  position: absolute; bottom: 100%; left: 0; right: 0;
  aspect-ratio: var(--festón-ratio);
  background: var(--color-paper);
  -webkit-mask: var(--festón-pie) center / 100% 100% no-repeat;
  mask: var(--festón-pie) center / 100% 100% no-repeat;
  z-index: 4;
  pointer-events: none;
}
.coda {
  padding: var(--space-6) var(--gutter) var(--coda-bottom);
  background: var(--color-paper);
  color: var(--color-ink);
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: var(--space-2);
}
.coda__col { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.coda__col--end { text-align: right; }
/* El pie hablaba en versalitas de Archivo a 8px con tracking ancho. Eso es voz
   de afiche técnico —de ahí venía la referencia— y encima queda por debajo del
   piso de lectura. Ahora usa la serif de la pieza, en caja baja, a 13px: el
   pie pasa a sonar como el resto de la invitación en vez de como un crédito.
   La segunda línea baja a itálica de 12 para separarse sin gritar. */
.coda__line {
  /* El piso subió de 12 a 16. Con 12 el pie entraba en un renglón a cualquier
     ancho, pero en Playfair eso daba 6,2px de altura de x: el texto más chico
     de la pieza, en la única parte que repite la dirección del salón.
     El costo es conocido y se acepta: a 320px la columna mide 109 y "Almte.
     Brown 2050" a 16 mide 140, así que parte en dos y la columna derecha queda
     con tres líneas contra dos de la izquierda. Es un pie que repite datos que
     ya están arriba en grande; que se lea vale más que que empareje. */
  font: 400 clamp(var(--text-sm), 3.5vw, var(--text-base))/var(--leading-snug) var(--type-heading);
  color: var(--color-ink);
  font-variant-numeric: tabular-nums;
  text-wrap: balance;
}
.coda__line--soft {
  font-style: italic; font-size: clamp(var(--text-sm), 3.2vw, var(--text-base));
  color: var(--color-ink-muted);
}
/* Filigrana: el monograma bien tenue, como una marca de agua impresa. */
.coda__mark {
  width: 42px; aspect-ratio: 264 / 295;
  background: var(--color-ink); opacity: .3;
  -webkit-mask: var(--img-mono) center / contain no-repeat;
  mask: var(--img-mono) center / contain no-repeat;
}

/* ═══════════════ Escritorio ═══════════════ */
/* La invitación es una pieza vertical: en un celular ocupa todo el ancho, y
   recién pasado el tope de --shell queda centrada sobre el mismo marrón. */
@media (min-width: 480px) {
  body { background: var(--color-ink); }
}

/* ═══════════════ Festón entre secciones ═══════════════ */
/* El corte va como ::before de la sección de ABAJO, pintado con el color de la
   de ARRIBA: se lee como si la sección superior terminara en esa forma y en los
   valles asomara la siguiente. Va abajo y no arriba para no depender de que la
   sección de arriba no recorte: un ::after colgando por fuera lo comería
   cualquier overflow:hidden que aparezca más adelante.

   El aspect-ratio del bloque iguala al del viewBox, así la máscara escala
   uniforme: los lóbulos no se estiran ni se cortan a los costados a ningún
   ancho. A 375px el festón mide 104px de alto; a 440px, 122px.

   La curva es el calco del borde de la referencia: se trazó el contorno del
   terciopelo píxel a píxel, se descartó el cordón del centro y se ajustaron
   splines. De ahí sale la proporción 0,267 de profundidad sobre ancho. */
.festón { position: relative; }
.festón::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0;
  aspect-ratio: var(--festón-ratio);
  background: var(--festón-c);
  -webkit-mask: var(--festón-img, var(--festón)) center / 100% 100% no-repeat;
  mask: var(--festón-img, var(--festón)) center / 100% 100% no-repeat;
  z-index: 4;
  pointer-events: none;
}

/* Cada corte toma el color de la sección que tiene arriba.
   Save the Date no lleva festón: la tarjeta de la mano se vería cortada. */
/* Save the Date pasó al marrón oscuro, el mismo que usa Ubicación de fondo:
   este corte queda del mismo color a los dos lados y prácticamente no se ve. */
.venue   { --festón-c: var(--color-ink); }       /* arriba tiene Save the Date */
/* Sin --festón-img: usa el corte por defecto, que es el espejo vertical del
   que llevaba antes. */
.dress   { --festón-c: var(--color-ink); }
/* Confirmación estrena corte: hasta ahora venía del mismo terciopelo que
   Vestimenta y no había dos colores que separar. Con Vestimenta en negro,
   este corte es el que enmarca el bordó del formulario. */
.rsvp    { --festón-c: var(--color-surface-mirilla); }

