/* =============================================================================
   Minh Trung & Minh Thuỳ — Wedding Invitation
   Aesthetic follows anniversarymovie.kr/view/template01, whose values are the
   source for the tones and component styles below: a white card banded with
   near-white greys, hairline #eee rules, small radii, and Playfair Display
   section titles over a humanist sans.
   ============================================================================= */

:root {
  --white: #ffffff;
  /* The reference's two band tones, sampled from its sections. */
  --cream: #f6f6f6;
  --cream-deep: #f9f9f9;
  --page: #f5f5f5;   /* outside the card column, and the footer */
  /* The alternating section ground. Warm, in the cover plate's family (#e2dfd8)
     but much lighter, so the small grey labels printed on it keep roughly the
     contrast they had on the cream sections this replaces. */
  --tint: #efebe3;
  /* The boarding pass is card stock lying on linen, so it is ivory rather than
     screen white — pure #fff read as a cold rectangle that did not belong to
     the cloth under it. --notch is whatever shows through the punched holes in
     the tear line, so it has to be the ground BEHIND the card, not the card. */
  --card-face: #fcfaf5;
  --card-foot: #f4f1e9;
  /* A plain section is the card's own white, so a hole punched in a pass shows
     white through it. `.paper` overrides this with the linen sheet's tone. */
  --notch: #ffffff;

  --ink: #191919;
  --ink-soft: #202020;
  --text: #363636;
  --text-mid: #434343;
  --text-body: #535353;
  --muted: #626262;
  --muted-soft: #717171;
  --faint: #808080;
  --faint-soft: #909090;
  /* --hint and --faint are display tones only. Against the linen's darker
     fibres (5% of it sits below 211) they fall to 2.6-3.5:1, so no label small
     enough to need reading may use them. --text-body is the floor there. */
  --hint: #969696;
  --hint-soft: #a6a6a6;

  --accent: #7c7c7c;
  --accent-deep: #5c5c5c;
  /* The reference draws every rule and control outline as one hairline grey. */
  --line: #e5e5e5;
  --line-soft: #eeeeee;
  --line-faint: #eeeeee;
  --btn-fill: #f2f2f2;
  --tile-fill: #f8f8f8;    /* the small map/link tiles */
  --rose: #c08a7a;

  --radius: 6px;   /* map and link tiles */
  --radius-lg: 8px;   /* cards and accordion rows */

  --serif: "Cormorant Garamond", Georgia, serif;
  --sans: "Be Vietnam Pro", -apple-system, BlinkMacSystemFont, sans-serif;
  /* The reference's own script and display faces. Its body face, Gowun Dodum,
     has no Vietnamese subset, so Be Vietnam Pro stays — every accented vowel
     would otherwise fall back to a different face mid-sentence. */
  --script: "Pinyon Script", "Cormorant Garamond", cursive;
  --display: "Playfair Display", "Cormorant Garamond", serif;
  /* Ticket figures and labels. IBM Plex Mono carries the Vietnamese subset —
     DM Mono and Courier Prime do not, and would drop every diacritic. */
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Plane silhouette, turned to point along the route. Used as a mask so it
     takes the surrounding text colour, and drawn rather than set as a glyph
     because ✈ renders differently on every platform. */
  --plane-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath transform='rotate(90 12 12)' d='M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5z'/%3E%3C/svg%3E");

  --maxw: 480px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  background: var(--page);
  color: var(--text);
  font-family: var(--sans);
  font-weight: 400;   /* the reference sets body text at a normal weight */
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-deep); }

img { display: block; width: 100%; height: 100%; object-fit: cover; }

input, select, textarea, button { font-family: var(--sans); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The shared coastline and plane live in an inline <svg> with no width or
   height. Inline is still inline: it sat in a line box and pushed the whole
   invitation 20px down the page. Taking it out of flow removes that. */
.svg-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.card {
  position: relative;
  max-width: var(--maxw);
  margin: 0 auto;
  background: var(--white);
  overflow: hidden;
  /* Explicit scale(1), not `none` — `none` does not interpolate reliably, which
     collapsed the settle below into an instant snap. */
  transform: scale(1);
  transition: transform 2.9s cubic-bezier(0.38, 0.02, 0.3, 1);
}
/* Card sits a touch deeper while the opening screen is still over it, then
   settles forward as that screen clears — reads as the card being handed over
   rather than simply fading in. */
body.env-closed .card { transform: scale(0.965); }

/* Shared bits ------------------------------------------------------------- */
/* Section titles, matched to the reference's: a weighted Playfair set close to
   black at 19.2px with only 2px of tracking. The wide, pale, lightweight eyebrow
   this replaced read as a caption rather than a title. */
.eyebrow {
  font-family: var(--display);
  font-size: 19.2px;
  font-weight: 700;
  letter-spacing: 2px;
  text-indent: 2px;
  color: var(--ink);
  text-align: center;
}

.drop-rule {
  display: block;
  width: 1px;
  height: 38px;
  margin: 22px auto;
  background: var(--line);
}

/* The reference has no dark or outlined buttons: every control is a pale fill,
   either a bordered tile with a small radius or a flat grey block. */
.btn-outline {
  display: inline-block;
  padding: 12px 22px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--tile-fill);
  font-size: 15px;
  letter-spacing: 0.04em;
  color: var(--text);
  text-align: center;
  cursor: pointer;
}

.btn-solid {
  padding: 14px 0;
  border: none;
  background: var(--btn-fill);
  color: var(--text);
  font-size: 16px;
  letter-spacing: 0.08em;
  cursor: pointer;
}
.btn-solid[disabled] { opacity: 0.6; cursor: default; }

/* Photo placeholder ring, shown until a real image loads ------------------ */
.slot {
  position: relative;
  background: #efefef;
  overflow: hidden;
}
.slot img { opacity: 0; transition: opacity 0.4s ease; }
.slot img.loaded { opacity: 1; }
.slot:not(.filled)::after {
  content: attr(data-placeholder);
  position: absolute;
  inset: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--line);
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--hint);
  text-align: center;
  padding: 6px;
}

/* ======================= OPENING SCREEN (intro) ==========================
   Drifting silk under a gloved hand that holds the card out to the guest.
   Opening runs in stages, driven by classes from main.js:
     sealed → breaking (the card is taken, 0.5s) → opening (it comes forward
     and dissolves, 1.2s) → lifting (the whole screen fades, 1.1s).
   ------------------------------------------------------------------------- */

.envelope {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: block;
  overflow: hidden;
  border: none;
  padding: 0;
  /* The paper tone the silk is graded towards, so the very first paint — before
     the poster has decoded — is already the right colour rather than white. */
  background: #e7e0d4;
  cursor: pointer;
  opacity: 1;
  transition: opacity 1.1s cubic-bezier(0.4, 0, 0.5, 1);
}
.envelope[hidden] { display: none; }
.envelope.lifting { opacity: 0; }
.envelope:not(.sealed) { pointer-events: none; }

/* Fills the screen whatever its shape: the film is a 9:16 phone frame, so on a
   wide screen it is the sides that carry it and the top and bottom that crop. */
.env-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Silk shot slightly cool against the card's warm paper — nudged over so the
     two read as one room. */
  filter: saturate(0.92) sepia(0.12) brightness(1.02);
}

/* Warm wash over the film: holds the script legible over the brightest folds,
   and closes the last of the gap between the silk and the card's own paper. */
.env-veil {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 92% 62% at 50% 46%, rgba(255, 252, 245, 0.34), transparent 72%),
    linear-gradient(180deg, rgba(228, 218, 202, 0.42), rgba(214, 202, 184, 0.24) 46%, rgba(196, 182, 162, 0.5));
  transition: opacity 1.2s ease;
}

