/* ---------- Tokens (from Figma) ---------- */
:root {
  --wine: #744852;
  --wine-deep: #744853;
  --blush: #f6e5e4;
  --rose: #d4a5a5;
  --white: #fff;

  --font-display: "le Marino", "Instrument Serif", Georgia, serif;
  --font-sans: "Satoshi", system-ui, -apple-system, sans-serif;
  --font-ui: "Fraunces", Georgia, serif;
  --font-hand: "Grey Qo", cursive;

  /* Every section in the mock is one 393px phone column; positions below are in those pixels. */
  --screen-w: 393px;
  --screen-h: 852px;
}
/* Russian: the faces above have no Cyrillic, so kindred ones stand in (fetched by main.js the first
   time Russian is chosen). Uzbek is written in Latin letters and keeps the faces above. */
html[lang="ru"] {
  --font-display: "Cormorant Garamond", "le Marino", Georgia, serif;
  --font-sans: "Manrope", system-ui, -apple-system, sans-serif;
  --font-ui: "Lora", Georgia, serif;
}

/* ---------- Base ---------- */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html { scroll-behavior: smooth; }
html.is-locked { overflow: hidden; }

/* What Safari shows behind the clock when nothing fixed is there: the screen at the top (main.js). */
html { background: var(--top, var(--wine-deep)); }

body {
  margin: 0;
  /* main.js keeps this in step with the screen in view, so a desktop's margins match it. */
  background: var(--side, var(--wine-deep));
  transition: background-color 0.5s ease;
  color: var(--wine);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

h1, h2, h3, p, ol, fieldset, legend { margin: 0; padding: 0; font-weight: 400; }
ol { list-style: none; }
fieldset { border: 0; min-width: 0; }
img { display: block; max-width: none; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }

.app {
  position: relative;
  max-width: var(--screen-w);
  margin: 0 auto;
  overflow-x: clip;
  background: var(--white);
}

/* Every screen is as tall as the first: the window at its largest (see .hero), and never shorter
   than the mock's own height. A screen's board (--stage-h of the mock's pixels) sits in its middle,
   so a taller window adds space evenly above and below rather than a strip at the bottom. */
:root { --screen-full: calc(max(var(--hero-h, 100svh), var(--hero-grow, 0px), 100lvh) + 96px); }
.screen {
  --stage-h: var(--screen-h);
  position: relative;
  min-height: max(var(--stage-h), var(--screen-full));
  overflow: hidden;
  isolation: isolate;
  background: var(--white);
}
.screen--wine  { background: var(--wine-deep); color: var(--blush); }
.screen--blush { background: var(--blush); }

/* The mock's 393px column, centred: children are placed in its pixels. */
.stage {
  position: absolute;
  inset: 0 auto 0 50%;
  width: var(--screen-w);
  translate: -50% 0;
}
.screen > .stage { top: calc((100% - var(--stage-h)) / 2); bottom: auto; height: var(--stage-h); }

/* Absolutely placed block: --x / --y / --w come straight from the mock. */
.at {
  position: absolute;
  left: var(--x, 0);
  top: var(--y, 0);
  width: var(--w, auto);
}

/* On a phone the page fills the screen edge to edge instead of the 393px column of the mock. */
@media (max-width: 600px) {
  .app { max-width: none; }
}

/* Blocks lift into place the first time they are scrolled to. Nothing is hidden unless main.js
   has said it will bring it back. `translate` so it composes with any transform. */
.is-animated .rise { opacity: 0; translate: 0 16px; }
.is-animated .rise.is-in {
  opacity: 1;
  translate: none;
  transition: opacity 0.7s ease var(--in, 0s), translate 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) var(--in, 0s);
}

/* ---------- Type ---------- */
.t-display  { font: italic 400 48px/52px var(--font-display); letter-spacing: -0.01em; }
.t-title    { font: italic 400 40px/44px var(--font-display); letter-spacing: -0.01em; }
.t-subtitle { font: italic 400 32px/36px var(--font-display); letter-spacing: -0.01em; }
.t-sheet    { font: italic 400 24px/28px var(--font-display); letter-spacing: -0.01em; }
.t-couple   { font: italic 400 28px/32px var(--font-display); letter-spacing: -0.01em; }
.t-lead     { font: 400 18px/22px var(--font-sans); letter-spacing: -0.01em; }
.t-body     { font: 400 16px/20px var(--font-sans); letter-spacing: -0.01em; }
.t-small    { font: 400 14px/18px var(--font-sans); letter-spacing: -0.01em; }
.t-date     { font: 700 20px/22px var(--font-sans); letter-spacing: -0.01em; }
.t-time     { font: 400 28px/32px var(--font-sans); letter-spacing: -0.01em; }
.t-moment   { font: 300 18px/22px var(--font-sans); letter-spacing: -0.01em; }
.stage .at, .hero__group .at, .heading { text-align: center; }

