/* ═══════════════════════════════════════════════════════════════════════════
   HEART Rally — participant app
   Palette and mood taken from the event poster: magenta → crimson → orange
   with a radiant gold core.

   Performance rule for this file: only `transform` and `opacity` are ever
   animated. No animated blur, filter, box-shadow or width — those are what
   make gradient-heavy pages stutter on mid-range Android phones.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Tokens ───────────────────────────────────────────────────────────────── */
:root {
  --c-magenta: #b0186a;
  --c-crimson: #e01b3c;
  --c-red:     #f0322a;
  --c-orange:  #ff6b1f;
  --c-amber:   #ffa415;
  --c-gold:    #ffd97a;
  --c-cream:   #fff6ec;
  --c-ink:     #2a0512;

  --text:        #fff6ec;
  --text-dim:    rgba(255, 246, 236, 0.74);
  --text-faint:  rgba(255, 246, 236, 0.66);

  --surface:        rgba(46, 6, 20, 0.34);
  --surface-strong: rgba(40, 5, 17, 0.52);
  --surface-solid:  rgba(38, 5, 16, 0.78);
  --border:         rgba(255, 217, 122, 0.26);
  --border-strong:  rgba(255, 217, 122, 0.52);

  --ok:    #7ee0a8;
  --warn:  #ffd97a;
  --error: #ff9d8a;

  --r-sm: 12px;
  --r-md: 18px;
  --r-lg: 26px;
  --r-full: 999px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 180ms;
  --dur: 320ms;
  --dur-slow: 460ms;

  --shell: 480px;
  --pad: 20px;

  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

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

/* The document scrolls, not the body.
   `height: 100%` together with `overflow-x: hidden` makes the body a fixed-height
   scroll container (the spec turns the other axis into `auto`), which pins
   `position: sticky` to a nested scroller instead of the viewport — and iOS
   Safari mis-places sticky elements in exactly that arrangement as the address
   bar grows and shrinks. That was behind the header overlapping long content. */
html {
  overflow-x: hidden;
}

body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  font-family: 'Plus Jakarta Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.55;
  color: var(--text);
  background: var(--c-ink);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  text-rendering: optimizeLegibility;
  overscroll-behavior-y: none;
}

button, input, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; border: 0; background: none; }
img { max-width: 100%; display: block; }
:focus-visible { outline: 2px solid var(--c-gold); outline-offset: 3px; border-radius: 6px; }
::selection { background: var(--c-gold); color: var(--c-ink); }

/* ── Ambient background ───────────────────────────────────────────────────── */
/* Three fixed layers. The two blobs drift on transform only; the grain and the
   vignette are static, so the compositor has nothing to repaint. */
.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background:
    radial-gradient(70% 42% at 74% 30%, var(--c-amber) 0%, transparent 60%),
    radial-gradient(95% 60% at 44% 46%, var(--c-red) 0%, transparent 66%),
    linear-gradient(158deg, var(--c-crimson) 0%, var(--c-red) 30%, var(--c-orange) 56%, var(--c-magenta) 100%);
}

.bg__blob {
  position: absolute;
  width: 130vmax;
  height: 130vmax;
  border-radius: 50%;
  opacity: 0.42;
  will-change: transform;
}

.bg__blob--gold {
  top: -35vmax;
  left: -8vmax;
  background: radial-gradient(circle at 50% 50%, rgba(255, 217, 122, 0.85) 0%, rgba(255, 164, 21, 0.3) 34%, transparent 62%);
  animation: drift-a 34s var(--ease) infinite alternate;
}

.bg__blob--magenta {
  bottom: -70vmax;
  right: -35vmax;
  background: radial-gradient(circle at 50% 50%, rgba(176, 24, 106, 0.9) 0%, rgba(176, 24, 106, 0.28) 40%, transparent 66%);
  animation: drift-b 42s var(--ease) infinite alternate;
}

/* A whisper of texture so the gradient reads like the poster's silk, not a
   flat CSS ramp. Static PNG noise, no runtime cost. */