/* The hand is cut off at the wrist, so it is hung off the bottom edge rather
   than centred — the arm continues past the screen the way it would past the
   edge of a photograph. Sized by height so the card lands at eye level on a
   tall screen, with the third term capping it by width on a short wide one. */
.env-offer {
  position: absolute;
  left: 50%;
  bottom: 0;
  /* The picture is 1072px across, so this cap is also what keeps it from being
     scaled past its own resolution on a 3x screen. */
  height: min(50dvh, 450px, 84vw);
  aspect-ratio: 536 / 745;
  transform: translateX(-50%);
  transform-origin: 50% 26%;   /* the card, not the wrist — it is what comes forward */
  opacity: 1;
  /* Percentages inside the offer resolve against its width, which is what keeps
     the name registered to the printed panel at any size. */
  container-type: inline-size;
  transition:
    transform 1.2s cubic-bezier(0.34, 0.03, 0.28, 1),
    opacity 0.9s ease 0.25s;
}
.env-hand {
  display: block;
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 26px 34px rgba(94, 76, 52, 0.28));
}

/* The blank panel inside the moulded border. Found by scanning the artwork for
   the run of columns and rows whose variance drops — the moulding is all edges,
   the panel is flat — rather than read off the picture by eye, which put the
   box's right edge 8% of the width out into the ornament and took the line with
   it. Columns 136-857 and rows 130-471 of 1072x1490. */
.env-card-text {
  position: absolute;
  left: 12.69%;
  top: 8.72%;
  width: 67.26%;
  height: 22.89%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.6cqw;
  text-align: center;
}
.env-card-line {
  font-family: var(--script);
  font-size: 10cqw;
  line-height: 1.2;
  color: #3f382e;
  white-space: nowrap;
  /* What is left once the panel above is measured properly: the script hangs
     its strokes outside their own advance widths, so the line the layout has
     centred still prints 4px right of the panel. A centred line shares an
     indent between both ends, so correcting 4px takes 8px of it. */
  text-indent: -0.32em;
  /* And the same face carries its ink high in the line box, which left the
     line 3.3px above the panel's middle with the gaps reading 24 over 30. */
  position: relative;
  top: 0.14em;
}
/* "Trân trọng kính mời" runs a third longer than "You are invited" while the
   panel printed on the card stays the size it is, so the line is set to what
   its own words need rather than to one size that crops one of them. */
html[lang="vi"] .env-card-line { font-size: 8cqw; }

/* Sits just above the offered card rather than under it: the lower half of the
   screen is all hand. Measured off the card itself rather than off a repeat of
   the hand's height — two copies of that sum are two chances to disagree, and
   when they did the line landed in the middle of the card. */
.env-hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  margin-bottom: 12px;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(15px, 6.4cqw, 21px);
  letter-spacing: 0.02em;
  text-align: center;
  color: #6b5a44;
  opacity: 1;
  transition: opacity 0.5s ease;
}
.envelope.sealed .env-hint { animation: hintBreathe 3.6s ease-in-out infinite; }
@keyframes hintBreathe {
  0%, 100% { opacity: 0.62; }
  50% { opacity: 1; }
}

/* Taken: the card dips towards the guest for a beat before it is lifted away. */
.envelope.breaking .env-offer {
  transform: translateX(-50%) scale(0.978);
  transition: transform 0.5s cubic-bezier(0.36, 0.06, 0.3, 1);
}
.envelope.breaking .env-hint,
.envelope.opening .env-hint,
.envelope.lifting .env-hint { opacity: 0; animation: none; }

/* Lifted: it comes forward past the viewer and dissolves, and the silk gives
   way to the card underneath. */
.envelope.opening .env-offer,
.envelope.lifting .env-offer {
  transform: translateX(-50%) translateY(-7%) scale(1.42);
  opacity: 0;
}
.envelope.opening .env-veil,
.envelope.lifting .env-veil { opacity: 0.15; }

/* =================== FLOATING CONTROLS (language + music) =============== */
/* Both controls share one fixed bar so there is a single floating group,
   and both sit under the envelope's z-index until the card is opened. */
.controls-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  /* One cell, both modes in it. */
  display: grid;
  grid-template-areas: "stack";
  justify-items: center;
  align-items: center;
  padding: 0 0 22px;
  pointer-events: none;
}

/* Both modes occupy the same cell of the bar so they can cross-fade in place
   rather than one vanishing and the other appearing. */
.ctl-mode {
  grid-area: stack;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  transition: opacity 0.3s ease, transform 0.34s cubic-bezier(0.22, 1, 0.36, 1),
              visibility 0.34s;
}
/* visibility rather than display: it can be transitioned, and it still takes the
   inactive mode out of the accessibility tree. */
.ctl-mode.is-off {
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px) scale(0.94);
  pointer-events: none;
}

/* The pills arrive one after another rather than as a block. Reversed on the way
   out so the group leaves from the same end it entered. */
.ctl-actions > * {
  transition: opacity 0.28s ease, transform 0.36s cubic-bezier(0.22, 1, 0.36, 1);
}
.ctl-actions.is-off > * {
  opacity: 0;
  transform: translateY(12px);
}
.ctl-actions > *:nth-child(1) { transition-delay: 0.02s; }
.ctl-actions > *:nth-child(2) { transition-delay: 0.08s; }
.ctl-actions > *:nth-child(3) { transition-delay: 0.14s; }
.ctl-actions.is-off > *:nth-child(1) { transition-delay: 0.08s; }
.ctl-actions.is-off > *:nth-child(2) { transition-delay: 0.04s; }
.ctl-actions.is-off > *:nth-child(3) { transition-delay: 0s; }

/* The calendar menu has its own small entrance. */
.ctl-menu {
  transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
              visibility 0.22s;
}

@media (prefers-reduced-motion: reduce) {
  .ctl-mode,
  .ctl-actions > *,
  .ctl-menu { transition-duration: 0.01ms; }
  .ctl-mode.is-off,
  .ctl-actions.is-off > * { transform: none; }
}

/* Action pills, matching the language pill's glass so the bar keeps one look
   across the swap. */
.ctl-btn {
  display: flex;
  align-items: center;
  gap: 7px;
  height: 38px;
  padding: 0 14px;
  pointer-events: auto;
  cursor: pointer;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.12em;
  color: var(--text);
  border: 1px solid rgba(124, 124, 124, 0.35);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.78);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 6px 20px rgba(83, 83, 83, 0.16);
  white-space: nowrap;
  transition: color 0.28s ease, background-color 0.28s ease, border-color 0.28s ease;
}
.ctl-btn svg { width: 15px; height: 15px; flex: 0 0 auto; }
.ctl-btn:hover { border-color: var(--accent); color: var(--ink); }

.ctl-menu-wrap { position: relative; display: flex; }
/* Opens upward: the bar is pinned to the bottom of the screen. */
.ctl-menu {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  /* Centring lives in the transform, so every state has to carry it or the menu
     slides sideways as it opens. */
  transform: translateX(-50%) translateY(0) scale(1);
  min-width: 176px;
  padding: 5px;
  pointer-events: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--white);
  box-shadow: 0 10px 30px rgba(83, 83, 83, 0.22);
}
.ctl-menu.is-off {
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(6px) scale(0.96);
  pointer-events: none;
}
.ctl-menu-item {
  display: block;
  padding: 10px 12px;
  border-radius: var(--radius);
  font-size: 14.5px;
  color: var(--text);
  white-space: nowrap;
  cursor: pointer;
}
.ctl-menu-item:hover { background: var(--cream); color: var(--ink); }

/* Labelled, the three pills measure 335px. That clears a 360px screen but not a
   320px one, so the labels go only below that and the icons carry the meaning. */
