/* =============================================================================
   PRESNT guest web app — layout & components.

   Every screen block below cites the Mobbin reference it is rooted in
   (design/refs/guest/*, notes in design/refs/README.md "Round-4").
   Tokens come from tokens.css, which is a 1:1 port of PresentTheme.swift.
   ========================================================================== */

/* ---------------------------------------------------------------- reset -- */

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

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--p-background);
}

body {
  font: var(--p-t-body);
  color: var(--p-text-primary);
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}

input, button, textarea {
  font: inherit;
  color: inherit;
}

button { cursor: pointer; }

:focus-visible {
  outline: 3px solid var(--p-focus-ring);
  outline-offset: 2px;
  border-radius: 4px;
}

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

[hidden] { display: none !important; }

/* --------------------------------------------------------------- shell --- */

/* Dynamic viewport height keeps the sticky pay footer above mobile Safari's
   collapsing toolbar; the svh fallback is the pre-2022 100vh. */
#app {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
}

.screen {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

/* Screens that own the whole viewport (camera surfaces) never scroll. */
.screen--fixed {
  height: 100vh;
  height: 100svh;
  overflow: hidden;
}

.wrapper {
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  padding: 0 var(--p-card-edge);
}

/* ------------------------------------------------- shared: brand mark ---- */

.brandmark {
  display: flex;
  align-items: center;
  gap: var(--p-s);
  padding: var(--p-l) 0 var(--p-s);
  font: var(--p-t-caption);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--p-text-secondary);
}

.brandmark__dot {
  width: var(--p-live-dot);
  height: var(--p-live-dot);
  border-radius: 50%;
  /* Deliberately NOT live-red. It sat next to the wordmark on the offline pay
     sheet, three lines above a chip reading "Not live" — a red dot promising
     the opposite of the page. The dot is part of the mark; LIVE NOW is the
     only thing that gets to be red. */
  background: currentColor;
}

/* --------------------------------------------------- shared: avatar ------ */

.avatar {
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font: var(--p-t-title);
  color: var(--p-avatar-initial);
  background: var(--p-avatar-fallback);
  box-shadow: var(--p-shadow-floating);
}

/* Seed hooks are retained deliberately — see tokens.css. On paper they
   all resolve to ink; on camera they keep the four original gradients. */
.avatar[data-seed="0"] { background: var(--p-avatar-0); }
.avatar[data-seed="1"] { background: var(--p-avatar-1); }
.avatar[data-seed="2"] { background: var(--p-avatar-2); }
.avatar[data-seed="3"] { background: var(--p-avatar-3); }

.avatar--lg {
  width: var(--p-avatar-large);
  height: var(--p-avatar-large);
  font: var(--p-t-display);
  border: var(--p-avatar-ring-width) solid var(--p-hairline);
}

/* The photo sits ON TOP of the initial, never instead of it: if the image
   fails the letter is already drawn underneath, so a broken avatar degrades to
   a finished one rather than to an empty circle. */
.avatar { position: relative; overflow: hidden; }

.avatar__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

.avatar--md { width: 64px; height: 64px; }
.avatar--sm {
  width: var(--p-avatar-small);
  height: var(--p-avatar-small);
  font: var(--p-t-label);
  font-weight: 700;
}

/* ------------------------------------------------- shared: chips/pills --- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--p-s);
  padding: var(--p-chip-v) var(--p-chip-h);
  border-radius: 999px;
  border: var(--p-hairline-width) solid var(--p-hairline);
  background: var(--p-chip-fill);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  font: var(--p-t-label);
  color: var(--p-text-primary);
  white-space: nowrap;
}

/* LIVE NOW badge — Cameo's live-vs-scheduled contrast, on the app's
   liveRedDeep (the only red that carries small white text at AA). */
