:root {
  color-scheme: light;
  --berry: #7b2d58;
  --gold: #c99a3d;
  --ink: #1f1720;
  --paper: #fffaf7;
  --wedding-background-image: url('/images/background.png');
}

* { box-sizing: border-box; }
body {
  position: relative;
  margin: 0;
  min-height: 100vh;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: var(--paper);
  background:
    linear-gradient(180deg, rgba(31, 23, 32, 0.68), rgba(31, 23, 32, 0.82)),
    var(--wedding-background-image) center / cover fixed no-repeat,
    #1f1720;
}

body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: var(--wedding-background-image) center / cover fixed no-repeat;
  opacity: 0.48;
  transition: opacity 220ms ease;
}

body.slot-spinning::before {
  opacity: 1;
}

/* GodHost "Broadcast Now" text ticker -- a thin bar pinned to the top of
   the viewport (not a modal/overlay), so it never blocks the reels, the
   idle gallery, or anything else the guest is doing -- it just scrolls
   past and can be dismissed early. See apps/web/src/main.js's
   showTickerBanner()/pollForReelUpdates() for when this gets populated. */
.ticker-banner {
  position: fixed;
  top: 5px;
  left: 0;
  right: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 40px;
  padding: 0 12px;
  background: rgba(31, 23, 32, 0.92);
  border-bottom: 2px solid #d4af6a;
  overflow: hidden;
}
.ticker-banner[hidden] { display: none; }
.ticker-banner-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  color: #fffaf7;
  font-weight: 700;
  font-size: .92rem;
}
.ticker-banner-text span {
  display: inline-block;
  padding-left: 100%;
  animation: ticker-banner-scroll linear infinite;
}
@keyframes ticker-banner-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}
.ticker-banner-close {
  flex-shrink: 0;
  min-width: 28px;
  min-height: 28px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  color: #fffaf7;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}
.ticker-banner-close:hover,
.ticker-banner-close:focus-visible { background: rgba(255, 255, 255, 0.3); }
@media (prefers-reduced-motion: reduce) {
  .ticker-banner-text span { animation: none; padding-left: 0; white-space: normal; }
  .ticker-banner { height: auto; min-height: 40px; padding: 8px 12px; }
}
.app-shell {
  width: min(1120px, 100%);
  margin: 0 auto;
  padding: 24px;
  /* .airbridge-panel is position: fixed and always floats above the very
     bottom of the viewport (button + its status label above it, roughly
     150px tall) regardless of how much content is above it. Reserving that
     much space at the bottom of normal document flow keeps the last
     in-flow element (the well-wishes button) from ever rendering underneath
     it on short viewports, instead of relying on scroll position or content
     length to happen to clear it. */
  padding-bottom: max(150px, env(safe-area-inset-bottom));
}
.hero {
  padding-block: 24px 10px;
  text-align: center;
  text-shadow: 0 3px 18px rgba(0, 0, 0, 0.44);
}
.eyebrow { color: #f7d788; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; }
h1 { margin: 0 0 1em; font-size: clamp(2rem, 8vw, 4rem); }
#coupleNames { margin: 0 0 2px; }
/* Only clickable (and only looks it) once applyWeddingState confirms there's
   an actual welcome video to play -- see main.js's toggleWelcomeVideo(). No
   underline: the "(click me ❤️)" hint directly beneath it (also clickable)
   is what signals that, without decorating the names themselves. */
#coupleNames.has-welcome-video {
  cursor: pointer;
}
#coupleNames.has-welcome-video:hover,
#coupleNames.has-welcome-video:focus-visible {
  color: #f7d788;
}
.welcome-video-hint {
  margin: 2px 0 0;
  font-size: .6rem;
  font-style: italic;
  color: #f7d788;
  opacity: .9;
  cursor: pointer;
}
.welcome-video-hint:hover,
.welcome-video-hint:focus-visible {
  opacity: 1;
}
.welcome-message { width: min(560px, calc(100% - 32px)); margin: 0 auto 18px; }
.welcome-message video { width: 100%; max-height: 320px; border-radius: 12px; background: #110d12; }

.reels {
  --lever-space: clamp(58px, 14vw, 76px);
  --slot-gap: 0px;
  position: relative;
  display: block;
  width: min(94vw, 760px);
  margin: 20px auto 124px;
  padding-right: var(--lever-space);
}

.slot-wrapper {
  width: 100%;
  margin: 0 auto;
  border: 10px solid #fff;
  background: rgba(0, 0, 0, 0.45);
  /* createReelView sets an inline background-image here once a GodHost has
     provisioned a reel-background GIF (theme.reelBackgroundUrl); this plain
     dark fill is just the fallback until then. */
  background-size: cover;
  background-position: center;
  transition: border-color 200ms ease;
}

/* A guest win where all three reels show the same symbol (a normal win or
   Made in Heaven -- see main.js's spin handler, keyed off
   authorization.resultType !== 'NO_WIN') flashes the wrapper's border
   goldenrod for 2 seconds before settling back to white. Retriggered by
   forcing a reflow and re-adding the class (see triggerReelWinFlash), so
   back-to-back wins each get their own full flash. */
@keyframes reel-win-flash {
  0%, 100% { border-color: #fff; }
  15%, 45%, 75% { border-color: goldenrod; }
  30%, 60%, 90% { border-color: #fff; }
}

.slot-wrapper.win-flash {
  animation: reel-win-flash 2s ease-in-out;
}

.slot-wrapper2 {
  width: 100%;
  margin: 0 auto;
  border: 2px solid #000;
  display: flex;
  justify-content: center;
  gap: var(--slot-gap);
}

.slot {
  position: relative;
  width: calc((100% - (2 * var(--slot-gap))) / 3);
  /* Exactly square, matching each 172x172 sprite frame 1:1. A window taller
     or shorter than one full frame always crops the resting symbol
     asymmetrically, which reads as "stopped between two rows" even when
     the frame shown is otherwise exactly correct. Matching the window to
     the frame removes that ambiguity entirely -- the whole symbol is
     always shown, fully centered, with no sliver of a neighboring row
     ever visible. */
  aspect-ratio: 1 / 1;
  min-height: 76px;
  flex: 1 1 0;
  overflow: hidden;
  border: 1px solid #fff;
  /* Fallback placeholder art was a background-image pointing at
     /images/thevoice_normal.png, a file that no longer exists in this
     project -- every reel render was firing a 404 for it. A real sprite
     (built server-side by ReelSpriteService) covers this element via the
     .slot-sprite <img> in virtually every case anyway, so there's nothing
     meaningful lost by dropping the fallback outright rather than pointing
     it at a replacement asset. */
  /* background: url(/images/thevoice_normal.png) repeat-y; */
  /* background-size: 100% auto; */
  image-rendering: auto;
}

/* The wedding-specific reel sprite: one 172x172 frame per symbol id,
   stacked vertically. width:100% + height:auto preserves its natural
   1-column-of-18-frames aspect ratio at whatever size it renders, so each
   frame is always exactly 1/18th of the image's *own current* height --
   see spin-controller.js's applySpriteFrame, which shows a given frame
   with a percentage-based translateY resolved against that same current
   size. That's what makes this immune to the reel being resized after a
   spin settles, unlike the raw-pixel background-position this replaced. */
.slot-sprite {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  display: block;
  pointer-events: none;
}

/* Applied only while this reel is resting on the shared protected row and
   showing the couple's overlay photo (see updateReelProtectedOverlays in
   packages/ui/src/index.js). Many overlay photos are sticker-style images
   with transparent regions -- without hiding the sprite <img> entirely,
   whatever was resting underneath (a starter image, or an old guest-won
   replacement photo) shows straight through those transparent gaps.
   Removed the instant a new spin starts (see spin-controller.js), so the
   reel shows itself spinning normally again. */
.slot.protected-masked .slot-sprite {
  visibility: hidden;
}

.slot.motion,
.slot.spinning {
  /* The spinning-motion look is a CSS blur instead of a second
     server-generated blurred sprite -- one fewer file to keep in sync per
     wedding, and no risk of the blur/sharp sprites ever mismatching. */
  filter: blur(3px) brightness(1.05);
}

/* The couple's actual left/center/right photo, shown on top of whichever
   reel(s) this spin's outcome independently decided to overlay (see
   decideSpinOutcome in packages/core/src/spin-engine.js and
   updateReelProtectedOverlays in packages/ui/src/index.js) -- never tied to
   a particular symbol, since none is reserved anymore. Sits above the
   sprite background but below the inset-shadow ::after so the reel still
   reads as glass/depth. object-fit is "contain", not "cover" -- cover was
   cropping the top and bottom off photos that weren't already exactly
   square, which both hides part of what the GodHost uploaded and reads as
   broken. Same reasoning ReelSpriteService uses "contain" for the sprite's
   own baked-in photos: letterboxed-but-whole beats cropped-but-filled for a
   couple's photo. A goldenrod border marks these photos as the special
   "Made in Heaven" content, distinct from any ordinary reel symbol --
   box-sizing: border-box keeps the border inside the same inset:0 box
   instead of pushing the image outward. */
/* Opacity-only show/hide (see updateReelProtectedOverlays in
   packages/ui/src/index.js) instead of the `hidden` attribute -- `hidden`
   forces display:none, which snaps instantly and can't be transitioned.
   This fades in over 260ms, timed by main.js to start the moment THIS
   specific reel's nudge settles rather than after all three reels finish,
   so the couple's photo reads as part of the reel coming to rest instead of
   a separate pop tacked on at the very end. */
.reel-protected-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  object-fit: contain;
  background: rgba(0, 0, 0, 0.55);
  border: 4px solid goldenrod;
  box-shadow: 0 0 14px rgba(218, 165, 32, 0.7), inset 0 0 10px rgba(218, 165, 32, 0.35);
  pointer-events: none;
  z-index: 1;
  opacity: 0;
  transition: opacity 260ms ease;
}

.reel-protected-overlay-visible {
  opacity: 1;
}

.clear { clear: both; }

.slot .stop {
  display: none !important;
}

.slot:not(.spinning) {
  cursor: zoom-in;
}

.slot::after {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  /* box-shadow:
    inset 0 12px 16px rgba(255, 255, 255, 0.54),
    inset 0 -12px 16px rgba(0, 0, 0, 0.32); */
  pointer-events: none;
}

.primary-action {
  min-height: 44px;
  border: 0;
  border-radius: 8px;
  padding: 0 18px;
  background: var(--berry);
  color: white;
  font-weight: 700;
}

.slot-lever {
  position: absolute;
  top: 4px;
  right: 0;
  width: clamp(56px, 13vw, 72px);
  height: clamp(132px, 31vw, 180px);
  padding: 0;
  border: 0;
  background: transparent;
  transform-origin: 50% 86%;
  cursor: pointer;
  touch-action: manipulation;
  z-index: 4;
  perspective: 180px;
  transform-style: preserve-3d;
}

.slot-lever:disabled {
  cursor: wait;
  opacity: 0.92;
}

.slot-lever-arm {
  position: absolute;
  left: 42%;
  top: 18%;
  width: 18%;
  height: 76%;
  border-radius: 8px;
  background:
    linear-gradient(
      90deg,
      #5b5b5b,
      #f1f1f1 45%,
      #777
    );
  box-shadow: 2px 3px 6px rgba(0, 0, 0, 0.35);
}

.slot-lever-knob {
  position: absolute;
  z-index: 1;
  left: 14%;
  top: 0;
  width: 72%;
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    radial-gradient(
      circle at 35% 28%,
      #ffb4b4,
      #d31628 48%,
      #6b0010 78%
    );
  box-shadow:
    inset -4px -5px 8px rgba(0, 0, 0, 0.3),
    0 5px 8px rgba(0, 0, 0, 0.35);
}

.slot-lever.is-pulling {
  animation: pull-slot-lever 720ms cubic-bezier(.28, .04, .18, 1);
}

@keyframes pull-slot-lever {
  0% {
    transform: rotateX(0deg) rotateZ(0deg) translateY(0) scale(1);
  }

  38% {
    transform: rotateX(-58deg) rotateZ(6deg) translateY(20px) scale(0.94);
  }

  62% {
    transform: rotateX(-48deg) rotateZ(4deg) translateY(16px) scale(0.96);
  }

  100% {
    transform: rotateX(0deg) rotateZ(0deg) translateY(0) scale(1);
  }
}

.status { min-height: 24px; text-align: center; font-weight: 700; }

.airbridge-panel {
  position: fixed;
  left: 50%;
  bottom: max(12px, env(safe-area-inset-bottom));
  z-index: 20;
  display: grid;
  justify-items: center;
  gap: 0.35rem;
  width: auto;
  margin: 0;
  transform: translateX(-50%);
}

/* Row that holds the settings gear alongside the AirBridge toggle -- kept
   as its own flex row (rather than putting both buttons directly in the
   .airbridge-panel grid) so the status label below still centers under the
   AirBridge button specifically, not the midpoint of both buttons. */
.airbridge-buttons {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* Same circular button footprint as .airbridge-icon-button, but without its
   dimmed/greyscale "not listening yet" default look or pulse animation --
   those are specific to AirBridge's on/off listening state, which doesn't
   apply to a settings gear. Hidden by default in the HTML; main.js reveals
   it only for Hosts (see universeAccess.isGodHost there). */
.settings-icon-button {
  width: 52px;
  height: 52px;
  min-height: 0;
  padding: 12px;
  border: 0;
  border-radius: 50%;
  background: rgba(35, 25, 38, 0.55);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.3);
  cursor: pointer;
  touch-action: manipulation;
  transition: background 180ms ease, transform 180ms ease;
}

.settings-icon-button svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: #fffaf7;
}

.settings-icon-button:hover,
.settings-icon-button:focus-visible {
  background: rgba(35, 25, 38, 0.78);
}

.settings-icon-button:active {
  transform: scale(0.94);
}

.settings-icon-button:focus-visible {
  outline: 3px solid #f7d788;
  outline-offset: 4px;
}

.settings-icon-button[hidden] {
  display: none;
}

/* Same circular footprint/treatment as .settings-icon-button, but visible
   to every guest (not just Hosts) -- see admin.js/main.js: settingsToggle
   stays hidden until universeAccess.isGodHost resolves, calendarToggle
   never does. */
.calendar-icon-button {
  width: 52px;
  height: 52px;
  min-height: 0;
  padding: 12px;
  border: 0;
  border-radius: 50%;
  background: rgba(35, 25, 38, 0.55);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.3);
  cursor: pointer;
  touch-action: manipulation;
  transition: background 180ms ease, transform 180ms ease;
}
.calendar-icon-button svg { display: block; width: 100%; height: 100%; fill: #fffaf7; }
.calendar-icon-button:hover, .calendar-icon-button:focus-visible {
  background: rgba(35, 25, 38, 0.78);
}
.calendar-icon-button:active { transform: scale(0.94); }
.calendar-icon-button:focus-visible { outline: 3px solid #f7d788; outline-offset: 4px; }

/* Guest calendar modal -- same dark full-screen-overlay treatment as
   .media-lightbox below, but a scrollable list box instead of a single
   media element, since a calendar can have any number of events. */
.calendar-modal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: grid;
  place-items: center;
  padding: clamp(16px, 4vw, 48px);
  background: rgba(31, 23, 32, 0.92);
}
/* Without this, the [hidden] attribute set by main.js's closeCalendarModal()
   (and the HTML's default hidden state) is silently overridden by the
   `display: grid` above -- author CSS beats the UA [hidden] stylesheet rule,
   so the modal rendered as a permanent full-screen overlay blocking the
   game regardless of hidden state. Same fix pattern already used for
   .settings-icon-button[hidden] below. */
.calendar-modal[hidden] {
  display: none;
}
.calendar-modal-box {
  width: min(100%, 480px);
  max-height: 82vh;
  overflow-y: auto;
  background: #fffaf7;
  color: #231926;
  border-radius: 12px;
  padding: 18px;
}
.calendar-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}
.calendar-modal-header h2 { margin: 0; font-size: 1.1rem; }
.calendar-modal-close {
  min-height: 36px;
  min-width: 36px;
  border: 0;
  border-radius: 50%;
  background: #f1e6da;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}
.calendar-modal-empty { color: #7a6f7e; font-style: italic; }
.calendar-modal-event {
  padding: 12px 0;
  border-top: 1px solid #e8d8cd;
}
.calendar-modal-event:first-child { border-top: 0; padding-top: 0; }
.calendar-modal-event h3 { margin: 0 0 4px; font-size: 1rem; }
.calendar-modal-event-when { display: block; font-size: .86rem; color: #5d5361; margin-bottom: 4px; }
.calendar-modal-event-comment { margin: 4px 0 0; font-size: .86rem; color: #5d5361; }

/* Address and event-website links, rendered as icon-first affordances
   instead of raw address/URL text -- see main.js renderCalendarModal(). */
.calendar-modal-event-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 4px 0 2px;
}
.calendar-modal-directions-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 6px 8px;
  border-radius: 999px;
  background: #f1e6da;
  color: #7b2d58;
  font-size: .82rem;
  font-weight: 700;
  text-decoration: none;
}
.calendar-modal-directions-link svg { width: 18px; height: 18px; fill: #7b2d58; flex-shrink: 0; }
.calendar-modal-directions-link:hover,
.calendar-modal-directions-link:focus-visible { background: #e8d8cd; }
.calendar-modal-website-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 6px 8px;
  border-radius: 999px;
  background: #f1e6da;
  color: #7b2d58;
  font-size: .82rem;
  font-weight: 700;
  text-decoration: none;
  max-width: 100%;
}
.calendar-modal-website-link svg { width: 18px; height: 18px; fill: #7b2d58; flex-shrink: 0; }
.calendar-modal-website-link span { overflow-wrap: anywhere; }
.calendar-modal-website-link:hover,
.calendar-modal-website-link:focus-visible { background: #e8d8cd; }

.airbridge-icon-button {
  width: 68px;
  height: 68px;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.3);
  cursor: pointer;
  touch-action: manipulation;
  filter: grayscale(1);
  opacity: 0.38;
  transition: filter 180ms ease, opacity 180ms ease, transform 180ms ease;
}

.airbridge-icon-button.is-active {
  filter: none;
  opacity: 1;
  animation: airbridge-listening-pulse 3.2s ease-in-out infinite;
}

@keyframes airbridge-listening-pulse {
  0%, 100% {
    opacity: 1;
  }

  50% {
    opacity: 0.1;
  }
}

.airbridge-icon-button img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.airbridge-icon-button:disabled {
  cursor: wait;
  opacity: 0.72;
}

.airbridge-icon-button:focus-visible {
  outline: 3px solid #f7d788;
  outline-offset: 4px;
}

.airbridge-panel .status {
  position: absolute;
  bottom: 74px;
  width: max-content;
  max-width: 88vw;
  margin: 0;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(31, 23, 32, 0.58);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 180ms ease, transform 180ms ease;
  pointer-events: none;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
}

.airbridge-panel .status.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.secondary-action {
  min-height: 44px;
  border: 1px solid rgba(255, 250, 247, 0.62);
  border-radius: 8px;
  background: rgba(255, 250, 247, 0.88);
  color: #241725;
  font-weight: 800;
}

.app-links {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 12px;
  margin: 14px 0;
}

.app-links a {
  min-height: 38px;
  display: inline-grid;
  place-items: center;
  padding: 0 14px;
  border-radius: 8px;
  background: rgba(31, 23, 32, 0.64);
  color: #f8d77d;
  font-weight: 800;
  text-decoration: none;
}

.app-links a:focus-visible,
.app-links a:hover {
  background: rgba(255, 250, 247, 0.92);
  color: #241725;
}

@media (max-width: 720px) {
  .app-shell { padding: 16px; }
  .reels {
    width: 96vw;
    padding: 8px var(--lever-space) 8px 8px;
  }
  .slot-lever {
    right: 0;
    top: 8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
  .slot-lever.is-pulling,
  .airbridge-icon-button.is-active { animation: none; }
}

.capture-panel {
  margin-block: 20px;
  padding: 16px;
  border: 1px solid #e8d8cd;
  border-radius: 8px;
  background: #fff;
}
.capture-panel:not(.has-review-media) .capture-review-actions {
  display: none;
}
.capture-panel.has-review-media .camera-frame[hidden] {
  display: none;
}
.capture-panel.has-review-media [data-capture-preview] {
  margin-block-start: 0;
}
.capture-cancel {
  margin-block-end: 10px;
}
.camera-frame {
  position: relative;
}
.camera-preview { width: 100%; max-height: 280px; border-radius: 8px; background: #1f1720; object-fit: cover; cursor: pointer; }
.camera-preview:active,
.record-countdown:active { transform: scale(0.99); }
.record-countdown {
  position: absolute;
  border: 0;
  cursor: pointer;
  top: 10px;
  right: 10px;
  min-width: 48px;
  min-height: 48px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: rgba(123, 45, 88, 0.92);
  color: #fffaf7;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 8px 24px rgba(31, 23, 32, 0.28);
}
.capture-mode-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.capture-mode-actions button { flex: 1 1 160px; min-height: 44px; cursor: pointer; }
/* Quick Photo (see guestQuickPhoto in main.js) is a caption-free, photo-only
   variant of the same capture panel used for Send Well Wishes and Join the
   Reels -- hiding these two pieces rather than removing them from the DOM
   keeps every existing listener/selector (record button's own hidden
   toggling, renderCapturePreview, the caption input read at submit time)
   working unmodified for the normal, non-quick flows. */
.capture-panel.quick-photo-mode [data-capture-record] { display: none !important; }
.capture-panel.quick-photo-mode .capture-caption-row { display: none !important; }
.capture-preview { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-block: 12px; }
.capture-preview-item { margin: 0; display: grid; gap: 8px; }
.capture-preview-item img,
.capture-preview-item video { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; background: #1f1720; }
.consent-row input { min-height: 44px; }
.consent-row { display: flex; align-items: center; gap: 10px; font-weight: 700; }
.capture-review-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-block: 12px;
}
.capture-review-actions button {
  flex: 1 1 140px;
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.capture-review-actions button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}
.capture-review-actions [data-capture-remove] {
  border-color: rgba(123, 45, 88, 0.38);
  background: #fffaf7;
  color: var(--berry);
}
progress { width: 100%; min-height: 14px; }

.guest-submit-panel {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin: 10px 0 16px;
}

.idle-gallery {
  position: fixed;
  inset: 0;
  z-index: 900;
  overflow: hidden;
  display: grid;
  place-items: center;
  margin: 0;
  padding: 0;
  background: rgba(17, 13, 18, 0.96);
  opacity: 0;
  pointer-events: none;
  transition: opacity 700ms ease;
}

.idle-gallery[hidden] {
  display: none;
}

.idle-gallery.is-active {
  opacity: 1;
  pointer-events: auto;
}

/* Optional GodHost-chosen ambient video/GIF behind the carousel -- see
   renderIdleGallery in packages/ui/src/index.js. Sits between .idle-gallery's
   own near-opaque dark background and .idle-gallery-stage (which follows it
   in the DOM and so paints on top), dimmed so it never competes with the
   actual photo/video or its caption. */
.idle-gallery-background {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.4;
}

.idle-gallery-stage {
  position: absolute;
  inset: 0;
  touch-action: pan-y;
}

.idle-gallery-slide {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 100%;
  padding: clamp(14px, 4vw, 42px);
  border: 0;
  background: transparent;
  color: #fffaf7;
  opacity: 0;
  pointer-events: none;
  transition: opacity 600ms ease;
}

/* Which slide is showing is tracked in JS (renderIdleGallery in
   packages/ui/src/index.js) rather than a pure-CSS staggered keyframe
   animation, since manual swipe/tap/double-tap navigation needs an
   explicit "current index" to advance, rewind, or hold on. */
.idle-gallery-slide.is-current {
  opacity: 1;
  pointer-events: auto;
}

.idle-gallery-slide img {
  max-width: 100%;
  max-height: 86vh;
  border-radius: 8px;
  object-fit: contain;
  /* No background-color here on purpose -- a transparent-background PNG
     sticker (e.g. an Apple-generated sticker) paints straight through to
     whatever's behind the <img> itself, same as the reel sprite (see
     .slot-sprite in this file, which also has no forced background). A
     solid color here previously showed through every transparent pixel as
     a flat black rectangle instead of true transparency. .idle-gallery's
     own near-opaque dark backdrop (rgba(17, 13, 18, 0.96)) still shows
     through for ordinary rectangular photos that don't fill the
     object-fit: contain box, so there's no visible letterboxing regression
     for non-transparent images. */
  /* box-shadow: 0 24px 90px rgba(0, 0, 0, 0.54); */
  /* The swipe/tap/double-tap gestures are all tracked by pointer listeners
     on .idle-gallery-stage (see renderIdleGallery in packages/ui/src/
     index.js), not on the image itself -- but a mouse pointerdown that
     lands ON the <img> can trigger the browser's native "drag this image
     out" gesture first, which captures the pointer and swallows the
     pointermove/pointerup the swipe logic needs (touch doesn't have this
     problem, which is why this only showed up with a mouse). Making the
     image itself un-hit-testable means every pointer event passes straight
     through to the slide/stage underneath instead -- same effect as
     layering a transparent swipe-catching overlay on top, without needing
     an extra element. */
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
}

.idle-gallery-slide span {
  position: fixed;
  left: 50%;
  bottom: max(18px, env(safe-area-inset-bottom));
  transform: translateX(-50%);
  max-width: min(90vw, 480px);
  padding: 4px 4px;
  font-weight: 800;
  font-size: 1.05rem;
  color: #fffaf7;
  text-align: center;
  overflow-wrap: anywhere;
  /* A one-line caption (guest-written, or a GodHost's starter-image label)
     sits directly over whatever photo/video is showing -- a light fill with
     a hard black outline stays legible against any image behind it,
     bright or dark, without needing an opaque pill to fake the contrast.
     -webkit-text-stroke does the actual outline in Chrome/Safari/Edge; the
     layered text-shadow is the same effect for Firefox, which (as of many
     shipped versions) ignores text-stroke on non-webkit builds. */
  -webkit-text-stroke: 3px #0b0710;
  paint-order: stroke fill;
  text-shadow:
    -1.5px -1.5px 0 #0b0710,
    1.5px -1.5px 0 #0b0710,
    -1.5px 1.5px 0 #0b0710,
    1.5px 1.5px 0 #0b0710,
    0 3px 10px rgba(0, 0, 0, 0.55);
}

body.capture-active .hero,
body.capture-active .reels,
body.capture-active .announcements,
body.capture-active .history-preview,
body.capture-active .guest-submit-panel,
body.capture-active .airbridge-panel,
body.capture-active .app-links {
  display: none;
}

/* The airbridge panel is hidden during capture (above), so the bottom
   clearance .app-shell normally reserves for it would just be dead space
   underneath the capture panel -- drop back to the plain padding. */
body.capture-active .app-shell {
  padding-bottom: 24px;
}

body.capture-active .capture-panel {
  color: #241725;
}

/* A brief toast shown whenever the real AirBridge modem (see main.js's
   modemFactory, which loads /airbridge/Transceiver/airbridge-api.js) decodes
   an acoustic packet -- the DSP layer itself already validates a pulse
   (checksum/hop matching) before ever calling back, so any packet reaching
   this popup is by definition a validated one. Styled as a small toast
   rather than a full-screen lightbox since it's a passive notification, not
   something the guest is meant to inspect closely. */
.airbridge-pulse-popup {
  position: fixed;
  top: max(16px, env(safe-area-inset-top));
  left: 50%;
  transform: translate(-50%, -12px);
  z-index: 1100;
  display: grid;
  gap: 4px;
  min-width: 220px;
  max-width: min(92vw, 420px);
  padding: 14px 18px;
  border-radius: 10px;
  background: rgba(31, 23, 32, 0.94);
  color: #fffaf7;
  text-align: center;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.4);
  border: 1px solid rgba(201, 154, 61, 0.6);
  opacity: 0;
  pointer-events: none;
  transition: opacity 220ms ease, transform 220ms ease;
}
.airbridge-pulse-popup.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
  pointer-events: auto;
}
.airbridge-pulse-popup strong {
  color: var(--gold);
  font-size: 0.95rem;
  letter-spacing: 0.02em;
}
.airbridge-pulse-popup span {
  font-size: 0.85rem;
  opacity: 0.88;
  word-break: break-all;
}

.media-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  place-items: center;
  gap: 14px;
  padding: clamp(16px, 4vw, 48px);
  background: rgba(31, 23, 32, 0.92);
  color: #fffaf7;
  cursor: zoom-out;
}
.media-lightbox img,
.media-lightbox video {
  max-width: min(100%, 1080px);
  max-height: 82vh;
  border-radius: 8px;
  object-fit: contain;
  background: #110d12;
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
}
.media-lightbox p {
  margin: 0;
  font-weight: 700;
  text-align: center;
}

.admin-tools[hidden] { display: none !important; }