@media (max-width: 350px) {
  .ctl-btn { padding: 0 13px; }
  .ctl-btn span { display: none; }
  .ctl-btn svg { width: 17px; height: 17px; }
}

.music-btn {
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  pointer-events: auto;
  cursor: pointer;
  color: var(--muted);
  border: 1px solid rgba(124, 124, 124,0.35);
  border-radius: 999px;
  background: rgba(255, 255, 255,0.78);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 6px 20px rgba(83, 83, 83,0.16);
  transition: color 0.28s ease, background-color 0.28s ease;
}
.music-btn[hidden] { display: none; }
.music-btn svg { width: 21px; height: 21px; }
/* Playing: waves shown, filled thumb like the active language. Muted: cross. */
.music-btn .ico-cross { display: none; }
.music-btn.muted .ico-waves { display: none; }
.music-btn.muted .ico-cross { display: block; }
.music-btn.playing {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
}
.lang-pill {
  position: relative;
  display: flex;
  align-items: stretch;
  height: 38px;
  pointer-events: auto;
  border: 1px solid rgba(124, 124, 124,0.35);
  border-radius: 999px;
  background: rgba(255, 255, 255,0.78);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  overflow: hidden;
  box-shadow: 0 6px 20px rgba(83, 83, 83,0.16);
}
.lang-thumb {
  position: absolute;
  top: 0;
  left: 0;
  width: 54px;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  transition: left 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
.lang-pill[data-lang="en"] .lang-thumb { left: 54px; }

.lang-btn {
  position: relative;
  width: 54px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  padding: 0;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.28s ease;
}
.lang-btn.active { color: var(--white); }

/* ================================ COVER ================================= */
/* The cover artwork is one image: paper, frame, flourish, title and the black
   photo band are all part of it. Only the photo and the names are live elements,
   positioned as percentages of the plate so they stay registered to the artwork
   however it scales. */
/* The sheet fills the screen; the artwork sits centred on it.
   It cannot instead be scaled to fill: the plate is a fixed 9:16 and a phone is
   taller, so covering the height means cropping 9.0% off each side while the
   artwork only spares 8.2%. That last 0.8% is the frame's own outer line, and
   cutting it is exactly the break in the border this design keeps being caught
   on. Extending the sheet costs nothing and cuts nothing. */
.cover {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Sampled off the plate's own edge, so the sheet and the artwork's paper read
     as one surface. The artwork cannot be recoloured to the page tone instead:
     its paper texture already reaches 255, so lifting the paper from 225 to 249
     left six levels of headroom for twenty-nine levels of grain and the paper
     went blotchy. */
  background: #e2dfd8;
  overflow: hidden;
}

.cover-plate {
  position: relative;
  /* Whichever of width or height runs out first decides the size, so the whole
     artwork stays visible on a short screen instead of running off the sheet. */
  width: min(100%, calc(100dvh * 1080 / 1920));
  aspect-ratio: 1080 / 1920;
  background: url("../assets/cover-plate.webp") center / 100% 100% no-repeat;
  container-type: inline-size;
}

/* Measured against the artwork: the black band's inner opening. */
.cover-photo {
  position: absolute;
  /* The black opening in the artwork, measured off the plate itself
     (x 225-854, y 783-1136 of 1080x1920). The photo has to fill it exactly —
     anything smaller leaves the black showing along the edges. */
  left: 20.83%;
  top: 40.78%;
  width: 58.33%;
  height: 18.44%;
}

/* Sits where the reference's script names sit. */
.cover-names {
  position: absolute;
  left: 50%;
  top: 60.5%;
  transform: translateX(-50%);
  width: max-content;
  font-family: var(--script);
  font-weight: 400;
  font-size: 6.15cqw;
  /* The script face sets a very narrow word space, which closes the gaps either
     side of the ampersand that the reference leaves open. */
  word-spacing: 0.06em;
  line-height: 1.35;
  color: var(--ink);
  text-align: center;
  white-space: nowrap;
}

.cover-cities {
  position: absolute;
  left: 0;
  right: 0;
  top: 69.5%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2.4cqw;
  font-size: max(11px, 2.7cqw);
  letter-spacing: 0.28em;
  color: #4a4a4a;
}
.cover-cities .dot {
  width: 0.9cqw;
  height: 0.9cqw;
  border-radius: 999px;
  background: var(--hint-soft);
}

/* ============================== INVITATION =============================== */
.section { position: relative; }

/* ---- alternating grounds ----
   Plain white and a sheet of the couple's linen, turn and turn about, so the
   card reads as a sequence of leaves rather than one long scroll. A plain
   section carries no image at all — it is the card's own white showing through,
   which is what "one white slide" was asking for.

   `cover`, not a stretch: the sheet is 0.57 in aspect while the sections run
   0.38 to 0.93, so fitting it exactly would distort the weave differently in
   every one. Covering keeps the weave in proportion and crops the sides.

   The passes sit on a plain section deliberately — the ivory card stock reads
   as paper laid on paper there, where against a darker sheet it did not. */
.paper {
  position: relative;
  background-color: transparent;
  background-image: url("../assets/paper-white.webp");
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  --notch: #f1efe9;   /* the sheet's own tone, so a punched hole reads as one */
}
/* Two sections carry their own ground instead of the shared sheet. Both images
   were lifted before export so their darkest pixel lands at gray 160: --text
   (#363636) needs the ground at 158 or above to hold 4.5:1, and the coupes
   photograph came in with shadows down at gray 15. That lift is what makes the
   run sheet readable over a photograph rather than a texture. */
/* Same treatment as the satin below, for the same reason. Covering the run
   sheet cropped 22% of the photograph's height, so the drape above the glasses
   and the table edge below them both arrived cut. The file is now trimmed to
   the run sheet's own 0.86 aspect — which also loses the dark chair that was
   forcing the heaviest part of the wash — then stacked over its own vertical
   flip, so the taller Adelaide run sheet can repeat without a seam. */
.paper-coupes {
  background-image: url("../assets/paper-coupes.webp");
  background-size: 100% auto;
  background-repeat: repeat-y;
  background-position: top center;
}
/* The satin does not use `cover` like the others. The RSVP runs to an aspect of
   0.27 against the silk's own 0.563, so covering it showed 48% of the image's
   width and the sweeping diagonal folds arrived cropped into vertical streaks.
   Fitting the width and repeating down the section keeps the drape's shape; the
   file is the photograph stacked over its own vertical flip, so every seam the
   repeat creates is a mirror rather than a cut. */
.paper-satin {
  background-image: url("../assets/paper-satin.webp");
  background-size: 100% auto;
  background-repeat: repeat-y;
  background-position: top center;
}
/* Two birds over a hazy sea, for the one section that asks a guest which of
   three places they are coming to. The photograph's own 0.71 aspect is close to
   the section's (0.54 on a phone, 0.69 at the card's full width), so `cover`
   trims a little off the sides and nothing else — and the birds sit at 57% of
   the height, well inside what survives either crop.

   Lifted to a floor of gray 101 rather than the 160 the coupes and the satin
   carry. That floor is there so --text (#363636) holds 4.5:1 over the ground,
   and what is under 160 here is two birds and the horizon, all of them below
   the head block and behind the passes' own opaque stock. The band the head
   text does sit over is 230 at its darkest. Lifting the rest to match would
   have taken the birds with it — at 160 they are all but gone from the sky. */
.paper-sky {
  background-image: url("../assets/paper-sky.webp");
  --notch: #f8f1e9;   /* the sky behind the passes, so a punched hole reads as one */
}

.invitation {
  padding: 64px 34px;
  text-align: center;
  container-type: inline-size;   /* the signature below is sized off this column */
}
/* The opening line, set large in the script face as the reference sets it. */
.invite-lead {
  font-family: var(--script);
  font-weight: 400;
  font-size: 33px;
  line-height: 1.4;
  color: var(--ink);
  margin-bottom: 20px;
  text-wrap: balance;
}
/* The body is the serif's italic, not its roman — the reference sets the whole
   passage in italic and it is what keeps it of a piece with the script above. */
.invite-text {
  font-family: var(--serif);
  font-style: italic;
  font-size: 18px;
  line-height: 1.95;
  color: var(--text);
  white-space: pre-line;
  text-wrap: pretty;
}
.invite-close {
  font-family: var(--script);
  font-weight: 400;
  font-size: 22px;
  line-height: 1.3;
  color: var(--ink);
  margin-top: 30px;
}
/* The names are one line or they are nothing: broken across two, the reading
   order stops being a pair of names and becomes a list. At 22px they need 340px
   against the 322px a 390px phone gives them, so the size follows the column
   rather than sitting at a fixed 22px and wrapping on every phone.

   100cqw is the section's own content width, so the ratio is all that has to be
   maintained: the script sets this line at 15.5px of width per px of type, and
   6.3cqw leaves it a little under the width available. Change the names and
   that ratio changes with them. Browsers without container queries keep the
   22px above and wrap as before. */
.invite-signature {
  font-family: var(--script);
  font-weight: 400;
  font-size: 22px;
  font-size: min(22px, 6.3cqw);
  line-height: 1.35;
  /* The script's word space closes up around the ampersand, as on the cover. */
  word-spacing: 0.06em;
  color: var(--ink);
  margin-top: 2px;
  text-wrap: balance;
}

/* ------------------------------- the film -------------------------------
   16:9, matching the film itself — a portrait frame would crop away more than
   half of every landscape shot. The poster and the video are stacked in the same
   box so swapping between them cannot shift the page. */
.film { margin: 32px auto 0; }
.film[hidden] { display: none; }
.film-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--cream);
  border: 1px solid var(--line);
}
.film-frame img,
.film-frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The video is only in the DOM ahead of time so its src can be assigned on the
   tap that plays it — the tap browsers require before video may start. */