.chip--live {
  background: var(--p-live-red-deep);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.chip--offline {
  background: var(--p-chip-fill);
  color: var(--p-text-secondary);
}

.chip__dot {
  width: var(--p-live-dot);
  height: var(--p-live-dot);
  border-radius: 50%;
  background: currentColor;
  animation: p-breathe 1s ease-in-out infinite alternate;
}

@keyframes p-breathe {
  from { opacity: 0.45; transform: scale(0.85); }
  to   { opacity: 1;    transform: scale(1); }
}

/* ------------------------------------------------- shared: buttons ------- */

/* PresentPillButtonStyle: capsule, hairline stroke, floating shadow,
   0.96 press scale, 44pt minimum target. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--p-s);
  min-height: var(--p-tap);
  padding: calc(var(--p-pill-v) + 2px) var(--p-xl);
  border-radius: 999px;
  border: var(--p-hairline-width) solid var(--p-hairline);
  background: var(--p-pill-bg);
  color: var(--p-text-primary);
  font: var(--p-t-headline);
  box-shadow: var(--p-shadow-floating);
  transition: transform var(--p-quick), background var(--p-quick),
    opacity var(--p-quick);
  -webkit-tap-highlight-color: transparent;
  text-decoration: none;
}

.btn:active:not(:disabled) { transform: scale(0.96); }

.btn svg,
.linkish svg,
.chip svg {
  width: 20px;
  height: 20px;
  flex: none;
}

.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  box-shadow: none;
}

.btn--block { width: 100%; }
.btn--primary,
.btn--accent {
  background: var(--p-pill-ink);
  color: var(--p-pill-ink-label);
  border-color: transparent;
}
.btn--primary { font: var(--p-t-title); }

/* THE primary pill. Both of these were painted in live red, which is the
   same mistake the iOS accept button carried: red in PRESNT means LIVE, and
   spending it on "pay and join" or "get the app" is how it stops meaning
   that. Ink fill, paper label — and on camera the pair inverts to a near
   white fill with an ink label, so one class serves both surfaces. */

/* Quiet secondary under a loud primary (Monzo receiver grammar, ported from
   PresentKnockCardView's decline button). */
.btn--quiet {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--p-text-secondary);
  font: var(--p-t-body);
}

.btn--quiet:hover { color: var(--p-text-primary); }

.linkish {
  display: inline-flex;
  align-items: center;
  gap: var(--p-s);
  min-height: var(--p-tap);
  padding: 0;
  border: 0;
  background: none;
  color: var(--p-accent);
  font: var(--p-t-body);
  text-decoration: none;
}

.linkish:hover { text-decoration: underline; }

/* ============================================================================
   SCREEN 1 — PAY SHEET
   Rooted in:
     guest/cameo-ticket-what-you-get-flow.png  (host face + name as the
       headline, "Included with your ticket" checklist, price-as-CTA-label,
       refund microcopy under the button)
     guest/cameo-calls-live-now-join.png       (LIVE NOW badge + "$X for N mins")
     guest/cuvva-confirm-and-pay.png           (pinned footer: total on the left,
       one full-width Pay now pill)
     guest/moonpay-confirm-order-sheet.png     (single-item hero sentence; the
       disabled CTA carries its reason as its own label)
   ========================================================================== */

/* The footer is pinned to the viewport, so the scrolling column reserves
   exactly its measured height (--paybar-h, written by paysheet.js) plus
   breathing room. No magic number, and nothing can hide under the CTA. */
.pay > .wrapper {
  padding-bottom: calc(var(--paybar-h, 200px) + var(--p-l));
}

.pay__hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--p-m);
  padding: var(--p-l) 0 var(--p-xl);
  text-align: center;
}

.pay__name {
  margin: 0;
  font: var(--p-t-display);
  letter-spacing: -0.02em;
}

.pay__handle {
  margin: 0;
  font: var(--p-t-body);
  color: var(--p-text-secondary);
}

/* Cameo's headline sentence: "<Host> will be taking Cameo Calls". */
.pay__pitch {
  margin: var(--p-s) 0 0;
  font: var(--p-t-headline);
  color: var(--p-text-secondary);
  text-wrap: balance;
}

.card {
  background: var(--p-card);
  border: var(--p-hairline-width) solid var(--p-hairline);
  border-radius: var(--p-r-card);
  padding: var(--p-l) var(--p-l) calc(var(--p-l) + 2px);
}

.card + .card { margin-top: var(--p-m); }

.card__title {
  margin: 0 0 var(--p-m);
  font: var(--p-t-body);
  color: var(--p-text-secondary);
}

/* "Included with your call:" — Cameo's three ticks, verbatim grammar. */
.included {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--p-m);
}

.included li {
  display: grid;
  grid-template-columns: 22px 1fr;
  align-items: start;
  gap: var(--p-m);
  font: var(--p-t-body);
  font-weight: 500;
}