/* ---------- Shared pieces ---------- */
.heading { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.date .heading { gap: 24px; }
/* The machine's screen. Its board is laid out in the window as the invitation opened (--pin-h),
   from the top, scaled down if that window is shorter than the mock (--dtk, main.js):
   · while the page stops here, the screen is as tall as the window can grow, so it ends where the page
     ends with nothing of the screen before showing above it, and anything past the end (under the
     toolbar, or in a rubber-band) is blush;
   · once the lever has been pulled it grows downwards to the shared screen height, so the room
     under the toolbar is still this screen's blush, not the next screen, and nothing on it moves. */
.screen.date > .stage {
  top: 0;
  bottom: auto;
  height: var(--stage-h);
  transform-origin: 50% 0;
  scale: var(--dtk, 1);
  /* Centred in the window, with the window measured up from the screen's bottom (where the page
     ends while it is held): any extra height a taller window (Safari's toolbars folded away) can
     need is room at the top, so nothing of the screen before ever shows. */
  translate: -50% calc(var(--date-top) + var(--date-held) - var(--pin-h) + (var(--pin-h) - var(--stage-h) * var(--dtk, 1)) / 2);
}
/* --date-top: room above the window. Safari 26's strip behind the clock is see-through and shows
   the page above the window's top edge (measured with ?debug on an iPhone: the screen's top was
   exactly at the window's top and the screen before still showed behind the clock). */
:root { --date-held: max(var(--pin-h), 100lvh); --date-top: 100px; }
.is-held .screen.date { min-height: 0; height: calc(var(--date-top) + var(--date-held)); }
.screen.date { min-height: calc(var(--date-top) + var(--date-held) + 96px); }
html.is-held { background: var(--blush); }
.date .heading .t-lead { margin-bottom: -4px; } /* the mock pairs the eyebrow and title at 20px */
.rsvp .heading { gap: 12px; }

/* A rose line either side of a small wine diamond. */
.divider { display: flex; align-items: center; justify-content: center; gap: 12px; height: 11.314px; }
.divider span { width: 120px; height: 1px; background: var(--rose); }
.divider i { width: 8px; height: 8px; background: var(--wine); rotate: 45deg; margin: 0 1.66px; }

/* Icons are the exported SVGs used as masks, so they take the colour of the text around them. */
.icon, .logo, .flower {
  display: block;
  flex: none;
  background: currentColor;
  -webkit-mask: var(--src) center / contain no-repeat;
  mask: var(--src) center / contain no-repeat;
}
.logo { width: 94.38px; height: 22px; --src: url("../assets/icons/logo.svg"); }
.icon--globe    { width: 16px; height: 16px; --src: url("../assets/icons/globe.svg"); -webkit-mask-size: 14.33px; mask-size: 14.33px; }
.icon--chevron  { width: 14px; height: 14px; --src: url("../assets/icons/chevron-down.svg"); -webkit-mask-size: 8px; mask-size: 8px; }
.icon--sound    { width: 24px; height: 24px; --src: url("../assets/icons/sound-off.svg"); }
.logo-svg { display: block; flex: none; width: 94.38px; height: 22px; }
.bar-icon { display: block; flex: none; }
.bar-icon--globe { width: 14.33px; height: 14.33px; margin: 0.83px; }
.bar-icon--chevron { width: 8px; height: 4.4px; margin: 4.8px 3px; }
.bar-icon--sound { width: 24px; height: 24px; }
.icon--arrow    { width: 14px; height: 17px; --src: url("../assets/icons/arrow-down.svg"); -webkit-mask-position: top; mask-position: top; }
.icon--calendar { width: 17.78px; height: 20px; --src: url("../assets/icons/calendar.svg"); }
.icon--pin      { width: 17.78px; height: 20px; --src: url("../assets/icons/pin.svg"); }
.flower         { width: 28px; height: 28px; --src: url("../assets/icons/flower-dark.svg"); opacity: 0.7; }
.flower--light  { --src: url("../assets/icons/flower-light.svg"); }

.flowers-row {
  position: absolute;
  left: 24px;
  top: 24px;
  width: 345px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.flowers-row--line span { width: 180px; height: 1px; background: rgba(246, 229, 228, 0.3); }

.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 12px 24px;
  border-radius: 8px;
  font: 400 20px/20px var(--font-display);
  letter-spacing: 0.01em;
  white-space: nowrap;
  transition: filter 0.2s ease;
}
.btn--wine  { background: var(--wine); color: var(--blush); }
.btn--blush { background: var(--blush); color: var(--wine); }
@media (hover: hover) { .btn:hover { filter: brightness(1.1); } }
/* A placed button keeps its centre and its width from the mock as a minimum, but grows to its
   words (both ways) when a language runs longer. transform composes with the translate the
   buttons use to come up. */
.btn.at { left: calc(var(--x) + var(--w) / 2); width: auto; min-width: var(--w); transform: translateX(-50%); }

/* Pictures that fill their screen whatever its proportions. */
.cover { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -1; }

/* ---------- Preloader ---------- */
.loader {
  position: fixed;
  inset: 0 0 -40vh;
  padding-bottom: 40vh;
  z-index: 70;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 20px;
  background: var(--wine-deep);
  color: var(--blush);
  transition: opacity 0.7s ease;
}
.loader__logo { opacity: 0.9; }
.loader__track { width: 132px; height: 1px; overflow: hidden; background: rgba(246, 229, 228, 0.18); }
.loader__track i {
  display: block;
  width: calc(var(--load, 0) * 100%);
  height: 100%;
  background: var(--blush);
  transition: width 0.4s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.loader.is-done { opacity: 0; pointer-events: none; }

/* ---------- 0. Gate ---------- */
/* In-app browsers (Telegram) and iOS 26 Safari float a see-through toolbar over the bottom of the
   page, below the edge of what "fixed, inset 0" reaches: the gate runs on under it, and its padding
   keeps the booth centred in the part of the screen that is clear. */
.gate {
  position: fixed;
  inset: 0 0 -40vh;
  padding-bottom: 40vh;
  z-index: 50;
  display: grid;
  place-items: center;
  background: var(--wine-deep);
  color: var(--blush);
  cursor: pointer;
  outline: none;
  transition: opacity 0.8s ease, visibility 0s linear 0.8s;
}
.gate.is-open { opacity: 0; visibility: hidden; }
.gate.is-handover { background: transparent; transition: none; }
.gate__stage { position: relative; width: 393px; height: 590px; }
.gate__booth { width: 393px; height: 590px; }
/* The slot's opening runs 173–183px down the 590px booth; the card vanishes above its lower lip. */
.gate__mail { position: absolute; inset: 0; }
.gate__mail.is-posting { clip-path: inset(180px -40px -40px -40px); }
.gate__card { position: absolute; inset: 0; transform-origin: 192.5px 269px; filter: drop-shadow(0 3px 4px rgba(40, 10, 20, 0.25)); } /* lying on the booth */
.gate__heart { position: absolute; left: 72px; top: 159px; width: 241px; height: 220px; transition: scale 0.2s ease; }
.gate:not(.is-opening):active .gate__heart { scale: 0.97; }
.gate.is-opening { cursor: default; }
/* Centred, by its own middle, on the wine heart's centre (its centre of mass in the artwork), so any
   language and any number of lines sits in the middle of it. */
.gate__hint {
  position: absolute;
  left: 196.3px;
  top: 262.5px;
  translate: -50% -50%;
  rotate: -15deg;
  font: italic 400 20px/24px var(--font-display);
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
}
.gate .flowers-row { position: fixed; top: auto; bottom: 24px; left: 50%; translate: -50% 0; }

/* ---------- Top bar ---------- */
.topbar {
  position: fixed;
  z-index: 30;
  top: 0;
  left: 50%;
  width: min(100%, var(--screen-w));
  translate: -50% 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 24px;
  color: var(--blush);
  /* Solid: Safari 26 carries a fixed top bar's colour up into the strip behind the clock, and it
     takes only an opaque one. */
  background: var(--bar-tint, var(--wine-deep));
  transition: translate 0.28s cubic-bezier(0.3, 0, 0.2, 1), background-color 0.35s ease, color 0.35s ease;
}
/* Safari 26 paints the strip behind the clock in the colour of a fixed bar at the top edge, and
   keeps that colour for as long as the bar is in the page, even slid away or invisible (tested on
   an iPhone: with no bar at all, the strip simply shows the page through). So once hidden the bar
   leaves the page (.is-gone, set by main.js when the slide ends) and the strip shows the page. */
.topbar.is-hidden { translate: -50% -100%; }
.topbar.is-gone { display: none; }
.topbar.is-dark { color: var(--wine); }
.is-locked .topbar { z-index: 60; } /* the booth screen carries the bar too */
.topbar__tools { display: flex; align-items: center; gap: 12px; }
.lang { position: relative; }
.lang__button { display: flex; align-items: center; gap: 4px; padding: 0; border: 0; background: none; color: inherit; font: 400 14px/18px var(--font-ui); letter-spacing: -0.01em; cursor: pointer; }
.lang .bar-icon--chevron { transition: rotate 0.25s ease; }
.lang.is-open .bar-icon--chevron { rotate: 180deg; }
.lang__menu {
  position: absolute;
  right: -6px;
  top: calc(100% + 12px);
  min-width: 140px;
  margin: 0;
  padding: 6px;
  list-style: none;
  border-radius: 10px;
  background: #fffaf8;
  color: var(--wine);
  box-shadow: 0 0 0 0.6px rgba(116, 72, 83, 0.18), 0 12px 28px rgba(67, 37, 45, 0.18);
  animation: menu-in 0.25s cubic-bezier(0.22, 0.61, 0.36, 1);
}
@keyframes menu-in { from { opacity: 0; translate: 0 -6px; } }
.lang__menu button {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: inherit;
  text-align: left;
  font: 400 16px/20px var(--font-ui);
  cursor: pointer;
  transition: background 0.2s ease;
}
.lang__menu button[aria-selected="true"] { background: rgba(116, 72, 83, 0.1); }
@media (hover: hover) { .lang__menu button:hover { background: rgba(116, 72, 83, 0.06); } }
.sound { width: 24px; height: 24px; }
.sound.is-empty { opacity: 0.45; }
/* The button's wave: a sine that runs along through the ring (fading at its ends) while the music
   plays, and settles into a still, flat line when it is off. One wavelength is 5 units, so moving
   it by 5 loops it without a seam; the stroke keeps its weight however the wave is squashed. */
.sound__wave { animation: wave-run 1.2s linear infinite paused; }
.sound[aria-pressed="true"] .sound__wave { animation-play-state: running; }
@keyframes wave-run { to { translate: -5px 0; } }
.sound__line { transform-box: view-box; transform-origin: 12px 12px; scale: 1 0.08; transition: scale 0.6s cubic-bezier(0.45, 0, 0.2, 1); }
.sound[aria-pressed="true"] .sound__line { scale: 1 1; }
@media (prefers-reduced-motion: reduce) { .sound__wave { animation: none; } }
@media (max-width: 600px) { .topbar { width: 100%; } }

/* ---------- 1. Invitation ---------- */
/* The strip is only ever seen below the slot (421.7px down the stage: 180px of the 590px booth,
   scaled to 270px wide and placed at 298px). */
/* The first screen is exactly the window the invitation opened in (--hero-h, locked by main.js so
   Safari's and Telegram's toolbars coming and going don't re-lay it out): the block of names and
   booth sits centred between the top bar (64px) and the scroll hint, scaled down by --hero-k when
   the window is shorter or narrower than the mock. */
/* The content is laid out in the window as it opened (--hero-h); the screen itself is at least as
   tall as the window can ever grow (100lvh, or --hero-grow when an in-app browser grows its web
   view instead), so when the toolbars slide away the next screen doesn't peek in underneath. */
/* Plus a margin of wine below: iOS Safari and Telegram float a see-through toolbar over the page,
   and what shows through it is the page *below* the window's own edge (measured with ?debug on an
   iPhone: Safari window 781, Telegram 732, and the next screen showing under both toolbars). */
.hero { min-height: 0; height: var(--screen-full); }
.hero__group {
  position: absolute;
  left: 50%;
  top: calc(64px + (var(--hero-h, 100svh) - 64px - 72px - 583px * var(--hero-k, 1)) / 2);
  width: 393px;
  height: 583px;
  translate: -50% 0;
  transform-origin: 50% 0;
  scale: var(--hero-k, 1);
}
.hero .scroll-hint { position: absolute; left: 50%; top: calc(var(--hero-h, 100svh) - 51px - env(safe-area-inset-bottom)); translate: -50% 0; width: 120px; }
.hero__outlet { position: absolute; inset: 0; clip-path: inset(301.7px 0 0 0); pointer-events: none; }
/* Until the booth has settled into place the hero holds its words, its booth and its strip back:
   main.js releases them one by one as the gate hands over. */
.is-intro .hero__title, .is-unprinted .scroll-hint { opacity: 0; }
.is-intro .hero__booth { visibility: hidden; }
.is-unprinted .hero__strip { translate: 0 -230px; }
.hero__title, .scroll-hint { transition: opacity 0.9s ease; }
.scroll-hint { transition-delay: 1.2s; } /* once the strip is out */
/* Printed: the motor pulls it out at a steady pace and it slows as it comes free. */
.hero__strip { transition: translate 1.3s cubic-bezier(0.35, 0.05, 0.25, 1); }
.hero__title { display: flex; flex-direction: column; align-items: center; gap: 20px; white-space: nowrap; }
.hero__strip { position: absolute; left: 164px; top: 317px; width: 64px; rotate: -5deg; }
/* The strip: a card of booth paper with the couple's three photos (js/config.js) down it, and a
   blank foot where the date is written. Photos turn black and white, as a booth prints them. */
.hero__photos {
  position: relative;
  width: 64px;
  height: 193px;
  display: flex;
  flex-direction: column;
  gap: 1.6px;
  padding: 3.5px 4.3px 27px 5.4px; /* the foot: room for the date written under the photos */
  background: linear-gradient(#fbf3eb, #f3ebe2) 2.3px 1.5px / 59.4px 190.7px no-repeat;
}
.hero__photos img { flex: 1 1 0; min-height: 0; width: 100%; object-fit: cover; filter: grayscale(1) sepia(0.12) contrast(1.04); }
.hero__photos.is-colour img { filter: none; }
.hero__date {
  /* Written on the strip's blank foot, centred on the paper (2.3–61.7px across, 166–192px down). */
  position: absolute;
  left: 32px;
  top: 172px;
  translate: -50% 0;
  font: 400 11.5px/14px var(--font-hand);
  color: var(--wine);
  white-space: nowrap;
}
.scroll-hint { display: flex; flex-direction: column; align-items: center; gap: 4px; font: 400 16px/20px var(--font-display); letter-spacing: 0.01em; }

/* ---------- 2. Dear guest ---------- */
.letter__bg, .letter__frame { position: absolute; left: 50%; top: 50%; width: max(100%, 393px); height: max(100%, 949px); object-fit: cover; translate: -50% -50%; z-index: -1; }
.letter__frame { overflow: visible; }
.letter__card { fill: #fbf9f5; stroke: #7a4650; stroke-width: 6.5; stroke-linejoin: round; }
.letter__rule { fill: none; stroke: #d6cbc6; stroke-width: 3; stroke-linejoin: round; }
/* The words sit in the middle of the scalloped frame, wherever the cover-fitted picture puts it:
   the frame's centre is 46.6% down the picture, which is drawn max(screen height, 949px,
   width × 1949/807) tall and centred on the screen. */
.letter { container-type: size; }
.letter__text {
  position: absolute;
  left: 50%;
  top: calc(50% - 0.034 * max(100cqh, 949px, 100cqw * 2.4151));
  width: 229px;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  gap: 20px;
  text-align: center;
}

/* ---------- 3. Save the date: the slot machine ---------- */
/* One picture of the machine, its lever lifted off (assets/img/slot-body, lever-shaft, lever-knob,
   all cut from the same artwork) and drawn back on top so it can be pulled. */
.machine { height: 473px; cursor: pointer; }
.machine__art { position: absolute; inset: 0; width: 100%; height: 100%; }
.machine.is-pulled { cursor: default; }
/* The lever's pieces in the artwork's own 1024 × 1536 pixels, as percentages of the machine. The
   hub it turns on is at 909, 805; main.js sets --c (the cosine of the swing: 1 up, 0 pointing at
   the guest, negative below) and --k (how much nearer the knob has come). */
.lever { position: absolute; inset: 0; pointer-events: none; --c: 1; --k: 1; }
.lever__shaft {
  position: absolute;
  left: 86.914%;
  top: 34.180%;
  width: 3.906%;
  height: 15.104%;
  transform-origin: 50% 120.69%; /* the hub */
  scale: 1 var(--c);
}
.lever__knob {
  position: absolute;
  left: 84.180%;
  top: 29.297%;
  width: 9.766%;
  height: 6.380%;
  /* The knob's centre sits 306 artwork pixels above the hub, and is carried down by (1 − c) of that. */
  translate: 0 calc((1 - var(--c)) * 306 / 1536 * 473px);
  scale: var(--k);
}
/* Each reel's window, in the 315 × 473 art: 135.4px down, 75.4px tall. */
.reel {
  position: absolute;
  left: var(--rx);
  top: 135.4px;
  width: var(--rw);
  height: 75.4px;
  overflow: hidden;
  border-radius: 3px;
  visibility: hidden;
}
.machine.is-rolling .reel { visibility: visible; } /* from the moment the lever catches */
/* A column of reel faces: the winning face on top, then idle faces for the spin. */
.reel__strip { position: absolute; left: 0; top: 0; width: 100%; }
.reel__strip i {
  display: block;
  height: 75.4px;
  background: url("../assets/img/slot-machine.webp") calc(-1 * var(--rx)) -135.4px / 315px 473px no-repeat;
}
/* The winning face: a blank face of the artwork with the date printed on it (from js/config.js). */
.reel__strip i:first-child { display: grid; place-items: center; background-image: url("../assets/img/slot-machine-blank.webp"); }
.reel__strip b { font: 700 21px/1 var(--font-ui); font-variation-settings: "opsz" 72; letter-spacing: -0.02em; color: #683b3a; translate: 0 1px; }
.reel__strip b.is-word { font-size: 16px; letter-spacing: -0.01em; } /* the month: three letters in a two-digit face */
.reel.is-spinning .reel__strip { filter: blur(1.4px); animation: spin 0.32s linear infinite; }
@keyframes spin { from { translate: 0 calc(-6 * 75.4px); } to { translate: 0 calc(-1 * 75.4px); } }
/* A soft ring round the lever's knob while the machine waits to be played. */
.machine__knob {
  position: absolute;
  left: 280px;
  top: 153px;
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
}
.machine.is-waiting .machine__knob { animation: knob 2.4s ease-out infinite; }
@keyframes knob {
  0% { opacity: 0.9; box-shadow: 0 0 0 0 rgba(116, 72, 82, 0.45); }
  70%, 100% { opacity: 0; box-shadow: 0 0 0 16px rgba(116, 72, 82, 0); }
}
/* Until the lever has been pulled the page simply ends at the slot machine: the screens below are
   out of the document, so a phone stops there by itself (nothing pulls the scroll back). */
.is-held #date ~ * { display: none; }
/* The Calendar button only comes up once the reels have landed on the date. */
.is-held .date .btn { opacity: 0; translate: 0 10px; pointer-events: none; }
.date .btn { transition: opacity 0.7s ease 0.6s, translate 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) 0.6s, filter 0.2s ease; }

/* The win lights, laid over the artwork with screen blending so they brighten what is there. */
.lights { position: absolute; inset: 0; pointer-events: none; mix-blend-mode: screen; }
.glow {
  position: absolute;
  left: var(--lx);
  top: var(--ly);
  translate: -50% -50%;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.glow.is-on { opacity: 1; }
.glow--heart {
  width: 5%;
  height: 3.2%;
  background: radial-gradient(closest-side, #ffd2dc, #ff7a98 55%, rgba(255, 90, 130, 0.5));
  -webkit-mask: url("../assets/icons/cal-heart.svg") center / contain no-repeat;
  mask: url("../assets/icons/cal-heart.svg") center / contain no-repeat;
  filter: drop-shadow(0 0 3px #ff8fab) drop-shadow(0 0 6px rgba(255, 120, 150, 0.8));
}
.glow--pearl { width: 9%; height: 6%; border-radius: 50%; background: radial-gradient(closest-side, rgba(255, 236, 228, 0.95), rgba(255, 190, 200, 0.45) 55%, transparent); }
/* The plaque glows as a warm halo round its lettering, faint where the letters are, so they stay
   easy to read. */
.glow--plaque {
  width: 62%;
  height: 17%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 210, 150, 0.08) 30%, rgba(255, 200, 140, 0.28) 72%, transparent);
  transition-duration: 0.6s;
}

/* Heart tokens thrown out of the machine on the win (main.js sets their flight). */
.token { position: absolute; left: 0; top: 0; pointer-events: none; will-change: transform, opacity; }

/* ---------- The booth's flash ---------- */
/* A full white flash over the whole screen, except that it fades out over the bottom 140px: the
   address bar floats there, and whatever is drawn beneath it lights the bar up a moment late (a
   white "echo" after each flash). Under the bar the fade is out of sight anyway. */
.flash {
  position: fixed;
  inset: 0;
  z-index: 80;
  background: linear-gradient(#fff, #fff);
  -webkit-mask: linear-gradient(to bottom, #000 calc(100% - 140px), transparent calc(100% - 30px));
  mask: linear-gradient(to bottom, #000 calc(100% - 140px), transparent calc(100% - 30px));
  opacity: 0;
  pointer-events: none;
}
.flash:not(.is-on) { display: none; }

/* ---------- 4. Schedule ---------- */
/* Pinned: the schedule fills the window and holds there while the track scrolls past (2600px of
   scrolling: the thread with a pause at each moment, then a rest); its 967px board is scaled down to fit a shorter window (--pk). */
.program-track { position: relative; height: calc(var(--pin-h) + 2600px); }
:root { --pin-h: max(var(--hero-h, 100svh), var(--hero-grow, 0px)); }
.program { --stage-h: 967px; position: sticky; top: 0; height: var(--pin-h); min-height: 0; background: transparent; }
.program-track { background: var(--white); }
.program > .stage { scale: var(--pk, 1); }
/* The thread: a faint track, and over it the wine line drawn down by the scroll (--p, from main.js). */
.program__line { position: absolute; left: 196px; top: 194px; width: 1px; height: 694px; background: var(--rose); }
.is-animated .program__line { opacity: 0.35; }
.program__thread {
  position: absolute; left: 196px; top: 194px; width: 1px; height: 694px;
  background: var(--wine);
  transform-origin: top;
  scale: 1 var(--p, 1);
}
.moment { display: flex; flex-direction: column; align-items: center; gap: 8px; white-space: nowrap; }
.dot { position: absolute; left: 192px; top: calc(var(--y) + 1.66px); width: 8px; height: 8px; background: var(--wine); rotate: 45deg; transition: scale 0.4s cubic-bezier(0.3, 1.6, 0.5, 1), background-color 0.3s; }

/* Reached by the thread: the diamond pops, the words rise in, then the picture comes and plays. */
.is-animated .moment-row .dot { background: var(--rose); scale: 0.7; }
.is-animated .moment-row.is-reached .dot { background: var(--wine); scale: 1; }
/* Coming in, the words lead and the picture follows; going back up it is the reverse: the picture
   leaves first, then the words. (A transition takes the timing of the state it is going to.) */
.is-animated .moment-row .moment { opacity: 0; translate: 0 12px; transition: opacity 0.5s ease 0.22s, translate 0.5s ease 0.22s; }
.is-animated .moment-row.is-reached .moment { opacity: 1; translate: none; transition: opacity 0.7s ease, translate 0.7s cubic-bezier(0.22, 0.61, 0.36, 1); }
.is-animated .moment-row .pic { opacity: 0; scale: 0.82; transition: opacity 0.4s ease, scale 0.45s ease; }
.is-animated .moment-row.is-reached .pic { opacity: 1; scale: 1; transition: opacity 0.6s ease 0.28s, scale 0.75s cubic-bezier(0.3, 1.35, 0.5, 1) 0.28s; }

/* ---------- The schedule's pictures, each with its own small scene ---------- */
/* Overlays are placed in the picture's own pixels (--x, --y, size --s; the picture is --iw wide and
   527 tall), so they stay on the right detail at any size. */
.pic { height: 140px; }
.pic__art, .pic__art > img { position: absolute; inset: 0; width: 100%; height: 100%; }
.fx {
  position: absolute;
  left: calc(var(--x) / var(--iw) * 100%);
  top: calc(var(--y) / 527 * 100%);
  width: calc(var(--s) / var(--iw) * 100%);
  aspect-ratio: 1;
  translate: -50% -50%;
  pointer-events: none;
}

/* 3 PM — the door plate swings on its bow as the bell is rung twice; its little heart beats. */
.pic--welcome .pic__art { transform-origin: 48% 21%; }
.pic--welcome.is-playing .pic__art { animation: swing 1.8s cubic-bezier(0.35, 0, 0.4, 1) 0.15s; }
@keyframes swing {
  0% { rotate: 0deg; } 18% { rotate: 6deg; } 40% { rotate: -4deg; } 60% { rotate: 2.2deg; } 80% { rotate: -0.8deg; } 100% { rotate: 0deg; }
}
.fx--press { border-radius: 50%; background: radial-gradient(closest-side, rgba(40, 10, 20, 0.45), transparent); opacity: 0; }
.pic--welcome.is-playing .fx--press { animation: press 0.9s ease 0.1s; }
@keyframes press { 0%, 100% { opacity: 0; } 12%, 22% { opacity: 1; } 34% { opacity: 0; } 50%, 60% { opacity: 1; } 72% { opacity: 0; } }
.fx--pulse { border-radius: 50%; background: radial-gradient(closest-side, rgba(255, 150, 175, 0.9), transparent); mix-blend-mode: screen; opacity: 0; }
.pic--welcome.is-playing .fx--pulse { animation: beat 1.4s ease 0.9s; }
@keyframes beat { 0%, 100% { opacity: 0; scale: 0.8; } 15% { opacity: 1; scale: 1.15; } 30% { opacity: 0.3; scale: 0.95; } 45% { opacity: 1; scale: 1.1; } }

/* 4 PM — the camera takes the picture: it kicks, the flash goes, and the instant photo develops. */
.pic--camera.is-playing .pic__art { animation: kick 0.45s cubic-bezier(0.3, 0, 0.3, 1) 0.2s; transform-origin: 55% 60%; }
@keyframes kick { 0%, 100% { scale: 1; rotate: 0deg; } 25% { scale: 0.96; rotate: -1.5deg; } }
.fx--flash { border-radius: 50%; background: radial-gradient(closest-side, #fff 0%, rgba(255, 255, 255, 0.85) 25%, rgba(255, 245, 235, 0) 100%); opacity: 0; scale: 0.4; mix-blend-mode: screen; }
.pic--camera.is-playing .fx--flash { animation: flash 0.6s ease-out 0.25s; }
@keyframes flash { 0% { opacity: 0; scale: 0.4; } 15% { opacity: 1; scale: 1.2; } 100% { opacity: 0; scale: 1.6; } }
/* The photo's face, still blank until the flash: it develops from cream to the picture. */
.fx--develop {
  left: 0; top: 0; width: 100%; height: 100%; aspect-ratio: auto; translate: none;
  background: #efe6dc;
  clip-path: polygon(45.4% 64.6%, 81.3% 63.6%, 87.6% 79.7%, 51.3% 84.6%);
}
.pic--camera.is-playing .fx--develop { animation: develop 2.2s ease-in 0.55s forwards; }
@keyframes develop { 0% { opacity: 1; filter: none; } 35% { opacity: 0.75; } 100% { opacity: 0; } }

/* 5 PM — the two glasses clink, the champagne fizzes, and the hearts float up. */
.pic--glasses.is-playing .pic__art { animation: clink 0.5s cubic-bezier(0.3, 0, 0.3, 1) 0.15s; transform-origin: 50% 85%; }
@keyframes clink { 0%, 100% { scale: 1; } 30% { scale: 1.035 0.97; } 55% { scale: 0.99 1.01; } }
.pic__hearts { opacity: 0; translate: 0 6%; scale: 0.6; transform-origin: 54% 20%; }
.pic--glasses.is-playing .pic__hearts { animation: hearts 1.1s cubic-bezier(0.3, 1.5, 0.5, 1) 0.45s forwards; }
@keyframes hearts { 0% { opacity: 0; translate: 0 6%; scale: 0.6; } 60% { opacity: 1; } 100% { opacity: 1; translate: none; scale: 1; } }
.fx--bubbles { aspect-ratio: 2.4; border-radius: 50%; overflow: hidden; }
.fx--bubbles i {
  position: absolute; bottom: -10%; width: 5%; aspect-ratio: 1; border-radius: 50%;
  background: rgba(255, 252, 240, 0.9); box-shadow: 0 0 2px rgba(255, 255, 255, 0.9);
  opacity: 0;
}
.fx--bubbles i:nth-child(1) { left: 22%; } .fx--bubbles i:nth-child(2) { left: 44%; } .fx--bubbles i:nth-child(3) { left: 61%; } .fx--bubbles i:nth-child(4) { left: 78%; }
.pic--glasses.is-playing .fx--bubbles i { animation: fizz 1.6s ease-in infinite; }
.fx--bubbles i:nth-child(2) { animation-delay: 0.4s !important; } .fx--bubbles i:nth-child(3) { animation-delay: 0.9s !important; } .fx--bubbles i:nth-child(4) { animation-delay: 1.25s !important; }
@keyframes fizz { 0% { opacity: 0; translate: 0 0; } 15% { opacity: 1; } 85% { opacity: 0.9; } 100% { opacity: 0; translate: 0 -260%; } }

/* 6 PM — the cake is set down with a little give, a gleam runs across it and the top catches light. */
.pic--cake.is-playing .pic__art { animation: set-down 0.7s cubic-bezier(0.3, 0, 0.3, 1); transform-origin: 50% 95%; }
@keyframes set-down { 0% { scale: 1 1; } 30% { scale: 1.025 0.96; } 60% { scale: 0.99 1.015; } 100% { scale: 1 1; } }
.fx--shine {
  left: 0; top: 0; width: 100%; height: 100%; aspect-ratio: auto; translate: none;
  background: linear-gradient(105deg, transparent 35%, rgba(255, 250, 240, 0.85) 50%, transparent 65%) -150% 0 / 250% 100% no-repeat;
  -webkit-mask: url("../assets/img/icon-cake.webp") center / 100% 100% no-repeat;
  mask: url("../assets/img/icon-cake.webp") center / 100% 100% no-repeat;
  mix-blend-mode: screen;
}
.pic--cake.is-playing .fx--shine { animation: shine 1.3s ease-in-out 0.45s; }
@keyframes shine { from { background-position: -150% 0; } to { background-position: 250% 0; } }

/* A four-pointed glint: the cake's top, and the disco ball's stars. */
.fx--star {
  background: radial-gradient(closest-side, #fff, rgba(255, 240, 230, 0) 70%),
              linear-gradient(transparent 46%, #fff 50%, transparent 54%),
              linear-gradient(90deg, transparent 46%, #fff 50%, transparent 54%);
  -webkit-mask: radial-gradient(closest-side, #000 60%, transparent);
  mask: radial-gradient(closest-side, #000 60%, transparent);
  mix-blend-mode: screen;
  opacity: 0;
  scale: 0.3;
}
.pic--cake.is-playing .fx--star { animation: glint 0.9s ease 1.3s; }
.pic--disco.is-playing .fx--star { animation: glint 1.1s ease calc(0.6s + var(--delay, 0s)) 2; }
@keyframes glint { 0%, 100% { opacity: 0; scale: 0.3; rotate: 0deg; } 40% { opacity: 1; scale: 1; rotate: 45deg; } }

/* 9 PM — the disco ball sways on its hook and turns: bands of light run across its mirrors. */
.pic--disco .pic__art { transform-origin: 46.6% 15%; }
.pic--disco.is-playing .pic__art { animation: sway 2.6s cubic-bezier(0.35, 0, 0.4, 1) 0.1s; }
@keyframes sway { 0% { rotate: 0deg; } 20% { rotate: -5deg; } 45% { rotate: 3.5deg; } 70% { rotate: -1.5deg; } 100% { rotate: 0deg; } }
.fx--mirror { border-radius: 50%; overflow: hidden; mix-blend-mode: screen; }
.fx--mirror i {
  position: absolute; inset: 0 -100%;
  background: repeating-linear-gradient(90deg, transparent 0 9%, rgba(255, 245, 240, 0.55) 11%, transparent 14% 22%);
  opacity: 0;
}
.pic--disco.is-playing .fx--mirror i { animation: mirror 2.4s cubic-bezier(0.45, 0, 0.3, 1) 0.2s; }
@keyframes mirror { 0% { opacity: 0; translate: -25% 0; } 20%, 75% { opacity: 1; } 100% { opacity: 0; translate: 25% 0; } }

@media (prefers-reduced-motion: reduce) {
  .fx--develop { display: none; }
}

/* ---------- 5. Venue ---------- */
/* The drive: pinned for --drive-scroll of scrolling while main.js scrubs the clip. Towards the end
   the screen darkens to wine round an arched window (--dim) and the location rises in (--ui). */
.drive-track { position: relative; height: calc(var(--pin-h) + 2400px); }
.drive { position: sticky; top: 0; height: var(--pin-h); min-height: 0; color: var(--blush); background: var(--wine-deep); }
.drive > .stage { scale: var(--dk, 1); }
.drive__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -1; }
/* As the window closes in, the villa at the end of the avenue is brought down into it: the picture
   pushes in from its top edge, so the frame's edge never comes into view. */
.drive__video { transform-origin: 50% 0; scale: calc(1 + var(--dim, 0) / 0.9 * var(--push, 0.55)); } /* --push: js/config.js */
.drive__ui > :not(.drive__window) { opacity: var(--ui, 0); translate: 0 calc((1 - var(--ui, 0)) * 14px); }
.drive__ui .btn { pointer-events: none; }
.drive.is-located .drive__ui .btn { pointer-events: auto; }
/* The window: everything round it is shaded with an enormous shadow, so the clip stays bright inside. */
.drive__window {
  position: absolute;
  left: 34px;
  top: 372px;
  width: 325px;
  height: 340px;
  border-radius: 162px 162px 14px 14px;
  box-shadow: 0 0 0 200vmax rgba(116, 72, 83, var(--dim, 0)), inset 0 0 0 1.5px rgba(246, 229, 228, calc(var(--dim, 0) * 0.7));
  pointer-events: none;
}
.venue__text { display: flex; flex-direction: column; align-items: center; gap: 12px; }

/* ---------- 6. Dress code ---------- */
/* The dress code's two colours as plain swatches — solid colour is the standard (a client may still
   ask for fabric pictures). --suit and --dress are set from js/config.js. */
.swatches { display: flex; justify-content: center; gap: 8px; }
.swatch { width: 52px; height: 52px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(74, 38, 48, 0.18); }
.swatch--suit { background: var(--suit); }
.swatch--dress { background: var(--dress); }
.dress__text { display: flex; flex-direction: column; align-items: center; gap: 12px; }
/* The couple spans the screen, so the section's board grows to hold them (art top + height + margin). */
.dress { --stage-h: 1045px; }
/* Pinned for the length of the track beyond its own height, while the scroll draws and colours the
   sketch. Where it pins (main.js, --dress-top): the text and the couple centred in the window when
   they fit (a computer), else by their foot (a phone). main.js also sets --dress-h, its height. */
.dress-track { position: relative; height: calc(var(--dress-h, 1045px) + var(--dress-hold, 1100px)); background: var(--blush); }
.dress { position: sticky; top: var(--dress-top, min(0px, calc(var(--pin-h) - var(--dress-h, 1045px)))); } /* --dress-top: main.js */
.dress__art { height: var(--h); }
.dress__art svg { display: block; width: 100%; height: 100%; overflow: visible; }
/* Each stroke draws itself by its own --d (0 drawn … 1 hidden), set by main.js from the scroll. */
.dress__lines path { stroke-dasharray: 1 1; stroke-dashoffset: var(--d, 0); }
/* Marker strokes: see-through, so where two cross the colour deepens, as real marker does. */
.dress__marks path { stroke-dasharray: 1 1; stroke-dashoffset: var(--d, 1); mix-blend-mode: multiply; }
.mark--dress { stroke: var(--dress); stroke-opacity: 0.75; }
.mark--suit { stroke: var(--suit); stroke-opacity: 0.55; }
.mark--line { stroke: var(--line); stroke-opacity: 0.6; }
.mark--shirt { stroke: #fffaf7; stroke-opacity: 0.9; mix-blend-mode: normal; }
.dress__shadows { opacity: var(--shade, 0); }
/* The groom's silhouette in the page's colour: her train passes behind him, not through him. */
.dress__occluder { fill: var(--blush); }

/* ---------- 7. RSVP ---------- */
.rsvp.is-sent { background: var(--blush); color: var(--wine); }
.reply {
  display: flex;
  flex-direction: column;
  gap: 20px;
  height: 540px;
  justify-content: center;
  padding: 24px 20px;
  border-radius: 16px;
  background: var(--white);
  color: var(--wine);
  text-align: left;
}
.rsvp__frame { height: 640px; object-fit: fill; pointer-events: none; }
.stage .reply { text-align: left; }
.field { display: flex; flex-direction: column; gap: 12px; }
.field fieldset, fieldset.field { gap: 10px; }
.field__label { font: 400 18px/22px var(--font-display); letter-spacing: 0.01em; padding: 0; margin-bottom: 2px; }
.field__input, .choice {
  width: 100%;
  height: 40px;
  padding: 0 14px;
  border: 0.7px solid rgba(116, 72, 82, 0.55);
  border-radius: 20px;
  background: none;
  color: var(--wine);
  font: 400 14px/18px var(--font-sans);
  letter-spacing: -0.01em;
  outline: none;
}
.field__input::placeholder { color: rgba(116, 72, 82, 0.7); }
.field__input:focus { border-color: var(--wine); }
.field__input--area { height: 92px; padding-top: 12px; border-radius: 12px; resize: none; }
.choice { display: flex; align-items: center; gap: 12px; cursor: pointer; }
.choice input {
  appearance: none;
  flex: none;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1px solid var(--wine);
  border-radius: 50%;
  display: grid;
  place-items: center;
}
.choice input::after { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--wine); scale: 0; transition: scale 0.2s ease; }
.choice input:checked::after { scale: 1; }
.field.is-invalid .field__input, .field.is-invalid .choice { border-color: #b3261e; }

.thanks__body { display: flex; flex-direction: column; align-items: center; gap: 28px; }

/* ---------- 8. With love ---------- */
/* The last screen, seen at the very end of the page (its bottom on the window's bottom): as tall
   as the window can grow plus room above for Safari's see-through strip. Its board and its picture
   are scaled together (--cu, main.js) from the footer up, so the words stay where the mock has them
   on the picture: enough to fit the window, and more if the picture needs it to reach both sides
   (some of its top, garland only, then runs above the window). */
.screen.closing { min-height: 0; height: calc(var(--date-top) + var(--date-held)); background: #f7ecea; }
.closing > .stage { top: auto; bottom: 0; height: var(--stage-h); transform-origin: 50% 100%; scale: var(--cu, 1); }
.closing__art {
  position: absolute; left: 50%; bottom: 62px; translate: -50% 0; z-index: -1;
  width: calc(430px * var(--cu, 1)); height: calc(790px * var(--cu, 1));
}
.closing__art img { position: absolute; inset: 0; width: 100%; height: 100%; }
.closing__text { display: flex; flex-direction: column; align-items: center; gap: 16px; }
.closing__text .t-title { white-space: nowrap; margin-bottom: -12px; }
.footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 62px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  background: var(--white);
  color: var(--wine);
}
.credit { font: 400 16px/1 var(--font-ui); }
.credit a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.to-top { font: 400 22px/1 var(--font-sans); width: 32px; text-align: right; }

/* ---------- Sheets (calendar / map) ---------- */
.sheet { position: fixed; inset: 0; z-index: 40; }
.sheet__backdrop { position: absolute; inset: 0; background: rgba(60, 30, 38, 0.45); opacity: 0; transition: opacity 0.35s ease; }
.sheet__panel {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: min(100%, var(--screen-w));
  height: calc(490px + env(safe-area-inset-bottom));
  background: var(--white);
  border-radius: 16px 16px 0 0;
  translate: -50% 100%;
  transition: translate 0.45s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.sheet.is-open .sheet__backdrop { opacity: 1; }
.sheet.is-open .sheet__panel { translate: -50% 0; }
.sheet__close { position: absolute; left: 337px; top: 12px; width: 44px; height: 44px; display: grid; place-items: center; }

.calendar {
  position: absolute;
  left: 42px;
  top: 132px;
  width: 309px;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  grid-auto-rows: 29px;
  row-gap: 17px;
  text-align: center;
  font: 400 18px/22px var(--font-sans);
  letter-spacing: -0.01em;
}
.calendar > * { padding-top: 3px; }
.calendar__dow { font: 400 20px/20px var(--font-display); padding: 0; }
.calendar__day { position: relative; z-index: 0; color: var(--blush); font-weight: 700; }
.calendar__day::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  top: calc(50% + 1.5px);
  width: 31.67px;
  height: 28.5px;
  translate: -50% -50%;
  background: url("../assets/icons/cal-heart.svg") center / contain no-repeat;
}

.map { position: absolute; left: 44px; top: 112px; width: 305px; height: 267px; overflow: hidden; border: 0.8px solid var(--wine); border-radius: 8px; background: #f7f2e8; }
.map > img { position: absolute; left: -0.8px; top: -0.8px; width: 305px; height: 267px; }
.map__marker {
  position: absolute;
  left: 137px;
  top: 122px;
  width: 101px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  font: italic 400 16px/20px var(--font-display);
  letter-spacing: -0.01em;
  white-space: nowrap;
}
.map__marker img { width: 19.5px; height: 21.5px; margin: 1.25px 0; }
.map__caption { display: flex; flex-direction: column; align-items: center; gap: 8px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition-duration: 0.01s !important; transition-delay: 0s !important; }
  html { scroll-behavior: auto; }
}


/*
 * No scroll bar, on any computer. The invitation is read as a phone reads
 * it, and a phone draws no bar - but Windows draws a grey gutter with a white
 * track down the side of the page, and in the constructor's phone that
 * gutter sat inside the screen. Scrolling itself is untouched.
 */
html,
body,
* {
  scrollbar-width: none;
}

html::-webkit-scrollbar,
body::-webkit-scrollbar,
*::-webkit-scrollbar {
  display: none;
}