.film-frame video { display: none; background: var(--ink); }
.film.playing .film-frame video { display: block; }
.film.playing .film-frame img,
.film.playing .film-play { display: none; }

.film-play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 54px;
  height: 54px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.7);
  background: rgba(25, 25, 25, 0.34);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.25s ease, transform 0.25s ease;
}
.film-play:hover {
  background: rgba(25, 25, 25, 0.55);
  transform: translate(-50%, -50%) scale(1.06);
}
/* Optically centred: a play triangle's visual centre sits left of its box. */
.film-play svg { width: 19px; height: 19px; margin-left: 3px; }

/* ============================ VENUE PICKER =============================== */
.picker {
  padding: 56px 24px;
}
.picker-head { text-align: center; margin-bottom: 8px; }
.picker-intro {
  font-size: 16.5px;
  line-height: 1.9;
  color: var(--text);
  margin-top: 14px;
  white-space: pre-line;
  text-wrap: pretty;
}
/* The call to action, set apart from the quiet grey around it. At the intro's
   own size and colour it read as small print rather than as the one instruction
   on the page, and guests were not tapping the passes. */
.picker-cta {
  font-family: var(--serif);
  font-style: italic;
  font-size: 18px;
  line-height: 1.5;
  color: var(--ink);
  margin-top: 10px;
  text-wrap: balance;
}

.country-tabs {
  display: flex;
  gap: 10px;
  margin: 28px 0 18px;
}
/* The unselected tab used to be transparent over a thin hairline, so on the
   linen it read as a caption rather than a control and guests did not know it
   could be tapped. It now has its own paper and a border with some weight. */
.country-tab {
  flex: 1;
  padding: 12px 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--white);
  color: var(--text-mid);
  font-size: 14.5px;
  letter-spacing: 0.16em;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(120, 110, 96, 0.07);
  transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.country-tab:hover { border-color: var(--accent); }
/* --accent-deep, not --accent: white on #7c7c7c measured 4.17:1, just under the
   bar. The darker tone clears 6.7:1 and reads as more firmly "selected" too. */
.country-tab.active {
  border-color: var(--accent-deep);
  background: var(--accent-deep);
  color: var(--white);
  box-shadow: none;
}

.venue-cards { display: grid; gap: 14px; }

/* Each card is a boarding pass in three bands, after the banded reference:
   a dark header, a white body over the flight map, and a pale footer of detail
   fields ending in a barcode. The reference carries that structure in crimson
   and blush; here it is carried in the site's own greys, so the card reads as a
   ticket through its structure rather than through a colour the page never uses.

   The header and footer run the full width, behind the stub, as they do on a
   real pass; only the tear line crosses them. */