.included svg {
  width: 22px;
  height: 22px;
  /* Was highlight-yellow. A tick confirming "you get a 1-on-1 call" is not a
     warning and not live — it is body copy that happens to be a glyph. */
  color: var(--p-text-primary);
}

/* Guest name field. */
.field { display: grid; gap: var(--p-s); }

.field__label {
  font: var(--p-t-label);
  color: var(--p-text-secondary);
}

.field__input {
  width: 100%;
  min-height: var(--p-tap);
  padding: var(--p-m) var(--p-l);
  border-radius: 999px;
  border: var(--p-hairline-width) solid var(--p-hairline);
  background: var(--p-field-fill);
  color: var(--p-text-primary);
  font: var(--p-t-headline);
  /* 16px+ stops mobile Safari zooming the page on focus. */
  font-size: 17px;
}

.field__input::placeholder { color: var(--p-placeholder-text); }

.field__hint {
  font: var(--p-t-caption);
  color: var(--p-text-secondary);
}

/* Cuvva's pinned footer: the total and the CTA travel together, always in
   reach. Its height is measured back into --paybar-h so the column above can
   reserve exactly the right amount of room. */
.paybar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  padding: var(--p-m) 0 max(var(--p-l), env(safe-area-inset-bottom));
  /* Opaque under the footer's own text — content must never bleed through it —
     with the fade living entirely ABOVE the bar. */
  background: var(--p-background);
}

.paybar::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 32px;
  background: linear-gradient(to bottom, transparent, var(--p-background));
  pointer-events: none;
}

.paybar__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--p-m);
  padding-bottom: var(--p-s);
}

.paybar__label {
  font: var(--p-t-label);
  color: var(--p-text-secondary);
}

.paybar__total {
  font: var(--p-t-title);
  font-family: var(--p-font-rounded);
  letter-spacing: -0.02em;
}

/* Refund line, directly under the button (Cameo). */
.paybar__fineprint {
  margin: var(--p-s) 0 0;
  font: var(--p-t-caption);
  color: var(--p-text-secondary);
  text-align: center;
  text-wrap: balance;
}

/* Inline camera/mic check — a one-line link, never a blocking gate. */
.precheck {
  display: flex;
  align-items: center;
  gap: var(--p-m);
  margin-top: var(--p-m);
}

.precheck__preview {
  width: 56px;
  height: 56px;
  flex: none;
  border-radius: var(--p-r-thumb);
  object-fit: cover;
  background: var(--p-placeholder-dark);
  transform: scaleX(-1);
}

/* ============================================================================
   BANNERS — dead ends that must not destroy the page.
   Rooted in:
     guest/viator-payment-failed-inline.png (payment failure is an inline
       banner; the form, the price header and the steps stay intact)
     guest/ebay-no-longer-available.png     (a closed transaction is a line
       above a still-rendered identity, never a wiped screen)
   ========================================================================== */

.banner {
  display: grid;
  gap: var(--p-xs);
  margin: var(--p-m) 0;
  padding: var(--p-m) var(--p-l);
  border-radius: var(--p-r-card);
  border-left: var(--p-avatar-ring-width) solid var(--p-notice-rule-alert);
  background: var(--p-notice-fill);
  font: var(--p-t-body);
  font-weight: 500;
}

.banner__title { font-weight: 700; }
.banner__body { color: var(--p-text-secondary); font: var(--p-t-label); }

/* eBay grammar: the headline sits above the intact host card, unhighlighted. */
.banner--closed {
  border-left-color: var(--p-notice-rule);
  background: var(--p-notice-fill);
}

/* ============================================================================
   SCREEN 2 — REQUESTING / WAITING
   Rooted in:
     guest/zoom-waiting-room-admit.png     (a plain-language status strip at the
       top; the guest is never left staring at a still frame with no caption)
     guest/azar-searching-then-connect.png (the waiting state is NAMED and given
       one warm human sentence, over the guest's own camera, with a second door)
     guest/sesame-connecting-to-incall-minimal.png (the control row that will
       exist in-call is already on screen, so connecting does not reflow)
   ========================================================================== */

.stage {
  position: absolute;
  inset: 0;
  background: var(--p-surface);
  overflow: hidden;
}

.stage__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--p-surface);
}

.stage__video--self { transform: scaleX(-1); }