.bg__grain {
  position: absolute;
  inset: -50%;
  opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

.bg__vignette {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom, rgba(28, 3, 12, 0.42) 0%, transparent 22%),
    radial-gradient(120% 100% at 50% 42%, transparent 36%, rgba(28, 3, 12, 0.62) 100%);
}

@keyframes drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(14vmax, 9vmax, 0) scale(1.16); }
}
@keyframes drift-b {
  from { transform: translate3d(0, 0, 0) scale(1.1); }
  to   { transform: translate3d(-12vmax, -10vmax, 0) scale(0.92); }
}

/* ── Shell ────────────────────────────────────────────────────────────────── */
.shell {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  max-width: var(--shell);
  margin-inline: auto;
  padding-bottom: calc(var(--safe-bottom) + 8px);
}

/* ── Top bar ──────────────────────────────────────────────────────────────── */
/* The bar's own gradient fades to fully transparent, so content would slide
   visibly through it; the opaque layer below is what actually hides it once
   there is something to hide. */
.chrome {
  position: sticky;
  top: 0;
  z-index: 20;
  /* At rest the bar is a light veil, so the poster gradient still reads through
     it. The opaque layer below only appears once there is content to hide. */
  background: linear-gradient(to bottom, rgba(30, 4, 13, 0.55), rgba(30, 4, 13, 0));
  /* The fade needs somewhere to happen, below the bar. */
  padding-bottom: 10px;
}

/* Solid backing, faded in on scroll. A translucent bar let text ghost through
   and left content half-visible under its soft edge; a permanently opaque one
   would hide the gradient the design is built on. Opacity animates on the
   compositor, which `background-image` would not. */
.chrome::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgb(28, 4, 12) 0%,
    rgb(28, 4, 12) 82%,
    rgba(28, 4, 12, 0.5) 93%,
    rgba(28, 4, 12, 0) 100%
  );
  opacity: 0;
  transition: opacity 200ms var(--ease);
  pointer-events: none;
}

.chrome[data-scrolled='1']::before { opacity: 1; }

/* Keep the bar above that backing. */
.chrome > * { position: relative; }

.topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: calc(var(--safe-top) + 12px) var(--pad) 12px;
}

.topbar__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-right: auto;
  min-width: 0;
}

.topbar__mark { width: 30px; height: 30px; flex: none; object-fit: contain; }

.topbar__word {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  line-height: 1;
}

.langtoggle {
  display: flex;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  background: var(--surface);
}

.langtoggle button {
  padding: 4px 11px;
  border-radius: var(--r-full);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}

.langtoggle button[aria-pressed='true'] {
  background: var(--c-gold);
  color: var(--c-ink);
}

/* ── Screens ──────────────────────────────────────────────────────────────── */
.screen {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 2px var(--pad) 28px;
  animation: screen-in var(--dur) var(--ease) both;
}

@keyframes screen-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}

.screen--center { justify-content: center; text-align: center; }

/* Children fade in one after another. Set --i on each to stagger. */
.stagger > * {
  animation: rise var(--dur-slow) var(--ease) both;
  animation-delay: calc(var(--i, 0) * 70ms);
}

@keyframes rise {
  from { opacity: 0; transform: translate3d(0, 12px, 0); }
  to   { opacity: 1; transform: none; }
}

/* ── Type ─────────────────────────────────────────────────────────────────── */
.display {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 300;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  font-size: clamp(42px, 15vw, 62px);
  line-height: 1;
  margin: 0;
}

.h1 {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 500;
  font-size: clamp(27px, 7.4vw, 34px);
  line-height: 1.15;
  margin: 0 0 6px;
}

.h2 {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 6px;
}

.muted { color: var(--text-dim); }
.faint { color: var(--text-faint); font-size: 13px; }
.serif-italic { font-family: 'Cormorant Garamond', Georgia, serif; font-style: italic; font-size: 18px; }