.venue-card {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 96px;
  grid-template-rows: auto 1fr auto;
  width: 100%;
  /* Sized by its contents: a pass prints its address, and an address wraps to a
     different number of lines per venue and per language. */
  height: auto;
  padding: 0;
  overflow: hidden;
  text-align: left;
  cursor: pointer;
  background: var(--card-face);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Rose, not grey: this is the same ink that rings the day on the calendar and
   marks the chosen row in the RSVP, so one colour means "your choice" across
   the whole card. A grey border on a grey hairline was too near its own
   unselected state to tell a guest which pass they had picked. */
.venue-card.active {
  border-color: var(--rose);
  box-shadow: 0 10px 26px rgba(192, 138, 122, 0.22);
  transform: translateY(-3px);
}

/* ---- header band ---- */
.vc-header {
  grid-area: 1 / 1 / 2 / -1;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 11px;
  height: 24px;
  background: var(--ink);
  color: var(--white);
}
.vc-header-plane {
  width: 11px;
  height: 11px;
  flex: 0 0 auto;
  background: var(--white);
  -webkit-mask: var(--plane-mask) center / contain no-repeat;
  mask: var(--plane-mask) center / contain no-repeat;
}
.vc-header-title {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 11.5px;
  letter-spacing: 0.2em;
}
/* Pushed right, and allowed to disappear before it can wrap the title.

   The tear notch is punched 96px in from the card's right edge, and the label
   plus the time needs about 150px of mono — so on a phone the words cannot sit
   clear of it and used to render underneath the notch. The time is the part
   that carries the meaning, so the words are what give way. */
.vc-clock-label { display: none; }
@media (min-width: 560px) {
  .vc-clock-label { display: inline; }
}
.vc-header-clock {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.7;
  white-space: nowrap;
  overflow: hidden;
}

/* ---- body band ---- */
.vc-body {
  grid-area: 2 / 1 / 3 / 2;
  position: relative;
  min-width: 0;
}

/* The flight map, framed on this card's own leg. */
.vc-map {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.vc-land { fill: var(--muted-soft); opacity: 0.17; }
.vc-arc {
  fill: none;
  stroke: var(--muted-soft);
  stroke-width: 1.1;
  stroke-dasharray: 2 3;
  stroke-linecap: round;
  opacity: 0.55;
  /* The two legs differ by two orders of magnitude in span, so the stroke has to
     be measured on screen rather than in the map's own degrees. */
  vector-effect: non-scaling-stroke;
}
.vc-dot { fill: var(--ink); opacity: 0.5; }
/* Shown instead of a plane when there is no leg to fly. */
.vc-marker {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.2;
  opacity: 0.45;
  vector-effect: non-scaling-stroke;
}
.vc-plane { fill: var(--ink); opacity: 0.45; }
.venue-card.active .vc-arc,
.venue-card.active .vc-marker,
.venue-card.active .vc-dot { opacity: 0.75; }
.venue-card.active .vc-plane { opacity: 0.7; }

/* ---- a field: label over value, as every field on a real pass is set ---- */
.vc-cell {
  position: relative;
  min-width: 0;
}
.vc-cell-label {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.16em;
  color: var(--text);
  white-space: nowrap;
}
.vc-cell-value {
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ---- stub ---- */
.vc-stub {
  grid-area: 2 / 2 / 3 / -1;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 0;
  padding: 0 6px;
}

/* The tear runs the full height of the card and is notched at the card's own
   edges. Drawn as its own layer rather than as the stub's border, which stopped
   at the body band and left two circles floating mid-card. */
.vc-tear {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 96px;
  width: 0;
  border-left: 1px dashed var(--card-face);
  pointer-events: none;
}
.vc-tear::before,
.vc-tear::after {
  content: "";
  position: absolute;
  left: -6px;
  width: 11px;
  height: 11px;
  border-radius: 999px;
  background: var(--notch);
}
.vc-tear::before { top: -6px; }
.vc-tear::after { bottom: -6px; }

.vc-codes {
  display: flex;
  align-items: center;
  gap: 5px;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 17px;
  letter-spacing: 0.02em;
  color: var(--ink);
}
/* Drawn, not typed: a plane glyph renders differently on every platform and
   falls back to a box on several. */
.vc-code-plane {
  width: 11px;
  height: 11px;
  flex: 0 0 auto;
  background: var(--ink);
  -webkit-mask: var(--plane-mask) center / contain no-repeat;
  mask: var(--plane-mask) center / contain no-repeat;
}
.vc-cities {
  display: grid;
  gap: 1px;
  width: 100%;
  text-align: center;
}
.vc-city-from,
.vc-city-to {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vc-city-to { color: var(--muted); }

/* ---- the stop list every pass carries ----
   The map behind stays absolute, so the body can leave flex for block. */
.vc-body {
  display: block;
  padding: 13px 11px;
}
.vc-stops { display: grid; }
.vc-stop { display: grid; gap: 1px; }
.vc-stop + .vc-stop {
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px dashed var(--line);
}
.vc-stop-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
/* --faint sat at 3.8:1 on the card stock, so this label carries the darker ink.
   The mono face and the tracking are what keep the ticket tone, not smallness. */
.vc-stop-label {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
}
.vc-stop-time {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.vc-stop-name {
  font-family: var(--serif);
  font-size: 17.5px;
  line-height: 1.15;
  color: var(--ink);
}
.vc-stop-hall {
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text);
}
.vc-stop-address {
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text);
}
/* The map is a background, not a watermark — same as the single-venue body, the
   text needs its own ground to stay legible over the coastline. */
.vc-body .vc-stop-label,
.vc-body .vc-stop-time,
.vc-body .vc-stop-name,
.vc-body .vc-stop-address {
  text-shadow: 0 0 3px var(--white), 0 0 7px var(--white), 0 0 11px var(--white);
}

/* ---- footer band ---- */
.vc-foot {
  grid-area: 3 / 1 / 4 / -1;
  display: flex;
  align-items: center;
  /* Tight on purpose. The card is 382px, not the viewport's width, and at the
     roomier spacing the strip had one pixel spare — the hall field could not
     grow and ellipsed a name that would otherwise have fitted. */
  gap: 8px;
  height: 44px;
  padding: 0 11px;
  background: var(--card-foot);
  border-top: 1px solid var(--line-soft);
}
/* Dashed rules between fields, as the reference sets them. */
.vc-foot > .vc-cell + .vc-cell { padding-left: 8px; border-left: 1px dashed var(--line); }
.vc-foot > .vc-cell:first-child { flex: 1 1 auto; }
.vc-foot > .vc-cell:not(:first-child) { flex: 0 0 auto; }
.vc-barcode {
  /* No auto margin: a flex auto margin takes the free space before flex-grow is
     applied at all, which starved the hall field by exactly the slack sitting
     here — "Grand Ballroom, tầng 3" needs 150px and was given 148. The hall
     growing to fill puts the barcode at the right edge anyway. */
  flex: 0 0 auto;
  width: 46px;
  height: 24px;
  background-image: repeating-linear-gradient(
    to right,
    var(--ink) 0 1px, transparent 1px 3px,
    var(--ink) 3px 5px, transparent 5px 6px,
    var(--ink) 6px 7px, transparent 7px 10px
  );
  opacity: 0.62;
}
.venue-card.active .vc-barcode { opacity: 0.85; }

/* The footer cannot hold three fields, a barcode and the stub below about 400px:
   at 375 the strip needs 363px of a available 305. The flight code is the one to
   go — it is already printed on the stub as the destination code. */
@media (max-width: 400px) {
  .vc-cell-flight { display: none; }
  /* A two-stop pass has no hall field, so once the flight code goes the date is
     the first field left — and `+` counts DOM siblings, not visible ones, so it
     kept a divider with nothing on the other side of it. */
  .vc-foot-stops > .vc-cell-flight + .vc-cell { padding-left: 0; border-left: 0; }
}

/* Narrower still, everything gives a little. A long hall name will ellipse here
   whatever is done — 272px of card cannot hold it beside a date and a barcode —
   and it stays readable in full in the LOCATION section below. */
@media (max-width: 360px) {
  .venue-card { grid-template-columns: 1fr 84px; }
  .vc-tear { right: 84px; }
  .vc-header, .vc-body, .vc-foot { padding-left: 9px; padding-right: 9px; }
  .vc-stop-name { font-size: 16px; }
  .vc-stop-address { font-size: 11.5px; }
  .vc-codes { font-size: 15px; }
  .vc-barcode { width: 40px; }
  .vc-header-clock { font-size: 10px; }
}


/* ============================= WEDDING DAY ==============================
   An embossed panel holding a framed portrait, the month, and the date. The
   panel art is a single image stretched to the box rather than a border-image:
   its corner ornaments are large and irregular, so slicing them cleanly is not
   possible, and the box is kept near the art's own 0.64 aspect so the stretch
   stays imperceptible. */
.wedding-day {
  padding: 60px 20px 50px;
  text-align: center;
}
.wd-panel {
  position: relative;
  max-width: 360px;
  margin: 26px auto 0;
  /* Softened corners: a hard rectangle of ornament butted against the plain
     section read as a pasted-on tile. */
  border-radius: 14px;
  overflow: hidden;
  /* Both the section and this panel are cream cloth, so where they met the
     panel stopped reading as an object and the two textures ran together. A
     soft shadow puts it back on top of the sheet rather than in it. */
  box-shadow: 0 6px 22px rgba(120, 110, 96, 0.16), 0 1px 3px rgba(120, 110, 96, 0.12);
  /* Generous side padding: the art's ornate border occupies roughly the outer
     10% and content must clear it. The bottom is deeper than the top because
     the lower band of relief starts higher — measured at 80% of the art's
     height, against the top band which ends by 12%. The art is stretched to
     this box, so that 80% moves with the box: leaving the last fifth of it
     empty is what takes the venue name off the scallops, and 45% of the width
     is the padding that measures out to a fifth for this much content. */
  padding: 12% 13% 45%;
  background: url("../assets/calendar-bg.webp") center / 100% 100% no-repeat;
}

/* ---- the portrait in its oval frame ----
   The photo sits underneath and the frame is painted over it, so the ornament's
   openwork keeps its own transparency. The inset numbers are the measured
   opening of the frame art — change the art and these must be remeasured. */
.wd-portrait {
  position: relative;
  width: 74%;
  margin: 0 auto 6%;
  aspect-ratio: 560 / 727;
}
.wd-portrait-photo {
  position: absolute;
  left: 20.71%;
  top: 23.66%;
  width: 59.11%;
  height: 62.45%;
  border-radius: 50%;
  overflow: hidden;
  background: var(--cream);
}
.wd-portrait-photo img { width: 100%; height: 100%; object-fit: cover; }
.wd-portrait::after {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../assets/photo-frame.webp") center / contain no-repeat;
  pointer-events: none;
}

/* ---- the calendar, printed on the panel rather than in a tile of its own ---- */
.calendar { margin: 0 auto; }
.cal-month {
  font-family: var(--serif);
  font-style: italic;
  font-size: 21px;
  color: var(--text-mid);
  margin-bottom: 12px;
}
.cal-dow-row,
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.cal-dow-row {
  font-family: var(--serif);
  font-weight: 600;
  font-size: 13.5px;
  letter-spacing: 0.02em;
  color: var(--text-mid);
  margin-bottom: 6px;
}
/* A deeper terracotta than the heart's: --rose is fine for a 30px drawn mark
   but only reached 2.2:1 as a 12px label on the panel's relief. */
.cal-dow-row .sun { color: #7a3f2e; }
.cal-grid { gap: 1px; }
.cal-day {
  position: relative;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--serif);
  font-size: 18px;
  /* Lining figures: the serif's default text figures set a calendar at three
     different heights, which no grid of numbers should be. */
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--text);
}
/* The wedding day: the number stays as it is and a heart is drawn around it. */
.cal-day.is-day { color: var(--ink); }
.cal-heart {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 52px;
  height: 40px;
  /* The drawn loop is not centred in its own file: the alpha centroid of
     heart.webp measures 59.2% across and 37.7% down. Offsetting the box by
     exactly that much is what puts the loop around the figure rather than
     beside it, and lets the swash trail off to the left as it does in the
     drawing. Recompute both numbers if the artwork is ever replaced. */
  transform: translate(-59.2%, -37.7%);
  background-color: var(--rose);
  -webkit-mask: url("../assets/heart.webp") center / contain no-repeat;
  mask: url("../assets/heart.webp") center / contain no-repeat;
  pointer-events: none;
}
.cal-day .mark { position: relative; }

/* ---- the date and the names, closing the panel ---- */
.wd-stamp {
  margin-top: 18px;
  font-family: var(--serif);
  font-size: 19px;
  letter-spacing: 0.04em;
  color: var(--text-mid);
  font-variant-numeric: lining-nums;
  text-shadow: 0 0 6px rgba(255, 255, 255, 0.95), 0 0 12px rgba(255, 255, 255, 0.75);
}
.wd-script {
  margin-top: 7px;
  /* The serif at 600, not the script: Pinyon is loaded at one weight, so there
     is no heavier cut of it, and its hairlines disappeared into the panel's
     relief at every size that also fit "Sheraton Saigon Grand Opera Hotel". */
  font-family: var(--serif);
  font-weight: 600;
  font-size: 23px;
  line-height: 1.32;
  letter-spacing: 0.01em;
  color: var(--ink);
  /* The relief is busy enough to break up a line of type, so the name carries
     a little paper of its own behind it. */
  text-shadow: 0 0 7px rgba(255, 255, 255, 0.96), 0 0 16px rgba(255, 255, 255, 0.8);
}

/* ---- the countdown ----
   Set as type, not as four boxes. The bordered grey tiles read as interface
   chrome directly under an embossed antique panel; the same figures with the
   panel's serif and a hairline between them belong to it instead. */
.countdown {
  display: flex;
  justify-content: center;
  align-items: stretch;
  margin-top: 24px;
}
.cd-cell {
  min-width: 66px;
  padding: 2px 4px;
  display: grid;
  gap: 6px;
  justify-items: center;
  align-content: start;
}
.cd-cell + .cd-cell { border-left: 1px solid var(--line-soft); }
.cd-num {
  font-family: var(--serif);
  font-size: 30px;
  line-height: 1;
  color: var(--text-mid);
  /* Lining figures: the serif's default text figures would set the four
     numbers at three different heights, which a clock should not do. */
  font-variant-numeric: lining-nums tabular-nums;
}
.cd-lbl {
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
}

/* Below ~360px the panel's 13% side padding leaves too little for seven
   columns, so it tightens and the day cells shrink with it. */
@media (max-width: 360px) {
  .wd-panel { padding: 11% 9% 52%; }
  .cal-day { height: 26px; font-size: 16px; }
  .cal-dow-row { font-size: 12.5px; }
  /* The heart has to shrink with the column it sits in: at its full size it is
     wider than a 32px cell and pushes the grid out. */
  .cal-heart { width: 34px; height: 26px; }
  .wd-script { font-size: 20px; }
  .cd-cell { min-width: 58px; }
  .cd-num { font-size: 26px; }
  .cd-lbl { font-size: 11px; letter-spacing: 0.14em; }
}

/* ================================ TIMELINE ==============================
   Two stops to a row, separated by hairlines rather than by gaps, so the set
   reads as one printed panel the way a paper run sheet does. */
.timeline { padding: 0 20px 54px; }
.timeline[hidden] { display: none; }
.timeline .eyebrow { padding: 14px 0 26px; }

.tl-grid {
  list-style: none;
  display: grid;
  grid-template-columns: 1fr 1fr;
  max-width: 380px;
  margin: 0 auto;
  border-top: 1px solid var(--line-soft);
}
.tl-cell {
  display: grid;
  justify-items: center;
  align-content: start;
  padding: 24px 10px 22px;
  border-bottom: 1px solid var(--line-soft);
  min-width: 0;
}
/* The column rule belongs to the left-hand cell, so it never lands on the
   panel's outer edge. */
.tl-cell:nth-child(odd) { border-right: 1px solid var(--line-soft); }
/* An odd number of stops would otherwise leave that rule dangling into empty
   space, so the last one takes the full width instead. */
.tl-cell:last-child:nth-child(odd) {
  grid-column: 1 / -1;
  border-right: 0;
}

/* A fixed box whatever the drawing's proportions — the wide ones (the car) and
   the tall ones (the champagne tower) then sit on a common baseline instead of
   each setting its own row height. */
.tl-art {
  width: 66px;
  height: 66px;
  margin-bottom: 14px;
}
.tl-art img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Stacked rather than one wide blur: a tight pass fills between the strokes,
     the wider one lifts the whole drawing off the weave behind it. */
  filter:
    drop-shadow(0 0 2px rgba(255, 255, 255, 0.95))
    drop-shadow(0 0 5px rgba(255, 255, 255, 0.85))
    drop-shadow(0 0 11px rgba(255, 255, 255, 0.6));
}

.tl-time {
  font-family: var(--serif);
  font-size: 16px;
  letter-spacing: 0.13em;
  color: var(--text);   /* not --text-mid: the coupes ground behind it is darker than the linen was */
  /* lining-nums, or Cormorant's default text figures set "18:00" at three
     different heights and turn the 1 into something that reads as a small-cap I.
     A clock face wants figures that all sit on the same two lines. */
  font-variant-numeric: lining-nums tabular-nums;
}
.tl-what {
  margin-top: 6px;
  font-family: var(--serif);
  font-style: italic;
  font-size: 17px;
  line-height: 1.35;
  color: var(--text);
  text-align: center;
  text-wrap: balance;
}

/* Below ~360px the two columns cannot hold a 56px drawing and a wrapped label
   without the words breaking mid-syllable, so everything gives a little. */
@media (max-width: 360px) {
  .tl-cell { padding: 20px 6px 18px; }
  .tl-art { width: 54px; height: 54px; margin-bottom: 11px; }
  .tl-time { font-size: 15px; letter-spacing: 0.1em; }
  .tl-what { font-size: 15.5px; }
}

/* ================================ LOCATION ============================== */
.location { padding: 0 0 56px; }
.location .eyebrow { padding: 14px 0 26px; }

.loc-date {
  text-align: center;
  padding: 0 24px 22px;
  font-size: 15px;
  letter-spacing: 0.02em;
  color: var(--text-mid);
}
/* One shape at a time — see renderSelectedVenue. */
.loc-date[hidden],
.loc-head[hidden],
.loc-places[hidden],
.loc-map-wrap[hidden] { display: none; }

.loc-head { text-align: center; padding: 0 24px; }
.loc-name { font-family: var(--serif); font-size: 26px; color: var(--text-mid); }
.loc-hall { font-size: 15.5px; color: var(--muted); margin-top: 6px; }
.loc-hall[hidden] { display: none; }
.loc-address { font-size: 15px; color: var(--muted); margin-top: 4px; }

.loc-map-wrap { padding: 0 24px; margin-top: 22px; }
/* display:block is required: this slot is an <a>, and an inline box ignores
   height and overflow, collapsing the panel.
   aspect-ratio rather than a fixed height, matched to the generated map images
   (620x300): with a fixed height the slot's aspect ranged 1.30-2.06 across
   viewports while the image is 2.07, so object-fit:cover cropped the sides and
   clipped the baked-in Mapbox/OpenStreetMap attribution — which is a licence
   condition, not decoration. Matching the aspect means nothing is ever cropped. */
.loc-map {
  display: block;
  position: relative;
  aspect-ratio: 620 / 300;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: #f7f3ec; /* the tinted maps' own background, so any gap is seamless */
}
.loc-links {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 10px;
}
.loc-links .btn-outline { padding: 12px 0; }

.loc-places {
  display: grid;
  gap: 26px;
  padding: 0 24px;
}
.loc-place-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 7px;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--line-soft);
}
.loc-place-label {
  font-size: 13.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
}
.loc-place-time {
  font-family: var(--mono);
  font-size: 13.5px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.loc-place-name {
  font-family: var(--serif);
  font-size: 22px;
  line-height: 1.25;
  color: var(--text-mid);
}
.loc-place-hall {
  font-size: 15px;
  color: var(--text);
  margin-top: 3px;
}
/* An address is the one thing on this card a guest copies into a phone. */
.loc-place-address {
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--text);
  margin-top: 4px;
}