/* 40% dim over the live self-view behind the waiting copy (knockDim). */
.stage__dim {
  position: absolute;
  inset: 0;
  background: var(--p-knock-dim);
}

.stage__scrim {
  position: absolute;
  inset: auto 0 0;
  height: 260px;
  background: var(--p-bottom-scrim);
  pointer-events: none;
}

/* Zoom's status strip: same fact, plain language, top of the screen. */
.statusstrip {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 6;
  display: flex;
  justify-content: center;
  padding: max(var(--p-m), env(safe-area-inset-top)) var(--p-l) var(--p-m);
  pointer-events: none;
}

.statusstrip .chip {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Azar: the named waiting state + one human sentence, centered. */
.waiting {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--p-m);
  padding: var(--p-xl);
  text-align: center;
}

.waiting__avatar { margin-bottom: var(--p-s); }

.waiting__title {
  margin: 0;
  font: var(--p-t-display);
  letter-spacing: -0.02em;
  text-shadow: 0 2px 4px var(--p-text-shadow);
}

.waiting__sub {
  margin: 0;
  max-width: 30ch;
  font: var(--p-t-headline);
  font-weight: 500;
  color: var(--p-text-secondary);
  text-wrap: balance;
  text-shadow: 0 2px 4px var(--p-text-shadow);
}

.waiting__meta {
  margin: var(--p-s) 0 0;
  font: var(--p-t-label);
  color: var(--p-text-secondary);
}

/* Three quiet dots — Sesame's "no spinner theatre" rule, just enough motion
   to say the page is alive. */
.dots { display: inline-flex; gap: 5px; }

.dots span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  animation: p-dot 1.2s ease-in-out infinite;
}

.dots span:nth-child(2) { animation-delay: 0.15s; }
.dots span:nth-child(3) { animation-delay: 0.3s; }

@keyframes p-dot {
  0%, 60%, 100% { opacity: 0.25; }
  30%           { opacity: 1; }
}

/* ============================================================================
   SCREEN 3 — IN CALL
   Rooted in:
     guest/sesame-connecting-to-incall-minimal.png (radical subtraction: name
       pill, running timer, red end, two toggles — and the layout does not
       reflow between connecting and live, only the timer starts)
     guest/skype-call-ended-rate.png (labels UNDER the control glyphs — on a
       browser page nobody has learned our icon set)
   Plus the app's own ceremony: PresentCallView's countdown chip, the 60s rim
   flash (red -> purple) and the "+5 min · $5" extend pill.
   ========================================================================== */

/* Top-center countdown chip (PresentCallView.countdownChip). */
.hud-top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--p-m);
  padding: max(var(--p-l), env(safe-area-inset-top)) var(--p-l) 0;
  pointer-events: none;
}

.hud-top > * { pointer-events: auto; }

.countdown {
  font: var(--p-t-countdown);
  font-variant-numeric: tabular-nums;
  padding: var(--p-chip-v) var(--p-chip-h);
}

.countdown[data-low="true"] {
  color: var(--p-live-red);
  border-color: color-mix(in srgb, var(--p-live-red) 50%, transparent);
  animation: p-chip-pulse 1s ease-in-out infinite alternate;
}

@keyframes p-chip-pulse {
  from { transform: scale(1); }
  to   { transform: scale(1.05); }
}

.namepill { font-weight: 600; }

/* Self-view pill — same shape as the app's SelfViewPill, same corner. */
.selfview {
  position: absolute;
  top: var(--p-call-top-clearance);
  right: var(--p-card-edge);
  z-index: 6;
  width: var(--p-self-w);
  height: var(--p-self-h);
  border-radius: var(--p-r-self);
  overflow: hidden;
  border: var(--p-hairline-width) solid var(--p-hairline);
  background: linear-gradient(135deg, var(--p-self-bright), var(--p-self-dark));
  box-shadow: var(--p-shadow-floating);
  padding: 0;
  transition: top var(--p-spring), right var(--p-spring), left var(--p-spring),
    bottom var(--p-spring);
}

.selfview[data-corner="bottom"] {
  top: auto;
  bottom: var(--p-call-bottom-clearance);
}

.selfview video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);
}

.selfview[data-muted="true"]::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--p-knock-dim);
}