/* ── Cards ────────────────────────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 20px;
}

.card--strong { background: var(--surface-strong); }
.card + .card { margin-top: 12px; }

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  min-height: 54px;
  padding: 14px 22px;
  border-radius: var(--r-full);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-align: center;
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}

.btn:active:not(:disabled) { transform: scale(0.97); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

.btn--primary {
  background: linear-gradient(135deg, var(--c-gold), var(--c-amber));
  color: #40130a;
  box-shadow: 0 10px 26px rgba(120, 20, 10, 0.34);
}

.btn--ghost {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
}

.btn--quiet {
  min-height: 44px;
  width: auto;
  padding: 8px 16px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-dim);
}

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

.btn-row { display: flex; gap: 10px; margin-top: 14px; }
.btn-row .btn { flex: 1; }

/* The one-way submit. Once it latches it fills left to right for as long as the
   upload runs, so a second tap is both impossible and obviously unnecessary.
   The sweep is a transform on a pseudo-element, never an animated width. */
.btn--fill { position: relative; overflow: hidden; }

.btn--fill::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(64, 19, 10, 0.22);
  transform: scaleX(0);
  transform-origin: left;
}

.btn--fill[data-busy='1']::before { animation: fill-run 9s cubic-bezier(0.16, 0.9, 0.3, 1) forwards; }
.btn--fill .btn__label { position: relative; }

/* Deliberately never reaches 1: it is honest about waiting on the network, and
   the screen is replaced the moment the upload lands. */
@keyframes fill-run {
  0%   { transform: scaleX(0); }
  100% { transform: scaleX(0.94); }
}

/* ── Forms ────────────────────────────────────────────────────────────────── */
.field { margin-bottom: 16px; text-align: left; }

.input, .textarea, .select {
  width: 100%;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-strong);
  color: var(--text);
  transition: border-color var(--dur-fast) var(--ease);
  /* 16px minimum stops iOS Safari from zooming the viewport on focus. */
  font-size: 16px;
}

.textarea { min-height: 132px; resize: vertical; line-height: 1.5; }
.input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--border-strong); }
.input::placeholder, .textarea::placeholder { color: var(--text-faint); }

.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' fill='none'%3E%3Cpath d='M1 1l6 6 6-6' stroke='%23ffd97a' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-right: 42px;
}

.select option { background: #3a0a19; color: var(--c-cream); }

.checkbox {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  cursor: pointer;
  text-align: left;
}

.checkbox input {
  appearance: none;
  flex: none;
  width: 22px;
  height: 22px;
  margin-top: 1px;
  border: 2px solid var(--border-strong);
  border-radius: 7px;
  transition: background-color var(--dur-fast) var(--ease);
}

.checkbox input:checked {
  background-color: var(--c-gold);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='11' fill='none'%3E%3Cpath d='M1 5.5l4 4 8-8' stroke='%232a0512' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  border-color: var(--c-gold);
}

.checkbox__text { font-size: 14px; line-height: 1.45; }


/* ── Tag chip ─────────────────────────────────────────────────────────────── */
/* The account to tag, pulled out of the sentence so it can be tapped to copy. */
.tagchip {
  display: flex;
  align-items: center;
  gap: 13px;
  width: 100%;
  padding: 14px 16px;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--r-md);
  background: rgba(255, 217, 122, 0.1);
  text-align: left;
  transition: transform var(--dur-fast) var(--ease), background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

.tagchip:active { transform: scale(0.985); }

.tagchip__handle {
  display: block;
  font-size: 16.5px;
  font-weight: 700;
  line-height: 1.25;
  word-break: break-word;
}

.tagchip__hint { display: block; font-size: 12px; color: var(--text-faint); margin-top: 3px; }
.tagchip__icon { flex: none; color: var(--c-gold); }

.tagchip[data-copied='1'] {
  border-color: rgba(126, 224, 168, 0.75);
  background: rgba(126, 224, 168, 0.14);
}

.tagchip[data-copied='1'] .tagchip__icon,
.tagchip[data-copied='1'] .tagchip__hint { color: var(--ok); }

/* ── Notices ──────────────────────────────────────────────────────────────── */
.notice {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 12px 14px;
  border-radius: var(--r-md);
  font-size: 14px;
  line-height: 1.45;
  border: 1px solid var(--border);
  background: var(--surface-strong);
  animation: rise var(--dur) var(--ease) both;
}

.notice--error { border-color: rgba(255, 157, 138, 0.5); color: var(--error); }
.notice--ok { border-color: rgba(126, 224, 168, 0.45); color: var(--ok); }
.notice__icon { flex: none; font-size: 16px; line-height: 1.4; }

/* ── The stamp card ───────────────────────────────────────────────────────── */
/* The whole rally on one screen. Five slots to tap, a HEART stamp in each one
   that is finished, and — once they all are — the card folds its stamps into a
   row and the final quest opens beneath it. */
.cardwrap {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 1;
  gap: 14px;
  padding: 8px 0 4px;
}

.stampcard {
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface);
  padding: 16px;
}

