/* The opening overlay - AND the persistent chrome that surrounds it.
 *
 * Read that twice before deleting anything. The name says "intro" but the
 * bottom third of this file styles three controls that outlive the overlay
 * entirely: the dock, the save-the-date panel and the music toggle. They are
 * on screen for the whole life of the page, including for a guest who skipped
 * the opening or came back in the same session. This file is loaded
 * unconditionally from <head> and must stay that way - making it conditional
 * on the 3D gate would strip the styling off all three and leave unstyled
 * buttons floating over the hero.
 *
 * Design rule that the previous version broke repeatedly: NOTHING here may
 * hide an ancestor of the WebGL canvas. The old CSS hid `.envelope-stage` to
 * put the drawn envelope away, and the canvas lived inside it, so the
 * sequence erased itself. There is now no drawn envelope to hide, and the
 * mount has no wrapper that anything toggles.
 *
 * The overlay starts hidden in the markup and is shown by JS only once there
 * is something to see. If the script never runs, the overlay never appears
 * and the page is simply itself - which is the fallback, rather than a second
 * animation pretending to be one.
 */

.intro {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ivory-deep, #F3EADA);
  opacity: 1;
  transition: opacity 0.6s ease;
}

/* The overlay's share of the decoy dark theme. The mechanism, and why the site
   claims a dark mode it does not have, is documented at section 1b of the
   <style> block in index.html.

   This is the surface the original complaint was actually about: "behind the
   envelope is black" is .intro, inverted. It carries its own declarations here
   rather than in the page so the overlay's chrome stays in one file.

   Same rule as the page's block, and it is not optional: ONLY RESTATE WHAT
   LIGHT ALREADY DECLARES, NEVER INVENT A BACKGROUND. Both values below are
   checked against their computed light values. `.save-panel` was in here
   briefly and is gone again - it is transparent in light, and giving it a
   ground in dark would have put a cream slab behind the email form. */