/* Bottom control row — one row, generous targets, labels under the glyphs. */
.controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 8;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: var(--p-xl);
  padding: var(--p-l) var(--p-l)
    max(var(--p-screen-bottom), calc(env(safe-area-inset-bottom) + var(--p-l)));
}

.ctrl {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--p-xs);
  min-width: var(--p-tap);
  padding: 0;
  border: 0;
  background: none;
  color: var(--p-text-primary);
  -webkit-tap-highlight-color: transparent;
}

.ctrl__glyph {
  display: grid;
  place-items: center;
  width: var(--p-end-call);
  height: var(--p-end-call);
  border-radius: 50%;
  background: var(--p-pill-bg);
  border: var(--p-hairline-width) solid var(--p-hairline);
  box-shadow: var(--p-shadow-floating);
  transition: transform var(--p-quick), background var(--p-quick);
}

.ctrl:active .ctrl__glyph { transform: scale(0.94); }
.ctrl__glyph svg { width: 24px; height: 24px; }

.ctrl__label {
  font: var(--p-t-caption);
  color: var(--p-text-secondary);
}

.ctrl--end .ctrl__glyph { background: var(--p-live-red-deep); }
.ctrl[aria-pressed="true"] .ctrl__glyph { background: var(--p-control-fill); }

/* The extend pill rides above the controls once the rim ignites. */
.extendbar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--p-call-bottom-clearance) + var(--p-s));
  z-index: 8;
  display: flex;
  justify-content: center;
  padding: 0 var(--p-l);
}

/* THE signature moment: a red -> purple rim that ignites at 60s remaining.
   PresentCallView.RimFlashOverlay, expressed as a masked conic border. */
.rim {
  position: absolute;
  inset: 0;
  z-index: 9;
  border-radius: var(--p-r-screen);
  padding: var(--p-ring-width);
  background: var(--p-rim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--p-fade);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
}

.rim[data-on="true"] {
  opacity: 0.9;
  animation: p-rim-sweep 6s linear infinite, p-rim-pulse 2s ease-in-out infinite;
}

/* Final ~10s: the pulse locks to the app's cardiac double-beat. */
.rim[data-beat="true"] {
  animation: p-rim-sweep 1.6s linear infinite, p-rim-heart 0.9s linear infinite;
}

@keyframes p-rim-sweep {
  to { filter: hue-rotate(360deg); }
}

@keyframes p-rim-pulse {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 1; }
}

@keyframes p-rim-heart {
  0%   { opacity: 1; }
  12%  { opacity: 0.45; }
  38%  { opacity: 0.9; }
  55%  { opacity: 0.35; }
  100% { opacity: 0.35; }
}

/* Audio-unlock prompt (mobile Safari will not autoplay remote audio). */
.audiounlock {
  position: absolute;
  left: 50%;
  bottom: calc(var(--p-call-bottom-clearance) + 64px);
  transform: translateX(-50%);
  z-index: 9;
}

/* ============================================================================
   SCREEN 4 — WRAP
   Rooted in:
     guest/skype-call-ended-rate.png   (the call does not cut to a new visual
       world: same field, host's face still there, "Call Ended" + the duration)
     guest/shopee-live-ended-summary.png (the forward action card comes BEFORE
       the numbers — the second after it ends is peak intent)
   ========================================================================== */

.wrap {
  position: relative;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--p-l);
  min-height: 100vh;
  min-height: 100svh;
  padding: var(--p-xl) var(--p-card-edge)
    max(var(--p-screen-bottom), env(safe-area-inset-bottom));
  text-align: center;
}