/* Three then two, centred — the shape of the printed card. */
.stampcard__grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}

.slot {
  position: relative;
  flex: 0 0 calc(33.333% - 8px);
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 8px 6px;
  overflow: hidden;
  border: 1.5px dashed var(--border);
  border-radius: var(--r-md);
  background: rgba(40, 5, 17, 0.28);
  text-align: center;
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur) var(--ease);
}

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

.slot__num {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 24px;
  font-weight: 600;
  line-height: 1;
  color: var(--c-gold);
  /* Lining figures: the default old-style set renders "1" as a near-"I". */
  font-variant-numeric: lining-nums;
  font-feature-settings: 'lnum' 1;
}

/* A whisper of a label — the quest's own screen says where to go. Three lines
   at this size is what the longest station name ("Kapel St. Claude la
   Colombiere") needs, and it still leaves the slot square: the clamp is what
   stops a longer one from stretching the grid rather than being trimmed. */
.slot__title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 100%;
  font-size: 8.8px;
  line-height: 1.28;
  letter-spacing: 0.02em;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

/* An open slot breathes, so a card of five reads as five things to do. */
.slot[data-state='open'] { border-style: solid; border-color: var(--border-strong); }

.slot[data-state='open']::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: inherit;
  border: 1.5px solid var(--c-gold);
  animation: breathe 2.8s var(--ease) infinite;
  pointer-events: none;
}

@keyframes breathe {
  0%, 100% { transform: scale(0.97); opacity: 0.6; }
  50%      { transform: scale(1.04); opacity: 0; }
}

/* A stamped slot: the mark sits over the number, which stays underneath as the
   printed card's own numbering does. */
.slot[data-state='done'],
.slot[data-state='skipped'] {
  border-style: solid;
  border-color: var(--border-strong);
  background: rgba(255, 217, 122, 0.08);
}

.slot__stamp {
  position: absolute;
  inset: 12%;
  width: 76%;
  height: 76%;
  object-fit: contain;
  transform: rotate(-8deg);
}

.slot[data-state='skipped'] .slot__stamp { opacity: 0.55; }
.slot[data-state='done'] .slot__num,
.slot[data-state='skipped'] .slot__num,
.slot[data-state='done'] .slot__title,
.slot[data-state='skipped'] .slot__title { opacity: 0.22; }

/* The stamp that just landed thumps down rather than appearing. */
.slot[data-fresh='1'] .slot__stamp { animation: stamp-down 620ms var(--ease) both; }

@keyframes stamp-down {
  0%   { transform: rotate(-26deg) scale(2.4); opacity: 0; }
  55%  { transform: rotate(-4deg) scale(0.92); opacity: 1; }
  100% { transform: rotate(-8deg) scale(1); opacity: 1; }
}

/* ── Folded: the five stamps in a row, and the final quest ────────────────── */
.stampcard[data-folded='1'] {
  display: flex;
  justify-content: center;
  gap: 8px;
  padding: 12px;
}

.chip {
  flex: 0 0 auto;
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: rgba(255, 217, 122, 0.08);
}

.chip img { width: 78%; height: 78%; object-fit: contain; }
.chip[data-state='skipped'] img { opacity: 0.55; }