/* ================================= GALLERY ============================== */
.gallery {
  padding: 70px 24px 64px;
  text-align: center;
}
.gallery-title {
  font-size: 16px;
  font-weight: 400;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: var(--ink);
  margin-bottom: 44px;
}

.slides {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: #efefef;
}
.slides-track {
  display: flex;
  width: 100%;
  height: 100%;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide { flex: 0 0 100%; min-width: 0; }

.thumbs-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 24px;
}
.slide-arrow {
  flex: 0 0 auto;
  width: 22px;
  height: 56px;
  border: none;
  background: none;
  padding: 0;
  font-size: 22px;
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
}
.thumbs-window { flex: 1 1 auto; overflow: hidden; }
.thumbs-track {
  display: flex;
  gap: 8px;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.thumb {
  flex: 0 0 66px;
  height: 80px;
  padding: 0;
  border: 1px solid transparent;
  background: #efefef;
  overflow: hidden;
  opacity: 0.45;
  cursor: pointer;
  transition: opacity 0.3s ease, border-color 0.3s ease;
}
.thumb.active { opacity: 1; border-color: var(--ink); }

/* ============================== WISHING WELL =============================
   Set as a note tinted and ruled off from the page, because it is the one thing
   on the card a guest is asked to take in besides the RSVP — as another run of
   centred grey text it read as the quietest block here and scrolled straight
   past. */
.wishing-well { padding: 54px 18px; }
/* The lace sits behind the note rather than a tinted box: the art is white on
   transparent, so it reads against the card's own paper. Side padding is a
   percentage so the text keeps clear of the lace border at every width, and the
   art is stretched to the box — lace tolerates that where an ornate corner
   would not. */
.ww-note {
  max-width: 372px;
  margin: 0 auto;
  /* Locked to the lace art's own 720x960 so the oval can never distort. With a
     content-driven height the box stretched taller than the art as the copy
     re-wrapped — 0.60 against the art's 0.75 at 320px — and the text drifted
     below the oval's centre. Centring the content vertically then keeps it
     registered to the oval at any width, and if the copy ever outgrows the box
     it simply grows and stays centred rather than sliding down. */
  aspect-ratio: 720 / 960;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 19%;
  text-align: center;
  background: url("../assets/wishing-lace.webp") center / 100% 100% no-repeat;
  /* The lace opens as an oval — scanning the art row by row, the gap is 46% of
     the box wide a fifth of the way down, 68% across the middle, 34% again near
     the foot — while the copy below is set in a column of one width. So the
     type is sized off this box rather than off the viewport: shrink the lace
     and the words shrink with it, and the lines stay inside the opening at
     every width instead of at the two the breakpoints happened to name. */
  container-type: inline-size;
}

/* Narrow screens: the lace is a fixed shape but the copy re-wraps into more
   lines, so only the type gives way. The side padding stays at the base 19% —
   it is what holds the prose inside the oval's opening, and the 13% this used
   to buy width with let the serif's longer lines run out over the lace. */
@media (max-width: 360px) {
  .ww-body { line-height: 1.7; }
}

/* Set as the invitation is set: the script face for the opening, the serif's
   italic for everything under it. This block asks the same thing of a guest as
   the invitation does, so it reads in the same voice rather than in the form's. */
.ww-title {
  font-family: var(--script);
  font-weight: 400;
  font-size: 27px;
  font-size: min(27px, 12.8cqw);
  line-height: 1.3;
  color: var(--ink);
}
/* The sentence that carries the meaning, so it is the one set large. */
.ww-lead {
  margin-top: 10px;
  font-family: var(--serif);
  font-style: italic;
  font-size: 16px;
  font-size: min(16px, 7.6cqw);
  line-height: 1.6;
  color: var(--ink-soft);
  text-wrap: balance;
}
.ww-body {
  margin-top: 11px;
  font-family: var(--serif);
  font-style: italic;
  font-size: 15px;
  font-size: min(15px, 7.1cqw);
  line-height: 1.75;
  color: var(--text);
  text-wrap: pretty;
}

/* ================================== RSVP ================================ */
/* White, as in the reference: the filled fields and the flat submit are both
   pale greys, and on a grey band they stop reading as controls at all. */
.rsvp {
  padding: 56px 26px 64px;
}
/* The intro and the deadline read as one block, so the air before the form is
   owned by the form (and by the thank-you that replaces it) rather than by
   whichever line happens to come last. */
/* Same reasoning as .picker-cta: these two are asks, not captions. */
.rsvp-intro {
  text-align: center;
  font-family: var(--serif);
  font-style: italic;
  font-size: 18px;
  line-height: 1.5;
  color: var(--ink);
  margin-top: 14px;
  text-wrap: balance;
}
.rsvp-deadline {
  text-align: center;
  font-family: var(--serif);
  font-style: italic;
  font-size: 16px;
  line-height: 1.5;
  color: var(--text);   /* not --text-mid: the satin ground behind it is darker than the linen was */
  margin-top: 9px;
  text-wrap: balance;
}
.rsvp-form,
.rsvp-thanks { margin-top: 28px; }

.rsvp-thanks {
  padding: 34px 20px;
  text-align: center;
  background: var(--cream-deep);
  border: 1px solid #dbdbdb;
}
.rsvp-thanks[hidden] { display: none; }
.thanks-title { font-family: var(--serif); font-size: 24px; color: var(--text-mid); }
.thanks-body {
  font-size: 15px;
  line-height: 1.9;
  color: var(--muted-soft);
  margin-top: 10px;
}

.rsvp-form { display: grid; gap: 20px; }
.rsvp-form[hidden] { display: none; }
.field { display: grid; gap: 8px; min-width: 0; }
/* The stepper is three fixed controls, so it takes only what it needs and the
   name field keeps the rest. Splitting the row evenly left the name at 182px
   next to 56px of empty column. */
.field-row { display: grid; grid-template-columns: 1fr auto; gap: 14px; }
.field[hidden] { display: none; }
/* --muted, not --faint: these label the fields a guest has to fill in, and at
   12px --faint sits at 3.5:1 on white and 3.9:1 on the linen, both under the
   4.5 minimum. --muted clears 5:1 on either ground. */
.field-label { font-size: 15px; letter-spacing: 0.1em; color: var(--text); }
/* The question in full, under its label — these two ask something a four-word
   label cannot, and the allergy one has to name the raw seafood. */
/* These carry the actual questions a guest answers, so they are body copy, not
   a caption. --hint measured 2.6:1 against the paper. */
.field-hint {
  font-size: 15.5px;
  line-height: 1.75;
  color: var(--text);
  margin-top: -2px;
}

/* White paper with an edge, not a grey fill. The reference set these as
   #efefef inside a hairline, which held on the pale linen but disappeared once
   this section took the satin: the fill measured 1.13:1 against the highlight
   directly behind it. The outer shadow is what draws the box where the silk is
   bright, and the white fill is what holds it where the silk goes dark. */
.rsvp-form input,
.rsvp-form textarea {
  padding: 12px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--white);
  box-shadow:
    inset 0 1px 2px rgba(120, 110, 96, 0.10),
    0 1px 3px rgba(120, 110, 96, 0.12);
  /* 16px is a floor, not a taste. Safari on iOS zooms the page whenever a text
     field it focuses is set smaller than this, and then leaves the guest zoomed
     in on a half-filled form. The alternative fix — user-scalable=no on the
     viewport — stops the zoom by taking pinch-zoom away from everyone, which is
     the wrong trade on an invitation older guests will read. */
  font-size: 16px;
  color: var(--ink);
  outline: none;
  min-width: 0;
}
.rsvp-form textarea { resize: vertical; }
.rsvp-form input:focus,
.rsvp-form textarea:focus { border-color: var(--accent); }