.wrap__badge {
  background: var(--p-pill-ink);
  color: var(--p-pill-ink-label);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.wrap__duration {
  margin: 0;
  font-family: var(--p-font-rounded);
  font-size: var(--p-hero-money);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.wrap__thanks {
  margin: 0;
  max-width: 28ch;
  font: var(--p-t-headline);
  font-weight: 500;
  color: var(--p-text-secondary);
  text-wrap: balance;
}

/* Shopee's nudge card: the forward CTA, above any summary numbers. */
.wrap__cta {
  width: 100%;
  max-width: 420px;
  display: grid;
  gap: var(--p-m);
  margin-top: var(--p-s);
}

.wrap__summary {
  margin: 0;
  font: var(--p-t-label);
  color: var(--p-text-secondary);
}

/* ============================================================================
   SCREEN 5 — DEAD ENDS (full-page variant)
   Rooted in:
     guest/cuvva-quote-expired-recover.png (illustration, a headline naming the
       exact thing that failed, one plain sentence of why, a bolded "Our top
       tip", then recovery-primary over exit-secondary — and never an apology)
   ========================================================================== */

.dead {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--p-l);
  min-height: 100vh;
  min-height: 100svh;
  padding: var(--p-xl) var(--p-card-edge)
    max(var(--p-screen-bottom), env(safe-area-inset-bottom));
  text-align: center;
}

/* Cuvva's illustration panel, in the app's own rim gradient. */
.dead__art {
  width: 100%;
  max-width: 360px;
  aspect-ratio: 16 / 9;
  border-radius: var(--p-r-card);
  background:
    radial-gradient(120% 140% at 20% 0%, rgba(10, 10, 12, 0.14), transparent 60%),
    radial-gradient(120% 140% at 100% 100%, rgba(10, 10, 12, 0.07), transparent 62%),
    var(--p-card);
  border: var(--p-hairline-width) solid var(--p-hairline);
  display: grid;
  place-items: center;
}

.dead__art svg { width: 84px; height: 84px; color: var(--p-text-primary); }

.dead__title {
  margin: 0;
  font: var(--p-t-display);
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.dead__why {
  margin: 0;
  max-width: 34ch;
  font: var(--p-t-headline);
  font-weight: 500;
  color: var(--p-text-secondary);
  text-wrap: balance;
}

.dead__tip {
  margin: 0;
  max-width: 34ch;
  font: var(--p-t-body);
  font-weight: 500;
  color: var(--p-text-secondary);
  text-wrap: balance;
}

.dead__tip b { color: var(--p-text-primary); }

.dead__actions {
  width: 100%;
  max-width: 420px;
  display: grid;
  gap: var(--p-s);
  margin-top: var(--p-s);
}

/* Browser-specific permission instructions. */
.steps {
  margin: 0;
  padding-left: 1.25em;
  max-width: 34ch;
  text-align: left;
  font: var(--p-t-label);
  color: var(--p-text-secondary);
}

.steps li + li { margin-top: var(--p-xs); }

/* ------------------------------------------------------------- utility --- */

.center { text-align: center; }
.stack { display: grid; gap: var(--p-m); }
.mt-l { margin-top: var(--p-l); }

.spinner {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid var(--p-stroke-soft);
  border-top-color: var(--p-text-primary);
  animation: p-spin 0.8s linear infinite;
}

@keyframes p-spin { to { transform: rotate(360deg); } }

/* ------------------------------------------------- motion & contrast ----- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* The rim still marks low time — it just stops moving. */
  .rim[data-on="true"] { opacity: 0.75; }
}

@media (prefers-contrast: more) {
  /* Paper surfaces darken their ink; media surfaces brighten their white.
     Raising only the white set left every paper screen untouched, which is
     the surface a low-vision guest is most likely to be reading. */
  :root {
    --p-text-secondary: rgba(10, 10, 12, 0.92);
    --p-hairline: rgba(10, 10, 12, 0.4);
  }
  .on-media {
    --p-text-secondary: rgba(255, 255, 255, 0.92);
    --p-hairline: rgba(255, 255, 255, 0.4);
  }
}

/* Wider phones / laptops: the camera surfaces become a phone-shaped column
   instead of stretching into a desktop letterbox. Every overlay inside them is
   absolutely positioned against `.screen`, so constraining the screen is all it
   takes — the countdown, self-view, rim and controls follow automatically. */
@media (min-width: 720px) {
  /*
   * `.pay` WAS MISSING FROM THIS LIST, and it is the screen that takes money.
   *
   * The call, waiting and wrap surfaces have been constrained to a phone-shaped
   * column on desktop since this rule was written; the pay sheet is
   * `screen pay` (and `screen pay pay--poster` when the host is live), carries
   * neither `--fixed` nor `--stage`, and so stretched to the full window. On a
   * laptop that made "Ask to join" a 1400px pill and ran the price rule from
   * one edge of the screen to the other.
   *
   * A class list is a bad place to keep a rule, because adding a surface does
   * not add it here — which is exactly what happened. The pinned footer below
   * is constrained the same way for the same reason.
   */
  .screen--fixed,
  .screen--stage,
  .pay {
    max-width: 460px;
    margin-inline: auto;
    box-shadow: 0 0 0 1px var(--p-hairline);
  }

  .screen--fixed .stage,
  .screen--stage .stage,
  .screen--fixed .rim {
    border-radius: var(--p-r-screen);
  }

  /*
   * Both of these are `position: fixed`, so they are laid out against the
   * VIEWPORT and inherit nothing from the column above. Left alone they stay
   * full width while everything else narrows, which reads as MORE broken than
   * the stretched version did — because then the page visibly disagrees with
   * itself.
   *
   * A `left: 50%` + translate would be the reflex, and it is wrong twice here.
   * `.pay--poster { inset: 0 }` is defined further down this file at equal
   * specificity, so the `left` loses to it while the TRANSFORM still applies —
   * which drags an already-correct box 230px off centre. And the centring is
   * already free: `left: 0; right: 0` gives a full-width containing box, so
   * `max-width` plus auto margins centres a fixed element exactly like a
   * static one. Found by reading the computed style rather than the source.
   */
  .paybar,
  .pay--poster {
    max-width: 460px;
    margin-inline: auto;
  }
}

/* ==========================================================================
   POSTER — the pay sheet when the host is live.

   A poster for something happening now, not a checkout. The handle carries
   display scale, one ruled line states the terms, and the guest's own camera
   is the ground behind it — blurred, so it sets the room without competing
   with the type. Their face stays in the corner self-view, which is the same
   object and the same position the pill occupies once the call starts.

   Type is the system stack, deliberately: this page is where the money
   happens and it must render from one round trip, so it does not wait on a
   webfont. Weight and tracking do the work instead.
   ========================================================================== */

.pay--poster {
  position: fixed;
  inset: 0;
  display: block;
  padding: 0;
  overflow: hidden;
  background: var(--p-surface);
}

.pay--poster .precheck__preview,
.pay--poster .mirror__fallback {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.pay--poster .precheck__preview {
  display: block;
  object-fit: cover;
  /* Wallpaper, not a mirror: blurred and pushed back so the type wins. The
     scale keeps the blur from showing bare edges. */
  transform: scaleX(-1) scale(1.12);
  filter: blur(18px) saturate(0.75) brightness(0.7);
  background: var(--p-placeholder-dark);
}

.mirror__fallback {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--p-m);
  text-align: center;
  padding: var(--p-xl);
  background:
    radial-gradient(90% 60% at 50% 34%, rgba(10, 10, 12, 0.10), transparent 70%),
    var(--p-background);
}
.mirror__why { margin: 0; color: var(--p-text-primary); font-weight: 600; }
.mirror__hint {
  margin: 0; color: var(--p-text-secondary);
  font-size: 0.86rem; max-width: 28ch;
}

.poster__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(6, 6, 10, 0.55) 0%,
    rgba(6, 6, 10, 0.35) 40%,
    rgba(6, 6, 10, 0.88) 100%
  );
}