.stampcard[data-reveal='1'] .chip {
  animation: chip-in var(--dur-slow) var(--ease) both;
  animation-delay: calc(var(--i, 0) * 60ms);
}

.stampcard[data-reveal='1'] .chip:nth-child(1) { --i: 0; }
.stampcard[data-reveal='1'] .chip:nth-child(2) { --i: 1; }
.stampcard[data-reveal='1'] .chip:nth-child(3) { --i: 2; }
.stampcard[data-reveal='1'] .chip:nth-child(4) { --i: 3; }
.stampcard[data-reveal='1'] .chip:nth-child(5) { --i: 4; }
.stampcard[data-reveal='1'] .chip:nth-child(6) { --i: 5; }

@keyframes chip-in {
  from { opacity: 0; transform: translate3d(0, 10px, 0) scale(0.86); }
  to   { opacity: 1; transform: none; }
}

.finalquest {
  display: grid;
  place-content: center;
  gap: 6px;
  min-height: 168px;
  padding: 20px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface-strong);
  text-align: center;
  transition: transform var(--dur-fast) var(--ease);
}

.finalquest:active:not(:disabled) { transform: scale(0.985); }
.finalquest:disabled { cursor: default; }

.finalquest__label {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: clamp(28px, 9vw, 38px);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  line-height: 1.05;
  color: var(--c-gold);
}

.finalquest__hint { font-size: 12px; color: var(--text-faint); }

/* The panel used to be a slab of gold lettering and nothing else, and it read as
   a banner announcing the final quest rather than the way into it — people sat
   in front of it waiting. The pill is the affordance: the same gradient as every
   other primary button in the app, so it is recognisably the thing to press. */
.finalquest__cta {
  justify-self: center;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  padding: 12px 24px;
  border-radius: var(--r-full);
  background: linear-gradient(135deg, var(--c-gold), var(--c-amber));
  color: #40130a;
  font-size: 15px;
  font-weight: 700;
  box-shadow: 0 10px 24px rgba(120, 20, 10, 0.34);
}

/* Locked, it is the shut door at the bottom of the card: visible, unmistakably
   not ready. Open, it is the only bright thing on the screen. */
.finalquest[data-state='locked'] {
  border-style: dashed;
  border-color: var(--border);
  background: rgba(40, 5, 17, 0.28);
}

.finalquest[data-state='locked'] .finalquest__label { color: var(--text-faint); opacity: 0.5; }

.finalquest[data-state='open'] {
  position: relative;
  border-color: var(--c-gold);
  background:
    linear-gradient(160deg, rgba(255, 217, 122, 0.15), rgba(255, 164, 21, 0.05)),
    var(--surface-strong);
  box-shadow: 0 14px 34px rgba(120, 20, 10, 0.3);
}

/* The same breath an open slot takes, so the card's one remaining action looks
   like the card's other actions. */
.finalquest[data-state='open']::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: inherit;
  border: 1.5px solid var(--c-gold);
  animation: breathe 2.8s var(--ease) infinite;
  pointer-events: none;
}

.finalquest[data-state='open'] .finalquest__label {
  background: linear-gradient(100deg, var(--c-gold), var(--c-cream) 46%, var(--c-amber));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.finalquest[data-reveal='1'] { animation: final-open 720ms var(--ease) 260ms both; }

@keyframes final-open {
  0%   { opacity: 0; transform: scale(0.9); }
  60%  { opacity: 1; transform: scale(1.03); }
  100% { opacity: 1; transform: scale(1); }
}

/* ── Inside a quest ───────────────────────────────────────────────────────── */
/* One row: the way back, and the errand. Everything a quest screen says about
   itself is on this line. */
.questhead {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 12px;
  min-height: 40px;
}

.questhead .btn--quiet {
  flex: none;
  width: 40px;
  padding: 0;
  margin-left: -8px;
  font-size: 20px;
  line-height: 1;
}

.task {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
}

/* ── Quiz ─────────────────────────────────────────────────────────────────── */
.qcount { margin: 0; font-size: 12px; font-weight: 700; letter-spacing: 0.16em; color: var(--c-gold); font-variant-numeric: lining-nums; }
.qprompt { font-size: 19px; line-height: 1.42; font-weight: 600; margin: 0 0 4px; }

.qhint {
  display: flex;
  gap: 8px;
  font-size: 13.5px;
  color: var(--text-dim);
  font-style: italic;
  margin-bottom: 16px;
}

.options { display: grid; gap: 10px; }

.option {
  position: relative;
  display: flex;
  gap: 13px;
  align-items: center;
  width: 100%;
  padding: 15px 17px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-strong);
  text-align: left;
  font-size: 15px;
  line-height: 1.4;
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}

