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

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #1a1a1c;
  font-family: 'Montserrat', 'Helvetica Neue', Arial, sans-serif;
  color: #f5f5f5;
}

#deck {
  position: relative;
  width: 100vw;
  height: 100vh;
}

/* Mostly-sequential fade, not a full cross-dissolve. slides.js's show()
   removes is-active from the outgoing slide, then waits until it's ~90%
   faded out (SHOW_FADE_MS * SHOW_FADE_OVERLAP, kept in sync with the
   duration below) before giving the incoming slide is-active — so the two
   only overlap for the last, darkest 10% of the fade instead of the whole
   thing, avoiding both a jarring 50/50 blend AND a dead instant of pure
   black. `linear` (not `ease`) so that 90% of the time really is 10%
   opacity, not skewed by an easing curve. */
.slide {
  position: absolute;
  inset: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  text-align: center;
  padding: 8vh 8vw 12vh;
  opacity: 0;
  visibility: hidden;
  background: linear-gradient(160deg, #202022 0%, #131314 100%);
  will-change: opacity;
  transition: opacity 450ms linear, visibility 450ms;
}

.slide.is-active {
  opacity: 1;
  visibility: visible;
  z-index: 3;
}

.slide.slide--dark { background: linear-gradient(160deg, #17181a 0%, #0c0c0d 100%); }
.slide.slide--gold { background: linear-gradient(160deg, #2a2318 0%, #14110a 100%); }

/*
  Photo-driven slide: a full-bleed background photo, Ken Burns pan/zoom, and
  a bottom scrim gradient so text stays legible over any image. This is the
  DEFAULT look — most slides in the deck should set `bg` in js/slides.js.
  Text-only slides (kind: 'title', or content slides with no `bg`) fall back
  to the flat gradient above.

  The photo + Ken Burns + scrim are rendered by a single shared WebGL canvas
  (js/bg-webgl.js), reparented into whichever `.slide` is active — not plain
  CSS background-image/gradients. That canvas computes the scrim gradient
  and dithers it in-shader, which survives video re-compression far better
  than a CSS gradient + static noise overlay (see bg-webgl.js's own
  comments for why). `.bg-canvas` below is that canvas's styling.
*/
.slide.has-bg { justify-content: flex-end; }

.slide > .bg-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
}

.slide > * { position: relative; z-index: 2; }

/* A solid pill behind every kicker guarantees contrast regardless of how
   busy the underlying photo is (e.g. the ai-concierge.jpg chat collage) —
   text-shadow alone isn't enough over high-detail backgrounds. */
.slide .kicker {
  display: inline-block;
  font-size: 1.8vh;
  font-weight: 700;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: #f0cb84;
  background: rgba(10, 9, 7, 0.65);
  border: 1px solid rgba(200, 160, 90, 0.35);
  border-radius: 999px;
  padding: 0.9vh 2.2vh;
  margin-bottom: 2.2vh;
}

.slide h1 {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: 4.5vh;
  line-height: 1.15;
  margin-bottom: 2vh;
  text-shadow: 0 2px 24px rgba(0,0,0,0.55);
}

.slide p.body {
  font-size: 2.25vh;
  line-height: 1.5;
  max-width: 82vw;
  color: #e2e2e2;
  text-shadow: 0 2px 16px rgba(0,0,0,0.55);
}

.slide.slide--full-bg { justify-content: center; padding: 8vh 8vw; }

/* Hook act (slides 1-4): one continuous piece with the splash — bigger serif
   headline, same heavy vignette, same legacy/future accent language. Every
   other slide in the deck keeps the smaller bottom-pinned kicker/h1/body. */
.slide.slide--full-bg .kicker { font-size: 2.1vh; }
.slide.slide--full-bg h1 {
  font-size: 5.1vh;
  font-weight: 700;
  max-width: 84vw;
}
.slide.slide--full-bg p.body {
  font-size: 2.4vh;
  max-width: 76vw;
}

.slide .wordmark {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: 8.4vh;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: #fff;
  text-shadow: 0 4px 40px rgba(0,0,0,0.85), 0 2px 10px rgba(0,0,0,0.9);
}

.slide .wordmark::after {
  content: '';
  display: block;
  width: 6.4vh;
  height: 0.3vh;
  margin: 2.1vh auto 0;
  background: #c8a05a;
}

.slide .tagline {
  margin-top: 2.1vh;
  font-size: 2.4vh;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #fff;
  text-shadow: 0 2px 20px rgba(0,0,0,0.85);
}

/* Old vs. new emphasis, usable in the tagline/h1/body anywhere in the hook
   act (slides 1-4) — "app"/legacy reads faded and struck through, the
   AI/new idea reads as the loud, unmissable thing. Kept as bare classes
   (not scoped to .tagline) so h1/body copy can reuse the same language. */
.legacy {
  color: #d6d6d6;
  font-weight: 400;
  opacity: 0.85;
}

.future {
  /* inline, not inline-block: inline-block creates a line-break opportunity
     right after itself even with no space, which orphaned trailing
     punctuation (",", ".") onto their own line. Plain inline avoids that. */
  color: #ffcf7a;
  font-weight: 800;
  letter-spacing: 0.02em;
  animation: taglineGlow 1900ms ease-in-out infinite;
}

.tagline .future { font-size: 1.18em; }

@keyframes taglineGlow {
  0%, 100% { text-shadow: 0 0 1.6vh rgba(255, 195, 110, 0.5), 0 2px 20px rgba(0,0,0,0.9); }
  50%      { text-shadow: 0 0 3.4vh rgba(255, 210, 130, 0.9), 0 2px 20px rgba(0,0,0,0.9); }
}

.slide .subline {
  margin-top: 1.3vh;
  font-size: 1.85vh;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #c8a05a;
  text-shadow: 0 2px 16px rgba(0,0,0,0.7);
}

/* Staggered entrance for a slide's own text — each element rises in a
   beat after the last, instead of the whole slide popping in as one flat
   block. Elements sit invisible until the slide is dealt is-active, then
   animate in once (the deck never revisits an earlier slide). */
.slide .kicker,
.slide h1,
.slide p.body,
.slide .wordmark,
.slide .tagline,
.slide .subline {
  opacity: 0;
}

@keyframes contentRise {
  from { opacity: 0; transform: translateY(2.6vh); }
  to   { opacity: 1; transform: translateY(0); }
}

.slide.is-active .kicker {
  animation: contentRise 780ms cubic-bezier(.22,.61,.36,1) 260ms forwards;
}
.slide.is-active h1,
.slide.is-active .wordmark {
  animation: contentRise 860ms cubic-bezier(.22,.61,.36,1) 380ms forwards;
}
.slide.is-active p.body,
.slide.is-active .tagline {
  animation: contentRise 860ms cubic-bezier(.22,.61,.36,1) 500ms forwards;
}
.slide.is-active .subline {
  animation: contentRise 860ms cubic-bezier(.22,.61,.36,1) 620ms forwards;
}

/*
  Miri popup — an animated "ask Miri" chat card that pops up over the feature
  background partway through a content slide's dwell time. Two skins cycle
  across the deck (see MIRI_STYLES / js/slides.js): `--app` (Mirigi's own
  brand — charcoal/pearl/gold, matches the brochure palette) and
  `--whatsapp` (green header + bubble treatment, evoking WhatsApp without
  reproducing its logo/trademark assets).
*/
.miri-pop {
  position: absolute;
  left: 50%;
  top: 15vh;
  width: 76vw;
  transform: translate(-50%, 0.75vh);
  opacity: 0;
  z-index: 4;
  border-radius: 2vh;
  overflow: hidden;
  box-shadow: 0 3vh 5vh rgba(0,0,0,0.45);
  transition: opacity 460ms ease, transform 460ms cubic-bezier(.2,.8,.2,1);
}

.miri-pop.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}

.miri-pop__head {
  display: flex;
  align-items: center;
  gap: 1.4vh;
  padding: 1.6vh 2.2vh;
}

.miri-pop__avatar {
  width: 4vh;
  height: 4vh;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 2vh;
  flex-shrink: 0;
}

.miri-pop__headtext { font-size: 2vh; }

.miri-pop__body { padding: 0.5vh 2.2vh 2.4vh; display: flex; flex-direction: column; gap: 1.2vh; }

/*
  Every bubble (user or Miri) is hidden until its turn in the sequence —
  needed for multi-turn conversations, where several bubbles already exist
  in the DOM waiting their turn (see MIRI_STYLES / playConversation in
  js/slides.js). Two-turn ask/answer exchanges use the same mechanism.
*/
.bubble {
  max-width: 82%;
  padding: 1.2vh 1.8vh;
  border-radius: 1.8vh;
  font-size: 2vh;
  line-height: 1.4;
  text-align: left;
  opacity: 0;
  transition: opacity 260ms ease;
}
.bubble.is-visible { opacity: 1; }

.bubble--user { align-self: flex-end; border-bottom-right-radius: 0.4vh; }
.bubble--miri { align-self: flex-start; border-bottom-left-radius: 0.4vh; }

.bubble__meta {
  display: block;
  font-size: 1.4vh;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  opacity: 0.6;
  margin-bottom: 0.6vh;
}

/* Longer, multi-turn conversations (the flagship storytelling slides) get a
   tighter rhythm so 4-5 turns still fit above the pinned title/body text. */
.miri-pop--long { top: 8vh; }
.miri-pop--long .miri-pop__head { padding: 1.3vh 2vh; }
.miri-pop--long .miri-pop__body { gap: 0.9vh; }
.miri-pop--long .bubble { font-size: 1.8vh; padding: 1vh 1.5vh; line-height: 1.35; }

.bubble__typing { display: none; align-items: center; gap: 0.5vh; padding: 0.4vh 0; }
.bubble__typing.is-visible { display: inline-flex; }
.bubble__typing i {
  width: 0.8vh;
  height: 0.8vh;
  border-radius: 50%;
  opacity: 0.55;
  animation: typingDot 900ms ease-in-out infinite;
}
.bubble__typing i:nth-child(2) { animation-delay: 150ms; }
.bubble__typing i:nth-child(3) { animation-delay: 300ms; }

@keyframes typingDot {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30% { transform: translateY(-0.6vh); opacity: 1; }
}

/* Voice-note bubble — used when a reply needs more data than fits a short
   typed line (e.g. dictating a friend's name/ID/phone for guest registration). */
.bubble--audio { display: flex; align-items: center; gap: 1.1vh; }
.audio-play { font-size: 1.9vh; line-height: 1; flex-shrink: 0; opacity: 0.85; }
.audio-wave { display: flex; align-items: flex-end; gap: 0.35vh; height: 2.6vh; flex: 1; }
.audio-wave i { display: block; width: 0.35vh; border-radius: 1vh; background: currentColor; opacity: 0.4; height: calc(var(--h) * 1%); transition: opacity 260ms ease; }
.bubble--audio.is-playing .audio-wave i { opacity: 0.95; }
.audio-duration { font-size: 1.6vh; opacity: 0.7; flex-shrink: 0; }

/* Structured confirmation card — Miri reads a request back before acting on
   it, so the guest sees exactly what was captured (e.g. from a voice note). */
.confirm-fields { margin-top: 0.9vh; display: flex; flex-direction: column; gap: 0.5vh; }
.confirm-row { display: flex; justify-content: space-between; gap: 1.6vh; font-size: 1.7vh; opacity: 0; transform: translateY(4px); transition: opacity 240ms ease, transform 240ms ease; }
.confirm-row.is-visible { opacity: 1; transform: translateY(0); }
.confirm-label { opacity: 0.6; font-weight: 600; }
.confirm-value { font-weight: 700; text-align: right; }
.confirm-footer { display: block; margin-top: 0.9vh; font-weight: 700; opacity: 0; transition: opacity 240ms ease; }
.confirm-footer.is-visible { opacity: 1; }

/*
  Confirm chip — the visible "the guest is in control" beat. Miri never just
  acts: she proposes (the `ask` bubble), this chip appears, and only once it
  flips to a confirmed/checked state (simulating the guest's tap) does the
  status change to done. See playMiriPopup() in js/slides.js.
*/
.miri-chip {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 0.8vh;
  padding: 1vh 2vh;
  border-radius: 999px;
  font-size: 1.9vh;
  font-weight: 700;
  font-family: inherit;
  background: transparent;
  border: 0.2vh solid transparent;
  opacity: 0;
  transform: translateY(6px) scale(0.96);
  transition: opacity 320ms ease, transform 320ms cubic-bezier(.34,1.56,.64,1), background 220ms ease, color 220ms ease;
}
.miri-chip.is-visible { opacity: 1; transform: translateY(0) scale(1); }
.miri-chip.is-tapped { transform: translateY(0) scale(0.94); }
.miri-chip__check { font-size: 1.7vh; line-height: 1; }

/* — App skin: Mirigi brand (charcoal / pearl / gold) — */
.miri-pop--app { background: linear-gradient(180deg, rgba(255,255,255,.97) 0%, rgba(247,244,237,.96) 100%); border-top: 0.4vh solid #c8a05a; }
.miri-pop--app .miri-pop__avatar { background: #232323; color: #c8a05a; }
.miri-pop--app .miri-pop__headtext { color: #232323; }
.miri-pop--app .bubble--user { background: #232323; color: #fff; }
.miri-pop--app .bubble--miri { background: #f5f0e6; color: #232323; border-left: 0.3vh solid #c8a05a; }
.miri-pop--app .bubble__meta { color: #8a8578; }
.miri-pop--app .bubble__typing i { background: #232323; }
.miri-pop--app .miri-chip { border-color: #232323; color: #232323; }
.miri-pop--app .miri-chip.is-confirmed { background: #232323; color: #c8a05a; }

/* — WhatsApp skin — */
.miri-pop--whatsapp { background: #e9e3d8; }
.miri-pop--whatsapp .miri-pop__head { background: #075e54; }
.miri-pop--whatsapp .miri-pop__avatar { background: #128c7e; color: #fff; }
.miri-pop--whatsapp .miri-pop__headtext { color: #fff; }
.miri-pop--whatsapp .bubble--user { background: #dcf8c6; color: #1b1b1b; }
.miri-pop--whatsapp .bubble--miri { background: #fff; color: #1b1b1b; }
.miri-pop--whatsapp .bubble__meta { color: #6f6f6f; }
.miri-pop--whatsapp .bubble__typing i { background: #4a4a4a; }
.miri-pop--whatsapp .miri-chip { border-color: #128c7e; color: #128c7e; }
.miri-pop--whatsapp .miri-chip.is-confirmed { background: #128c7e; color: #fff; }
.miri-pop--whatsapp .bubble__typing i { background: #4a4a4a; }

#progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 3vh;
  display: flex;
  justify-content: center;
  gap: 0.8vh;
  z-index: 10;
}

#progress .dot {
  width: 0.9vh;
  height: 0.9vh;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.35);
  box-shadow: 0 0 2px rgba(0,0,0,0.6);
  transition: background 240ms ease;
}

#progress .dot.is-active { background: #c8a05a; }

/*
  Always-on QR badge — persists across every slide (it's a sibling of #deck,
  not per-slide markup) so it's scannable at any point in the video, not just
  a dedicated contact slide. Fixed top-right, clear of the centered Miri
  popups (which cap at 76vw wide) and the bottom-pinned title/body text.
*/
#qr-badge {
  position: fixed;
  top: 2.4vh;
  left: calc(100vw - 12.4vh);
  width: 10vh;
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6vh;
  background: rgba(255, 255, 255, 0.96);
  border-radius: 1vh;
  padding: 1vh 1vh 0.8vh;
  box-shadow: 0 1vh 2.4vh rgba(0, 0, 0, 0.4);
  transform: translate(0, 0) scale(1);
  transition: top 900ms cubic-bezier(.2,.8,.2,1), left 900ms cubic-bezier(.2,.8,.2,1), transform 900ms cubic-bezier(.2,.8,.2,1), box-shadow 900ms ease;
}

/* Contact slide (see slide.qrFocus in js/slides.js): the badge grows from
   its resting corner to dead-center and 3x size, so it's unmissable and
   easy to scan for that one beat of the video. */
#qr-badge.is-focused {
  top: 50vh;
  left: 50vw;
  transform: translate(-50%, -50%) scale(3);
  box-shadow: 0 3vh 8vh rgba(0, 0, 0, 0.65);
}

#qr-badge .qr-badge__code {
  display: block;
  width: 8vh;
  height: 8vh;
}

#qr-badge .qr-badge__label {
  font-size: 1.4vh;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #232323;
}

/* Dithering: our dark charcoal/gold gradients, scrims and the taglineGlow
   text-shadow pulse are all large, near-flat color fields, which is exactly
   what 8-bit rendering + 8-bit video encoding turns into visible banding —
   no encoder setting downstream can fully undo a quantization step that has
   already happened. The standard fix (same trick used in film/video color
   grading) is to inject a very faint noise texture BEFORE that quantization
   so each 8-bit step has something to round against instead of a hard edge.
   This sits above everything (z-index above .slide's own z-index:2 content)
   as a fixed, blended, non-interactive layer — subtle enough to be
   imperceptible as "grain" but enough to break up banding. */
#grain {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  /* Stronger + coarser than a typical UI dither: Playwright's raw capture
     is itself a lossy ~1Mbit/s VP8 encode before our own ffmpeg pass ever
     sees a frame, and video codecs specifically attenuate fine, per-pixel
     noise as "detail that doesn't matter" during DCT quantization — a
     subtle dither gets encoded away twice over. Coarser blobs (lower
     baseFrequency, bigger tile) and higher amplitude survive that better. */
  /* Was 0.16, tuned back when this was the only dithering in the pipeline
     and had to survive Playwright's own lossy raw capture on top of the
     final encode. Now js/bg-webgl.js's shader dither covers every
     photo/scrim slide (the vast majority of the deck) at the moment of
     generation, and scripts/render-hotelga-video.js's mp4 step dithers its
     own 10-bit->8-bit reduction (zscale=dither=error_diffusion) instead of
     silently truncating. Stacking full-strength grain on top of both now
     reads as visible noise, not "grain" — see the banding in the shadow
     behind "Un Nuevo Modelo" that prompted this. Left non-zero only as a
     safety net for pure-gradient slides with no `bg` (never WebGL-covered)
     and any future run where WebGL2 isn't available. */
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.35' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 260px 260px;
}