/* The corner self-view. Dimensions are PresentTheme.Sizes.selfViewPill
   (96x138) and Radii.selfPill (22) exactly — this pill is continuous into the
   call, so a near-miss would visibly jump at the highest-stakes moment. */
.poster__self {
  position: absolute;
  top: max(var(--p-l), env(safe-area-inset-top));
  right: var(--p-l);
  width: 96px;
  height: 138px;
  border-radius: 22px;
  overflow: hidden;
  border: 1px solid var(--p-hairline);
  box-shadow: 0 6px 12px var(--p-pill-shadow);
  background: var(--p-self-dark);
}
.poster__self .precheck__preview {
  position: static;
  transform: scaleX(-1);
  filter: none;
  width: 100%;
  height: 100%;
}

.banner--float {
  position: absolute;
  left: var(--p-l); right: var(--p-l);
  top: max(var(--p-l), env(safe-area-inset-top));
  z-index: 3;
}

.poster__foot {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: var(--p-l) var(--p-l) max(var(--p-l), env(safe-area-inset-bottom));
  display: grid;
  gap: var(--p-m);
}

.poster__eyebrow { margin: 0; }

.poster__id {
  display: flex;
  align-items: center;
  gap: 14px;
}

/*
 * Sized off the handle rather than fixed, so the two stay in proportion when
 * the type scales with the viewport. Slightly smaller than the cap height —
 * a face that out-measures the name reads as the wrong subject.
 */