.option:active:not(:disabled) { transform: scale(0.985); }

.option__key {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  font-size: 13px;
  font-weight: 700;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.option[data-result='correct'] {
  border-color: var(--c-gold);
  background: rgba(255, 217, 122, 0.16);
}

.option[data-result='correct'] .option__key {
  background: var(--c-gold);
  border-color: var(--c-gold);
  color: #40130a;
}

/* The gold ring that snaps outward when an answer lands. */
.option[data-result='correct']::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  border: 2px solid var(--c-gold);
  animation: ring-out 620ms var(--ease) forwards;
  pointer-events: none;
}

@keyframes ring-out {
  from { transform: scale(1); opacity: 0.9; }
  to   { transform: scale(1.06); opacity: 0; }
}

.option[data-result='wrong'] {
  border-color: rgba(255, 157, 138, 0.55);
  animation: shake 420ms var(--ease);
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%  { transform: translateX(-7px); }
  40%  { transform: translateX(6px); }
  60%  { transform: translateX(-4px); }
  80%  { transform: translateX(2px); }
}

.option:disabled { cursor: default; }
.option[data-dim='1'] { opacity: 0.45; }

/* ── Camera ───────────────────────────────────────────────────────────────── */
.camera {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: rgba(24, 3, 10, 0.72);
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
}

.camera video, .camera img, .camera canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.camera__placeholder { position: relative; text-align: center; padding: 28px; color: var(--text-dim); }

/* Front camera preview is mirrored so it behaves like a real mirror. The
   capture un-mirrors itself — see captureFrame() — so the saved photo reads the
   right way round. */
.camera video[data-mirror='1'] { transform: scaleX(-1); }

.shutter {
  position: absolute;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  width: 68px;
  height: 68px;
  border-radius: 50%;
  border: 4px solid rgba(255, 246, 236, 0.9);
  background: rgba(255, 246, 236, 0.22);
  transition: transform var(--dur-fast) var(--ease);
}

.shutter::after {
  content: '';
  position: absolute;
  inset: 6px;
  border-radius: 50%;
  background: var(--c-cream);
}

.shutter:active { transform: translateX(-50%) scale(0.9); }

/* Sits beside the shutter rather than under the frame: it belongs to the
   viewfinder, and a row of buttons below it was one more thing on the screen. */
.flip {
  position: absolute;
  right: 18px;
  bottom: 30px;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 246, 236, 0.55);
  background: rgba(24, 3, 10, 0.5);
  color: var(--c-cream);
  transition: transform var(--dur-fast) var(--ease);
}

.flip:active { transform: scale(0.9); }

/* The joy box's question, sitting between the errand and the two answers. */
.ask {
  margin: 22px 0 0;
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: clamp(21px, 6vw, 26px);
  font-weight: 500;
  line-height: 1.2;
}

/* The printed code, for a camera that will not cooperate. */
.codebox {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

.codebox .input { flex: 1; min-width: 0; }
.codebox .btn { flex: none; width: auto; padding-inline: 20px; }

/* ── QR scanner ───────────────────────────────────────────────────────────── */
.scanframe {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.scanframe__box {
  width: 62%;
  aspect-ratio: 1;
  border-radius: 20px;
  box-shadow: 0 0 0 100vmax rgba(24, 3, 10, 0.5);
  border: 2px solid rgba(255, 246, 236, 0.85);
}

/* Sweeping line, translate-only. */
.scanframe__line {
  position: absolute;
  width: 58%;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--c-gold), transparent);
  animation: sweep 2.2s var(--ease) infinite;
}