/* Guest count as a stepper: invalid input is impossible by construction, so the
   form needs no error copy for this field. */
.guest-stepper {
  display: flex;
  align-items: center;
  gap: 10px;
}
.guest-step {
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--white);
  box-shadow: 0 1px 2px rgba(120, 110, 96, 0.07);
  color: var(--text);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.guest-step:hover:not([aria-disabled="true"]) { border-color: var(--accent); color: var(--accent); }
.guest-step[aria-disabled="true"] {
  color: var(--line);
  box-shadow: none;
  cursor: default;
}
.guest-count {
  min-width: 32px;
  text-align: center;
  font-family: var(--serif);
  font-size: 26px;
  color: var(--ink);
  font-variant-numeric: tabular-nums lining-nums;
}
#guests-field[hidden] { display: none; }
/* Reclaim the empty column so the name field spans the row on a decline. */
.field-row:has(> #guests-field[hidden]) { grid-template-columns: 1fr; }

/* Grouped by country, so four near-identical rows across two countries cannot
   be mistaken for one another. */
.venue-choices {
  display: grid;
  gap: 22px;
  /* Extra air under the field label: without it "WHICH CELEBRATION" and the
     first country heading read as two stacked labels rather than a label and
     the group it introduces. */
  margin-top: 8px;
}
.venue-choices-list { display: grid; gap: 8px; }
.venue-group { display: grid; gap: 9px; }
.venue-group-title {
  font-family: var(--mono);
  font-size: 13.5px;
  letter-spacing: 0.16em;
  color: var(--text);
  padding-bottom: 7px;
  border-bottom: 1px solid var(--line-soft);
}
/* Belongs to neither country, so it is set apart from both rather than reading
   as the last entry of whichever group happens to come above it. */
.venue-decline { margin-top: -6px; }
.venue-choice {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 15px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  background: var(--white);
  color: var(--text);
  font-size: 16.5px;
  text-align: left;
  cursor: pointer;
}
/* Both options keep the white paper. Filling the selected one with grey made
   it the recessed box beside a bright white one, so the choice a guest had
   already made read as the disabled half of the pair. The mark is the rose
   border instead — the same ink that rings the day on the calendar. */
.venue-choice.active {
  border: 2px solid var(--rose);
  padding: 14px;
  box-shadow: 0 1px 6px rgba(192, 138, 122, 0.16);
}
.venue-choice.active .venue-choice-date { color: var(--rose); }
.venue-choice-date { color: var(--text); font-size: 14.5px; white-space: nowrap; }

/* Honeypot: removed from view and from the layout without display:none, which
   many bots treat as a signal to skip the field. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.rsvp-error {
  font-size: 13.5px;
  line-height: 1.7;
  color: #a4564a;
  text-align: center;
}
.rsvp-error:empty { display: none; }

/* ================================= FOOTER =============================== */
.footer {
  padding: 40px 24px 100px;
  text-align: center;
  background: var(--page);   /* the reference closes on its base tone */
  container-type: inline-size;   /* the names below are sized off this column */
}
/* Same reasoning as .invite-signature: at 34px the names need 353px against the
   342px a 390px phone gives them, and the line broke with "Thuỳ" left alone
   under it. This face sets them at 10.4px of width per px of type. */
.footer-names {
  font-family: var(--script);
  font-weight: 400;
  font-size: 34px;
  font-size: min(34px, 9.4cqw);
  word-spacing: 0.06em;   /* the script's word space is too tight around the "&" */
  color: var(--accent);
}
.footer-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin: 22px 0 18px;
}
.footer-note { font-size: 14.5px; letter-spacing: 0.08em; color: var(--text); }