.poster__avatar {
  width: clamp(44px, 11vw, 58px);
  height: clamp(44px, 11vw, 58px);
  font-size: clamp(1rem, 4.5vw, 1.3rem);
  flex: none;
  /* Over camera the fallback is a translucent wash, which the poster's own
     bottom scrim can swallow. A hairline keeps the shape when the disc alone
     would not — the same problem the connecting screen had. */
  box-shadow: 0 0 0 1px var(--p-stroke-soft), var(--p-shadow-floating);
}

.poster__handle {
  margin: 0;
  font-size: clamp(2.4rem, 13vw, 3.4rem);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 0.92;
  text-shadow: 0 3px 24px rgba(0, 0, 0, 0.7);
}

.poster__line {
  display: flex;
  align-items: baseline;
  gap: var(--p-s);
  padding-block: var(--p-m);
  border-top: 1px solid var(--p-stroke-soft);
  border-bottom: 1px solid var(--p-stroke-soft);
  font-variant-numeric: tabular-nums;
}
.poster__price { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.03em; }
.poster__sep { color: var(--p-text-secondary); }
.poster__len { font-size: 1.05rem; font-weight: 700; }
.poster__one {
  margin-left: auto;
  color: var(--p-text-secondary);
  font-size: 0.86rem;
}

.pay--poster .field__input {
  background: var(--p-pill-bg);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}
.pay--poster .paybar__fineprint { text-align: center; margin: 0; }

/* ---------------------------------------------------- wrap: the report --- */

/* The money is the headline. Duration was the largest element here; it is
   what we measured, not what someone who just paid cares about. */
.wrap__paid {
  margin: var(--p-s) 0 0;
  font-size: clamp(2.4rem, 12vw, 3.2rem);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.wrap__detail {
  margin: var(--p-s) 0 var(--p-xl);
  color: var(--p-text-secondary);
}

/* Quiet by construction: most calls are fine, and a wrap screen that leads
   with "report a problem" implies otherwise. */
.wrap__report { margin-top: var(--p-l); width: 100%; }
.wrap__reportOpen {
  color: var(--p-text-secondary);
  font-size: 0.88rem;
}
.wrap__reportOpen:hover { color: var(--p-text-primary); }

.wrap__reportPanel {
  display: grid;
  gap: var(--p-m);
  text-align: left;
  margin-top: var(--p-m);
}
.wrap__reportHint {
  margin: 0;
  font-size: 0.78rem;
  color: var(--p-text-secondary);
}
.wrap__reportText {
  width: 100%;
  font: inherit;
  color: var(--p-text-primary);
  padding: var(--p-m);
  border-radius: var(--p-r-thumb);
  border: 1px solid var(--p-hairline);
  background: var(--p-pill-bg);
  resize: vertical;
}
.wrap__reportText:focus { outline: none; border-color: var(--p-stroke-soft); }
.wrap__reportStatus {
  margin: var(--p-m) 0 0;
  font-size: 0.84rem;
  color: var(--p-text-secondary);
}

/* TEST MODE — the backend is running simulated payments.
   Loud on purpose and outside the screen stack, so it survives navigation and
   sits above both surfaces (paper AND on-camera) without either theme being
   able to tone it down. This is the one place in PRESNT where an alarming
   colour is correct: it is not a live indicator, it is a warning that the
   money on screen is not real. */
.testmode {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 999;
  padding: 6px 12px calc(6px + env(safe-area-inset-top));
  background: var(--p-testmode-bg);
  color: var(--p-testmode-label);
  font: var(--p-t-caption);
  font-weight: 700;
  letter-spacing: .04em;
  text-align: center;
  text-transform: uppercase;
  pointer-events: none;
}
/* Push every screen clear of it rather than letting it cover the first row. */
body[data-test-mode='true'] #app { padding-top: 28px; }

/* Terms and Privacy under the pay button. Quiet on purpose: they must be
   findable before somebody pays, and they must not compete with the price or
   the CTA — a disclosure that shouts is a disclosure people click away from. */
.paybar__legal {
  margin-top: 6px;
  text-align: center;
  font: var(--p-t-caption);
  color: var(--p-ink-muted);
}
.paybar__legalLink {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