@keyframes sweep {
  0%, 100% { transform: translateY(-29%); opacity: 0; }
  15%, 85% { opacity: 1; }
  50%      { transform: translateY(29%); }
}

/* ── Finish ───────────────────────────────────────────────────────────────── */
.codecard {
  position: relative;
  text-align: center;
  padding: 26px 22px;
  border-radius: var(--r-lg);
  background: var(--c-cream);
  color: var(--c-ink);
  overflow: hidden;
}

.codecard__qr {
  width: min(228px, 62vw);
  margin: 4px auto 16px;
  padding: 12px;
  border-radius: var(--r-md);
  background: #fff;
}

.codecard__qr canvas, .codecard__qr img { width: 100%; height: auto; image-rendering: pixelated; }

.codecard__code {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: clamp(30px, 10vw, 40px);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  line-height: 1;
  margin: 0;
  /* Cormorant ships old-style figures by default, which sit oddly next to caps. */
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: 'lnum' 1, 'tnum' 1;
}

/* Shown in the QR's place when the generator could not be loaded. Same footprint
   as the QR so the card does not jump, and unmistakably a fault rather than a
   design — the old fallback printed the code here and read as intentional. */
.qrfail {
  display: grid;
  place-items: center;
  gap: 12px;
  aspect-ratio: 1;
  padding: 18px;
  text-align: center;
}

.qrfail p { margin: 0; font-size: 13px; line-height: 1.4; color: #8a4a22; }

.qrfail .btn {
  width: auto;
  padding: 10px 20px;
  border-color: rgba(64, 19, 10, 0.35);
  color: #40130a;
}

.codecard__label { font-size: 11px; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; color: #9a4a1f; margin: 0 0 8px; }

.stamp {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 15px;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--r-full);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-gold);
}

/* The stamp that lands when a quest is finished: the mark thumps down over the
   screen, a ring goes out from under it, and the card takes over behind it. */
.thump {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  pointer-events: none;
  background: rgba(28, 3, 12, 0.5);
  animation: thump-veil 900ms var(--ease) both;
}

.thump__mark {
  width: min(170px, 46vw);
  animation: thump-mark 900ms cubic-bezier(0.2, 1.5, 0.4, 1) both;
}

.thump__ring {
  position: absolute;
  width: min(170px, 46vw);
  aspect-ratio: 1;
  border-radius: 50%;
  border: 2px solid var(--c-gold);
  animation: thump-ring 900ms var(--ease) 120ms both;
}

@keyframes thump-veil {
  0%   { opacity: 0; }
  16%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes thump-mark {
  0%   { opacity: 0; transform: scale(2.6) rotate(-24deg); }
  38%  { opacity: 1; transform: scale(1) rotate(-6deg); }
  78%  { opacity: 1; transform: scale(1) rotate(-6deg); }
  100% { opacity: 0; transform: scale(0.9) rotate(-6deg); }
}

@keyframes thump-ring {
  0%   { opacity: 0.9; transform: scale(0.7); }
  100% { opacity: 0; transform: scale(2.1); }
}

/* ── Coach marks ──────────────────────────────────────────────────────────── */
/* A guide step lights one element and dims the rest. Two layers do it, and the
   split is the whole point: the ring *paints* the dim, as its own huge outer
   shadow, so the lit area keeps the element's rounded corners — and the ring is
   `pointer-events: none`, so a tap on the element still reaches it and the guide
   can say "press this" and then let the participant press it. Four invisible
   panels laid around the same box *catch* the taps everywhere else. */
.guide {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
}

/* Invisible: the panels exist to swallow taps, not to be seen. The dim itself is
   the ring's outer shadow, which is what gives the lit area the same rounded
   corners as the element inside it — four rectangles cannot. */
.guide__panel {
  position: fixed;
  pointer-events: auto;
}

/* Traces the element's own corners — width, height and radius are set from its
   bounding box in guide.js. The spread is far larger than any phone so the whole
   screen outside the ring is covered; it is static, never animated. */
.guide__ring {
  position: fixed;
  border: 2px solid var(--c-gold);
  box-shadow: 0 0 0 100vmax rgba(18, 2, 8, 0.74);
  pointer-events: none;
  animation: guide-veil var(--dur) var(--ease) both;
}

.guide__ring::after {
  content: '';
  position: absolute;
  inset: -5px;
  border-radius: inherit;
  border: 2px solid var(--c-gold);
  animation: breathe 2.6s var(--ease) infinite;
}

.guide__bubble {
  position: fixed;
  left: 50%;
  width: min(calc(100vw - 32px), 380px);
  padding: 16px 18px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: rgba(36, 5, 15, 0.96);
  box-shadow: 0 20px 44px rgba(16, 2, 8, 0.55);
  pointer-events: auto;
  animation: guide-bubble var(--dur-slow) var(--ease) both;
}

.guide__step {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--c-gold);
}