/* A figure that has just changed settles into place, so the countdown reads as
   running rather than as a number that happens to be different each time it is
   looked at. Only the cell whose value moved is touched. */
.cd-num.tick { animation: cdTick 0.45s cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes cdTick {
  from { opacity: 0.25; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

/* ============================ SCROLL REVEAL ============================= */
/* A section fades in as it is reached and its own parts follow, one after the
   other. The movement moved from the section to its contents deliberately: a
   whole slab sliding up reads as a page being built, where a title settling
   before the line under it reads as a card being unfolded. */
.reveal {
  opacity: 0;
  transition: opacity 0.7s ease;
}
.reveal.visible { opacity: 1; }

.reveal-item {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 0.7s ease var(--stagger, 0ms),
    transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) var(--stagger, 0ms);
}
.reveal-item.visible { opacity: 1; transform: none; }

/* The hairline under a title draws itself downward instead of fading in as a
   finished stroke — it is a drawn line, so it arrives like one. */
.reveal-item.drop-rule {
  transform: scaleY(0);
  transform-origin: top center;
}
.reveal-item.drop-rule.visible { transform: scaleY(1); }

/* Narrow phones: the cover name line must not overflow the card. */
@media (max-width: 380px) {
  .cd-cell { min-width: 54px; }
}

@media (prefers-reduced-motion: reduce) {
  .envelope,
  .env-offer,
  .env-veil,
  .reveal-item,
  .slides-track,
  .thumbs-track,
  .venue-card,
  .lang-thumb,
  .reveal {
    transition-duration: 0.01ms;
  }
  .envelope.sealed .env-hint { animation: none; }
  .reveal-item { opacity: 1; transform: none; }
  .reveal-item.drop-rule { transform: scaleY(1); }
  .cd-num.tick { animation: none; }
  .reveal { opacity: 1; transform: none; }
}