@media (prefers-color-scheme: dark) {
  .intro { background: var(--ivory-deep, #F3EADA); }
  .intro-dock { background: var(--ivory, #FAF6ED); }
}

.intro[hidden] { display: none !important; }

.intro.is-closing {
  opacity: 0;
  pointer-events: none;
}

/* ---------- atmosphere ----------
 * An off-centre vignette with a warm pool of light where the envelope sits,
 * so the opening reads as an envelope lying under a lamp rather than a white
 * shape on a flat colour fill. It is the cheapest possible way to give the
 * scene depth: one element, one gradient, animating nothing but opacity.
 *
 * Three things about it that are load-bearing:
 *
 * `pointer-events: none` — the guest DRAGS the card and SCRATCHES the foil
 * through this element. Without it the opening becomes unusable.
 *
 * No z-index, and first in the markup, so it paints under the WebGL canvas
 * (which has z-index auto too and comes later) and over the overlay's own
 * background. Giving it any positive z-index would put it on top of the
 * envelope and turn atmosphere into a filter over the artwork.
 *
 * `inset: 0`, not the oversized negative inset the technique is usually
 * written with. A radial-gradient already fills everything beyond its ellipse
 * with the final stop, so there is no hard edge to hide, and a child hanging
 * 10% outside a position:fixed overlay is a scrollbar waiting to happen.
 */
.intro-atmosphere {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Warm lift in the middle, ground colour through the mid-field, --ink at the
     rim. Never pure black: on this palette a grey vignette reads as dirt.
     The centre is deliberately ABOVE and LEFT of dead centre — a light source
     square behind the subject is the one arrangement that looks like no
     lighting at all. */
  background: radial-gradient(
    ellipse 62% 56% at 46% 40%,
    rgba(255, 251, 240, 0.42) 0%,
    rgba(255, 251, 240, 0.14) 34%,
    rgba(243, 234, 218, 0) 64%,
    rgba(92, 68, 38, 0.17) 100%
  );
  animation: intro-atmosphere-breathe 11s ease-in-out infinite;
}

/* Slower than the sequence's own beats on purpose, so it never reads as a
   thing that is happening - just as a room that is not perfectly still. */
@keyframes intro-atmosphere-breathe {
  0%, 100% { opacity: 0.86; }
  50%      { opacity: 1; }
}

/* The canvas fills the overlay. Not a small card in the middle: the sequence
   lifts a card out, orbits a heart around it and spins it, and all of that
   needs room. */
.intro-stage {
  position: absolute;
  inset: 0;
}

.intro-stage canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Sits above the canvas so a drag reaches it rather than the page.
 *
 * TOP, not bottom. By the time either hint appears the camera has pushed in on
 * the card, so the lower half of the frame is the mouth of the envelope and a
 * line of type at `bottom: 16%` landed right across it — lettering sitting on
 * the paper, which just looked like a mistake. Above the card is the one part
 * of the composition that is reliably empty at this framing, on a phone and on
 * a desktop, so the hint reads as a caption rather than a smudge.
 *
 * The halo is the same ivory glow the hero type uses over the photograph: it
 * costs nothing when the background is already cream and saves the hint if the
 * framing ever changes enough to put the card behind it. */
.intro-hint {
  position: absolute;
  left: 50%;
  top: max(1.6rem, 7%);
  transform: translateX(-50%);
  z-index: 3;
  margin: 0;
  padding: 0 1rem;
  text-align: center;
  text-shadow: 0 1px 14px rgba(250, 246, 237, 0.95), 0 0 34px rgba(250, 246, 237, 0.8);
  font-size: var(--text-xs, 0.85rem);
  letter-spacing: 0.16em;
  text-transform: lowercase;
  color: var(--blue-deep, #4A6A80);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s ease;
}

.intro-hint.is-visible { opacity: 0.9; }

/* While the foil is up the instruction stays at the TOP, where every other
   hint in the sequence sits, and only changes its voice: uppercase, tracked
   out, and gold rather than blue, so it reads as the thing to do rather than
   a caption.
   It was briefly moved down into the skip button's place. That put two
   competing lines in the same corner and demoted the overlay's only
   pointer-reachable exit to make room; the top of the frame is empty at this
   framing, so nothing has to give way. */
.intro.is-scratching .intro-hint {
  font-size: var(--text-xs, 0.85rem);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-deep, #96751C);
}

/* The way out. Always present, always reachable, from the first frame -
   including for anyone who cannot drag, tap or scratch anything. */
.intro-skip {
  position: absolute;
  left: 50%;
  bottom: max(1.25rem, env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 4;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 1.25rem;
  background: none;
  border: none;
  font-family: inherit;
  font-size: var(--text-xs, 0.85rem);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--blue-deep, #4A6A80);
  /* Full strength, not 0.75.
     This is the overlay's only guaranteed exit — the one control that must
     work for someone who cannot drag the card or scratch the foil. At 0.75 the
     blue composited toward the background and measured about 2.3:1 against the
     ground, and the vignette behind it made that worse, not better. At full
     opacity it measures 3.87:1 over the vignette's rim. That clears AA for
     large text but not the 4.5 body threshold, so the halo below does the rest
     of the work; going darker than --blue-deep would clear it outright if this
     ever needs to be provably compliant. Restraint is not worth an exit nobody
     can see. */
  opacity: 1;
  text-shadow: 0 1px 12px rgba(250, 246, 237, 0.95), 0 0 30px rgba(250, 246, 237, 0.85);
  cursor: pointer;
  transition: color 0.2s ease, opacity 0.2s ease;
}

.intro-skip:hover { color: var(--gold-deep, #96751C); }
.intro-skip:focus-visible { opacity: 1; outline: 2px solid var(--blue-deep, #4A6A80); outline-offset: 3px; }

/* ---------- the scratch card ----------
   A DOM canvas centred over the stage. Deliberately NOT painted into the
   model's UVs: the text under the foil stays real markup a screen reader can
   read, and the proven scratch code is reused as-is. */
.intro-card {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(0.94);
  z-index: 2;
  /* Fixed cap in px. It used to be 24rem, which follows the root font size,
     so the card grew on every browser-zoom step. 320px stays put; the vw term
     only ever shrinks it on a narrow phone. */
  width: min(86vw, 320px);
  aspect-ratio: 4 / 3;
  border-radius: 10px;
  overflow: hidden;
  background: var(--ivory, #FAF6ED);
  border: 1px solid var(--gold, #C9A227);
  box-shadow: 0 22px 48px rgba(44, 42, 38, 0.3);
  opacity: 0;
  transition: opacity 0.45s ease, transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

.intro-card.is-in { opacity: 1; transform: translate(-50%, -50%) scale(1); }

.intro-card-under { position: absolute; inset: 0; }

.intro-card-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* No blur. It was softened so white type could sit on it; the type has its
     own opaque plate now, so the photograph can be what it is. */
  transform: scale(1.02);
}

/* White panel along the bottom, so the name never depends on what happens to
   be behind it in the photograph. */
.intro-card-plate {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0.7rem 1rem 0.9rem;
  background: rgba(255, 255, 255, 0.92);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  text-align: center;
}

.intro-card-eyebrow {
  margin: 0 0 0.15rem;
  font-size: var(--text-xs, 0.85rem);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--blue-deep, #4A6A80);
}

.intro-card-name {
  margin: 0;
  font-family: var(--font-display, Georgia, serif);
  font-size: var(--text-md, 1.35rem);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink, #2C2A26);
}

.intro-foil {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: grab;
  touch-action: none;          /* the drag belongs to the foil, not the page */
  transition: opacity 0.5s ease;
}

.intro-foil:active { cursor: grabbing; }
.intro-card.is-clear .intro-foil { opacity: 0; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .intro { transition: none; }
  .intro-card { transition: none; }
  /* The depth stays, the breathing stops. In practice the capability gate
     refuses the whole opening on reduced-motion so this is never reached —
     it is here so the rule survives if that gate is ever relaxed. */
  .intro-atmosphere { animation: none; opacity: 0.93; }
}

/* ==========================================================
   THE DOCK + SAVE-THE-DATE PANEL
   Re-homed here after css/intro-3d.css was deleted - these lost
   their styling with it, which is why the dock stopped looking
   like anything.
   ========================================================== */

/* position: fixed, so it is anchored to the VIEWPORT and stays in the
   bottom-right corner through any amount of scrolling. */
.intro-dock {
  /* Clips the sheen sweep (below) to the rounded corners. It lives HERE, in
     the element's one and only block — it was briefly added as a second
     `.intro-dock { }` further down, which is the exact defect CLAUDE.md names
     as the commonest in this stylesheet. */
  overflow: hidden;
  position: fixed;
  right: max(0.9rem, env(safe-area-inset-right, 0px));
  bottom: max(0.9rem, env(safe-area-inset-bottom, 0px));
  z-index: 900;
  width: 54px;
  height: 54px;
  padding: 5px;
  background: var(--ivory, #FAF6ED);
  border: 1px solid var(--gold, #C9A227);
  border-radius: 9px;
  box-shadow: 0 8px 20px rgba(44, 42, 38, 0.22);
  cursor: pointer;
  opacity: 0;
  transform: translateY(12px) scale(0.9);
  transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.intro-dock.visible { opacity: 1; transform: none; }
.intro-dock:hover { border-color: var(--gold-deep, #96751C); }
.intro-dock img { width: 100%; height: 100%; object-fit: contain; }

/* ---------- the dock's one-shot "look at me" ----------
   A single pass of light across the face rather than a shake. On a page whose
   whole vocabulary is foil and gilt edges, catching the light is the native
   gesture; a jiggle would read as a notification badge. The `overflow: hidden`
   this needs is set in `.intro-dock` above, not repeated here, and
   `pointer-events: none` keeps the sheen from swallowing the click it is
   asking for. */
.intro-dock::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    115deg,
    transparent 32%,
    rgba(255, 255, 255, 0.85) 47%,
    rgba(247, 233, 188, 0.65) 55%,
    transparent 70%
  );
  transform: translateX(-130%);
}

/* A 3-SECOND LOOP, not a one-shot.
   The whole cycle is 3s and the pass of light occupies only the first third of
   it — the rest of each keyframe is deliberately empty, which is what makes it
   a slow repeating glint rather than something strobing at the corner of the
   eye. Timing the pause into the keyframes rather than into JS means there is
   no interval to cancel: removing the class stops it. */
.intro-dock.is-nudging::after { animation: dock-sheen 3s ease-out infinite; }
.intro-dock.is-nudging { animation: dock-tilt 3s ease-out infinite; }

@keyframes dock-sheen {
  0%   { transform: translateX(-130%); opacity: 0; }
  6%   { opacity: 1; }
  28%  { opacity: 1; }
  34%  { transform: translateX(130%); opacity: 0; }
  100% { transform: translateX(130%); opacity: 0; }   /* the quiet two thirds */
}

/* Very small. The sheen is the signal; this only stops it looking like a
   reflection on a static object. */
@keyframes dock-tilt {
  0%       { transform: none; }
  8%       { transform: scale(1.06) rotate(-2deg); }
  17%      { transform: scale(1.02) rotate(1.5deg); }
  26%      { transform: scale(1.04) rotate(-0.8deg); }
  34%, 100% { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .intro-dock.is-nudging,
  .intro-dock.is-nudging::after { animation: none; }
}

/* The panel is gold leaf, not cream: it is the one place on the page that
   should feel like the foil the guest just scratched. Everything inside sits
   on that, so the type is dark and the field is a pale inset. */
.save-panel {
  position: fixed;
  right: max(0.9rem, env(safe-area-inset-right, 0px));
  bottom: calc(54px + max(0.9rem, env(safe-area-inset-bottom, 0px)) + 0.5rem);
  z-index: 901;
  width: min(92vw, 20rem);
  padding: 1.25rem 1.2rem 1.35rem;
  border-radius: 10px;
  border: 1px solid #7d621b;
  color: #2C2A26;
  /* Was the full scratch-foil gradient, which read as bright yellow at this
     size. Pulled toward warm beige: same metal, much lower saturation. */
  background:
    linear-gradient(135deg,
      #a08b5e 0%, #d8c9a6 18%, #efe6cf 34%, #c9b788 50%,
      #e8dcc0 68%, #bda87d 84%, #a08b5e 100%);
  box-shadow: 0 18px 44px rgba(44, 42, 38, 0.34), inset 0 1px 0 rgba(255,255,255,0.45);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}

.save-panel.open { opacity: 1; transform: none; }

.save-panel-close {
  position: absolute;
  top: 2px; right: 2px;
  width: 40px; height: 40px;
  background: none; border: none;
  font-size: 1.4rem; line-height: 1;
  color: #4a3a0e;
  cursor: pointer;
}

.save-panel-title {
  font-family: var(--font-display, Georgia, serif);
  font-size: var(--text-md, 1.3rem);
  margin: 0 0 0.3rem;
  padding-right: 1.8rem;
  color: #3a2d08;
}

.save-panel-note {
  font-size: var(--text-sm, 0.95rem);
  margin: 0 0 0.85rem;
  color: #4a3a0e;
}

.save-label {
  display: block;
  font-size: var(--text-xs, 0.8rem);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #4a3a0e;
  margin-bottom: 0.3rem;
}

.save-input {
  width: 100%;
  min-height: 44px;
  padding: 0.55rem 0.7rem;
  font-family: inherit;
  font-size: var(--text-base, 1rem);
  color: var(--ink, #2C2A26);
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid #8a6e20;
  border-radius: 4px;
}

.save-input:focus-visible { outline: 2px solid #4a3a0e; outline-offset: 2px; }
.save-input[aria-invalid='true'] { border-color: #8c2f26; }

.save-error {
  font-size: var(--text-xs, 0.8rem);
  color: #6d1f18;
  margin: 0.45rem 0 0;
  font-weight: 600;
}

.save-submit {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  width: 100%; min-height: 44px;
  margin-top: 0.8rem;
  padding: 0.65rem 1rem;
  font-family: inherit;
  font-size: var(--text-xs, 0.8rem);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #F7E9BC;
  background: #3a2d08;
  border: none; border-radius: 999px;
  cursor: pointer;
  transition: background 0.2s ease, opacity 0.2s ease;
}

.save-submit:hover:not(:disabled) { background: #4a3a0e; }
.save-submit:disabled { opacity: 0.65; cursor: progress; }

/* ---------- the confirmed state ----------
   Once the address is in, the panel stops being a form and becomes a receipt.
   The title and the note are hidden rather than left in place: they are the
   ask, and an ask sitting above its own confirmation reads as a failure.
   The panel shrinks to what is left — narrower, shallower, and the close
   button pulled in so it does not float in a corner of empty gold. */
.save-panel.is-done {
  width: min(88vw, 15rem);
  padding: 1rem 1.1rem 1.1rem;
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
              width 0.3s ease, padding 0.3s ease;
}

.save-panel.is-done .save-panel-title,
.save-panel.is-done .save-panel-note { display: none; }

.save-thanks {
  font-size: var(--text-sm, 0.95rem);
  color: #3a2d08;
  margin: 0;
  padding-right: 1.6rem;   /* clear of the close button */
  text-align: center;
}

/* No focus ring. dock.js focuses this so the confirmation is announced, but it
   is a status message with tabindex="-1" — nobody tabs to it, and drawing a
   dark box round two words of gold makes the panel look like it has an error
   in it. The announcement is what the focus is for; the ring would only be
   telling a sighted user about a "control" they cannot use. */
.save-thanks:focus,
.save-thanks:focus-visible { outline: none; }
/* ---------- music toggle ----------
   Bottom-LEFT, mirroring the dock on the right, and above the overlay so it
   is reachable during the opening too - the music starts on the first gesture
   and the guest may want it off immediately. Music that cannot be stopped is
   worse than no music, especially on a phone in a quiet room. */
.music-toggle {
  position: fixed;
  left: max(0.9rem, env(safe-area-inset-left, 0px));
  bottom: max(0.9rem, env(safe-area-inset-bottom, 0px));
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--blue-deep, #4A6A80);
  background: rgba(250, 246, 237, 0.86);
  border: 1px solid var(--gold, #C9A227);
  border-radius: 50%;
  cursor: pointer;
  opacity: 0.7;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  transition: opacity 0.2s ease, color 0.2s ease;
}

.music-toggle:hover { opacity: 1; color: var(--gold-deep, #96751C); }
.music-toggle:focus-visible { opacity: 1; outline: 2px solid var(--blue-deep, #4A6A80); outline-offset: 3px; }
.music-toggle svg { width: 20px; height: 20px; display: block; }

/* The slash is the OFF state, so it is hidden while the music plays. */
.music-toggle.is-playing .music-slash { display: none; }

@media (max-width: 420px) {
  .music-toggle { width: 40px; height: 40px; }
  .music-toggle svg { width: 18px; height: 18px; }
}