.guide__title {
  margin: 0 0 4px;
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 24px;
  font-weight: 600;
  line-height: 1.15;
  color: var(--text);
}

.guide__body {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-dim);
}

.guide__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 14px;
}

.guide__skip {
  padding: 8px 4px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-faint);
}

.guide__go {
  min-height: 44px;
  padding: 11px 22px;
  border-radius: var(--r-full);
  background: linear-gradient(135deg, var(--c-gold), var(--c-amber));
  color: #40130a;
  font-size: 15px;
  font-weight: 700;
  transition: transform var(--dur-fast) var(--ease);
}

.guide__go:active { transform: scale(0.97); }

/* The bubble keeps its own centring translate, so it cannot borrow `rise`. */
@keyframes guide-bubble {
  from { opacity: 0; transform: translate3d(-50%, 10px, 0); }
  to   { opacity: 1; transform: translate3d(-50%, 0, 0); }
}

@keyframes guide-veil {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Embers. 18 spans, transform + opacity only, and they stop after one pass. */
.embers { position: fixed; inset: 0; pointer-events: none; z-index: 40; overflow: hidden; }

.ember {
  position: absolute;
  bottom: -18px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--c-gold), var(--c-orange));
  opacity: 0;
  animation: ember-rise var(--d, 2600ms) var(--ease) var(--delay, 0ms) forwards;
}

@keyframes ember-rise {
  0%   { transform: translate3d(0, 0, 0) scale(0.4); opacity: 0; }
  12%  { opacity: 1; }
  100% { transform: translate3d(var(--dx, 0), -108vh, 0) scale(1.1); opacity: 0; }
}

/* ── Bits ─────────────────────────────────────────────────────────────────── */
.spinner {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2.5px solid rgba(255, 246, 236, 0.28);
  border-top-color: var(--c-cream);
  animation: spin 720ms linear infinite;
}

.spinner--ink { border-color: rgba(64, 19, 10, 0.28); border-top-color: #40130a; }

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

.center-note { text-align: center; padding: 40px 20px; color: var(--text-dim); }
.row { display: flex; align-items: center; gap: 10px; }
.row svg, .notice svg, .qhint svg { flex: none; }
.grow { flex: 1; }
.hidden { display: none !important; }

/* The `hidden` attribute loses to any explicit `display` in a rule, so panels
   toggled with `el.hidden = true` would stay on screen. This wins that fight. */
[hidden] { display: none !important; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── Motion preferences ───────────────────────────────────────────────────── */
/* Honour the OS switch: kill the ambient drift and every decorative loop, keep
   the interface fully usable. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .bg__blob { animation: none; }
  .embers { display: none; }
  /* Its resting position is a transform, so it must not be left mid-keyframe. */
  .guide__bubble { animation: none; transform: translate3d(-50%, 0, 0); }
}

/* ── Larger screens ───────────────────────────────────────────────────────── */
@media (min-width: 520px) {
  :root { --pad: 26px; }
  .shell { border-inline: 1px solid rgba(255, 217, 122, 0.14); min-height: 100dvh; }
}
