@import url("./tokens.css");

*, *::before, *::after { box-sizing: border-box; }
html, body { min-height: 100%; overflow-x: clip; }
html { background: var(--color-paper); scrollbar-color: var(--color-accent) var(--color-paper-2); }
*::-webkit-scrollbar { width: 0.75rem; }
*::-webkit-scrollbar-track { background: var(--color-paper-2); }
*::-webkit-scrollbar-thumb { background: var(--color-accent); border: 0.2rem solid var(--color-paper-2); border-radius: var(--radius-pill); }
body {
  margin: 0;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-base);
  pointer-events: none;
  opacity: 0.06;
  background-image: url("./assets/ticket-grain.png");
  background-size: 32rem 32rem;
}
button, input, textarea, select { font: inherit; }
button { color: inherit; }
a { color: inherit; text-underline-offset: 0.2em; }
::selection { background: var(--color-accent); color: var(--color-accent-ink); }
:focus-visible { outline: 3px solid var(--color-focus); outline-offset: 3px; }
.hidden { display: none !important; }
.tnum { font-variant-numeric: tabular-nums; }
.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; }

.shell {
  position: relative;
  z-index: calc(var(--z-base) + 1);
  width: min(100%, 30rem);
  min-height: 100dvh;
  margin-inline: auto;
  padding: max(var(--space-lg), env(safe-area-inset-top)) max(var(--space-md), env(safe-area-inset-right)) max(var(--space-2xl), env(safe-area-inset-bottom)) max(var(--space-md), env(safe-area-inset-left));
}
.shell--wide { width: min(100%, 62rem); }

.mast {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-md);
  align-items: start;
  padding-block-end: var(--space-lg);
}
.mast__title, h1, h2, h3 {
  margin: 0;
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 900;
  line-height: 1.02;
  text-transform: uppercase;
  overflow-wrap: anywhere;
  min-width: 0;
}
.mast__title { font-size: clamp(2.25rem, 10vw, 3.35rem); letter-spacing: -0.02em; }
.mast__meta { margin: var(--space-xs) 0 0; color: var(--color-ink-2); font-size: var(--text-sm); }

.status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 2.75rem;
  padding-inline: var(--space-sm);
  border: var(--rule) solid var(--color-rule);
  background: var(--color-paper-2);
  color: var(--color-ink-2);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 700;
  white-space: nowrap;
}
.status__mark { width: 0.65rem; height: 0.65rem; border: 2px solid currentColor; transform: rotate(45deg); }
.status[data-state="ready"] { color: var(--color-good); }
.status[data-state="pending"] { color: var(--color-accent); }
.status[data-state="offline"], .status[data-state="error"] { color: var(--color-danger); }

.ticket {
  position: relative;
  background: var(--color-paper-2);
  color: var(--color-ink);
  border-block: var(--rule) solid var(--color-rule);
}
.ticket::before, .ticket::after {
  content: "";
  position: absolute;
  inset-block: -1px;
  width: 0.75rem;
  background: radial-gradient(circle, var(--color-paper) 0 42%, transparent 45%) center / 0.75rem 1.35rem repeat-y;
  pointer-events: none;
}
.ticket::before { inset-inline-start: -0.38rem; }
.ticket::after { inset-inline-end: -0.38rem; }

.ticket-id {
  padding: var(--space-lg) var(--space-lg) var(--space-md);
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(7.5rem, 0.75fr);
  gap: var(--space-lg);
  background-color: var(--color-ink);
  background-image: url("./assets/ticket-grain.png");
  background-size: 24rem 24rem;
  color: var(--color-paper);
}
.ticket-id__event { font-family: var(--font-display); font-size: clamp(1.45rem, 7vw, 3.25rem); font-weight: 900; line-height: 0.92; text-transform: uppercase;
  /* Event names are operator-typed and can be long. Shrink and wrap on word
     boundaries first; only shatter a word that cannot fit on its own line. */
  overflow-wrap: break-word; text-wrap: balance; hyphens: auto; }
.ticket-id__eyebrow { display: block; margin-block-start: var(--space-sm); font-family: var(--font-display); font-size: var(--text-sm); font-weight: 900; letter-spacing: 0.25em; text-align: center; text-transform: uppercase; }
.ticket-id__guest { border-inline-start: 2px dashed color-mix(in oklch, var(--color-paper), transparent 45%); padding-inline-start: var(--space-md); }
.ticket-id__guest strong { display: block; font-family: var(--font-display); font-size: var(--text-lg); line-height: 1; text-transform: uppercase; }
.ticket-id__guest > span { display: block; color: var(--color-paper-3); font-size: var(--text-sm); margin-block-start: var(--space-xs); }
.ticket-id .status { margin-block-start: var(--space-md); }
.ticket-id .status[data-state="ready"] { color: var(--color-paper); }
.ticket-id .status[data-state="ready"] .status__mark { border-color: var(--color-good); }

.countdown {
  margin-block-end: var(--space-lg);
  padding: var(--space-lg);
  border: 2px dashed var(--color-accent);
  color: var(--color-ink);
  background: var(--color-paper-2);
}
.countdown strong { display: block; color: var(--color-accent); font-family: var(--font-display); font-size: clamp(2.4rem, 11vw, 4rem); line-height: 0.95; text-transform: uppercase; }
.countdown span { display: block; margin-block-start: var(--space-xs); color: var(--color-ink-2); }

.swipe {
  position: relative;
  min-height: 5.25rem;
  overflow: hidden;
  border: var(--rule) solid var(--color-accent-deep);
  border-radius: var(--radius-control);
  background: var(--color-accent);
  background-image: url("./assets/ticket-grain.png");
  background-size: 22rem 22rem;
  color: var(--color-accent-ink);
  touch-action: pan-y;
  user-select: none;
}
.swipe__fill { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--color-accent-deep); pointer-events: none; }
.swipe__label { position: absolute; inset: 0; display: grid; place-items: center; padding-inline: 5rem 4.75rem; font-family: var(--font-display); font-size: clamp(1.35rem, 6vw, 1.8rem); font-weight: 900; letter-spacing: 0.01em; text-transform: uppercase; white-space: nowrap; pointer-events: none; }
.swipe__hint { position: absolute; z-index: calc(var(--z-base) + 1); inset: 50% var(--space-sm) auto auto; transform: translateY(-50%); max-width: 4rem; font-family: var(--font-display); font-size: var(--text-xs); font-weight: 900; letter-spacing: 0.08em; line-height: 1.05; text-align: center; text-transform: uppercase; pointer-events: none; }
.swipe__handle {
  position: absolute;
  z-index: calc(var(--z-base) + 1);
  inset: 0.35rem auto 0.35rem 0.35rem;
  width: 4.55rem;
  border: 0;
  border-radius: 50%;
  background: var(--color-paper);
  color: var(--color-accent);
  display: grid;
  place-items: center;
  cursor: grab;
  touch-action: none;
  transition: transform var(--dur-long) var(--ease-out), opacity var(--dur-short) var(--ease-out);
}
.swipe__handle::before, .swipe__handle::after {
  content: "";
  position: absolute;
  width: 1rem;
  height: 1rem;
  border-block-start: 0.3rem solid currentColor;
  border-inline-end: 0.3rem solid currentColor;
  transform: rotate(45deg);
}
.swipe__handle::before { margin-inline-start: -0.65rem; }
.swipe__handle::after { margin-inline-start: 0.2rem; }
.swipe__handle[aria-disabled="true"] { opacity: 0.48; cursor: not-allowed; }
.swipe__handle.is-dragging { cursor: grabbing; transition: none; }
.swipe[data-state="locked"] { background: var(--color-paper-3); border-color: var(--color-rule); color: var(--color-ink-2); }
.swipe[data-state="locked"] .swipe__handle { color: var(--color-muted); }
.swipe[data-state="loading"] .swipe__handle { opacity: 0.7; }

.route { margin-block-start: var(--space-md); }
.stop {
  position: relative;
  padding: var(--space-lg) var(--space-lg) var(--space-xl);
  background: var(--color-paper-2);
  background-image: url("./assets/ticket-grain.png");
  background-size: 28rem 28rem;
  border-block-start: 1px dashed var(--color-muted);
  overflow: clip;
}
.stop:last-child { border-block-end: 1px dashed var(--color-muted); }
.stop__head { display: grid; grid-template-columns: 4.75rem minmax(0, 1fr); gap: var(--space-lg); align-items: stretch; }
.stop__index { display: grid; grid-template-rows: auto auto auto; justify-items: center; align-content: start; gap: var(--space-xs); padding-inline-end: var(--space-md); border-inline-end: 2px dashed var(--color-muted); font-family: var(--font-display); font-weight: 900; letter-spacing: 0.12em; text-transform: uppercase; }
.stop__index span, .stop__index small { font-size: var(--text-xs); }
.stop__number {
  width: 4.15rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--color-slot);
  color: var(--color-ink);
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: 2.8rem;
  font-weight: 900;
  box-shadow: inset 0 0 0 0.35rem var(--color-paper);
}
.stop__name { font-size: clamp(2rem, 9vw, 3rem); letter-spacing: -0.015em; }
.stop__address, .stop__offer { margin: 0; }
.stop__address { margin-block-start: var(--space-xs); color: var(--color-ink-2); font-size: var(--text-sm); }
.stop__offer { margin-block-start: var(--space-md); font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; text-transform: uppercase; }
.stop .swipe { margin-block-start: var(--space-lg); }
.stop__stamp {
  position: absolute;
  z-index: calc(var(--z-base) + 1);
  inset: auto var(--space-lg) var(--space-lg) auto;
  transform: rotate(-7deg) scale(0.75);
  opacity: 0;
  border: 0.28rem double var(--color-stamp);
  color: var(--color-stamp);
  padding: var(--space-xs) var(--space-md);
  font-family: var(--font-display);
  font-size: clamp(1.8rem, 8vw, 2.65rem);
  font-weight: 900;
  line-height: 0.9;
  text-align: center;
  text-transform: uppercase;
  pointer-events: none;
}
.stop__stamp small { display: block; font-family: var(--font-body); font-size: var(--text-xs); margin-block-start: var(--space-xs); }
.stop.is-done { color: var(--color-muted); padding-block-end: 7.25rem; }
.stop.is-done .swipe { display: none; }
.stop.is-done .stop__stamp { opacity: 1; transform: rotate(-7deg) scale(1); animation: stamp-in var(--dur-long) var(--ease-out) both; }
.stop.is-closed { color: var(--color-muted); background: var(--color-paper-3); }
.stop.is-closed .swipe { display: none; }
.closed-label { display: none; margin-block-start: var(--space-lg); min-height: 3.5rem; place-items: center; border: 1px dashed var(--color-muted); color: var(--color-ink-2); font-family: var(--font-display); font-size: var(--text-lg); text-transform: uppercase; }
.stop.is-closed .closed-label { display: grid; }
.pending-note { color: var(--color-accent); }

.ticket-action { padding: var(--space-md); background-color: var(--color-paper-2); background-image: url("./assets/ticket-grain.png"); background-size: 26rem 26rem; }
.ticket-action .readiness { padding-inline: var(--space-xs); }
.route-footer { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-md); align-items: center; padding: var(--space-lg); border-block-start: 2px dashed var(--color-muted); background-color: var(--color-paper-2); background-image: url("./assets/ticket-grain.png"); background-size: 28rem 28rem; }
.route-footer strong { font-size: var(--text-sm); }
.route-footer__queue { display: block; margin-block-start: var(--space-xs); color: var(--color-accent); font-size: var(--text-xs); text-align: end; }
.route-dots { display: flex; align-items: center; gap: var(--space-sm); margin-block-start: var(--space-sm); }
.route-dots span { width: 1.25rem; aspect-ratio: 1; border: 2px solid var(--color-muted); border-radius: 50%; }
.route-dots span.is-done { border-color: var(--color-accent); background: var(--color-accent); }
.route-dots span.is-closed { opacity: 0.45; }
.status--bare { min-height: 0; padding: 0; border: 0; background: transparent; }
.ticket-id, .swipe, .stop, .ticket-action, .route-footer { background-blend-mode: soft-light; }

.screen-state { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; background: var(--color-paper); padding: var(--space-xl); text-align: center; }
.screen-state h1 { font-size: clamp(2.5rem, 12vw, 4rem); }
.screen-state p { max-width: 34ch; margin: var(--space-md) auto 0; color: var(--color-ink-2); }
.spinner { width: 3.5rem; aspect-ratio: 1; margin-inline: auto; border: 0.35rem solid var(--color-paper-3); border-top-color: var(--color-accent); border-radius: 50%; animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(1turn); } }
@keyframes stamp-in {
  0% { opacity: 0; transform: rotate(-12deg) scale(1.35); }
  70% { opacity: 1; transform: rotate(-6deg) scale(0.94); }
  100% { opacity: 1; transform: rotate(-7deg) scale(1); }
}

.button {
  min-height: 3.25rem;
  padding-inline: var(--space-md);
  border: var(--rule) solid var(--color-accent-deep);
  border-radius: var(--radius-control);
  background: var(--color-accent);
  color: var(--color-accent-ink);
  font-weight: 800;
  white-space: nowrap;
  cursor: pointer;
  transition: transform var(--dur-micro) var(--ease-out), background-color var(--dur-short) var(--ease-out);
}
.button:active { transform: translateY(1px); }
.button:disabled { opacity: 0.5; cursor: not-allowed; }
.button--quiet { background: var(--color-paper-3); color: var(--color-ink); border-color: var(--color-rule); }
.button--danger { background: var(--color-paper-3); color: var(--color-danger); border-color: var(--color-danger); }
.button--wide { width: 100%; }

.field { display: grid; gap: var(--space-xs); margin-block-end: var(--space-md); }
.field label { color: var(--color-ink-2); font-size: var(--text-sm); font-weight: 700; }
.field input, .field textarea {
  width: 100%;
  min-height: 3.5rem;
  border: var(--rule) solid var(--color-rule);
  border-radius: var(--radius-control);
  outline: 2px solid transparent;
  outline-offset: 1px;
  background: var(--color-slot);
  color: var(--color-ink);
  padding: var(--space-sm) var(--space-md);
  caret-color: var(--color-accent);
}
.field textarea { min-height: 6rem; resize: vertical; }
.field input::placeholder, .field textarea::placeholder { color: var(--color-muted); opacity: 1; }
.field input:focus-visible, .field textarea:focus-visible { outline-color: var(--color-focus); }
.field input[aria-invalid="true"] { border-color: var(--color-danger); }
.field__help { min-height: 1.4em; color: var(--color-muted); font-size: var(--text-xs); }
.field__help.is-error { color: var(--color-danger); }
.field-row { display: grid; gap: var(--space-sm); }

.gate { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; background: var(--color-paper); padding: var(--space-lg); }
.gate__ticket { width: min(100%, 24rem); padding: var(--space-xl) var(--space-lg); background: var(--color-paper-2); border-block: 1px dashed var(--color-muted); }
.gate h1 { font-size: 3.4rem; }
.gate p { color: var(--color-ink-2); margin: var(--space-xs) 0 var(--space-lg); }

.toast { position: fixed; z-index: var(--z-toast); inset: auto var(--space-md) max(var(--space-md), env(safe-area-inset-bottom)); margin-inline: auto; width: fit-content; max-width: calc(100% - 2rem); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-control); background: var(--color-ink); color: var(--color-paper); font-weight: 800; opacity: 0; transform: translateY(0.5rem); pointer-events: none; transition: opacity var(--dur-short) var(--ease-out), transform var(--dur-short) var(--ease-out); }
.toast.is-showing { opacity: 1; transform: none; }

dialog { position: fixed; inset: 0; margin: auto; width: min(calc(100% - 2rem), 30rem); max-height: min(88dvh, 42rem); overflow-y: auto; padding: var(--space-lg); border: var(--rule) solid var(--color-rule); border-radius: var(--radius-sheet); background: var(--color-paper-2); color: var(--color-ink); }
dialog::backdrop { background: var(--color-scrim); }
dialog h2 { font-size: var(--text-2xl); margin-block-end: var(--space-xs); }
.dialog-copy { margin: 0 0 var(--space-lg); color: var(--color-ink-2); }
.dialog-actions { display: grid; gap: var(--space-sm); margin-block-start: var(--space-lg); }

.toolbar { position: sticky; inset-block-start: 0; z-index: var(--z-sticky); display: grid; grid-template-columns: minmax(0, 1fr) 3.5rem; gap: var(--space-xs); padding-block: var(--space-xs) var(--space-sm); background: var(--color-paper); }
.searchbox { display: flex; align-items: center; min-height: 3.5rem; border: var(--rule) solid var(--color-rule); border-radius: var(--radius-control); background: var(--color-paper-2); }
.searchbox input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--color-ink); padding-inline: var(--space-md); font-size: var(--text-md); }
.icon-button { width: 3.5rem; min-height: 3.5rem; border: var(--rule) solid var(--color-rule); border-radius: var(--radius-control); background: var(--color-accent); color: var(--color-accent-ink); font-size: 1.5rem; cursor: pointer; }

.metric-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-block-end: var(--space-lg); border-block: 1px dashed var(--color-muted); }
.metric { padding: var(--space-md) var(--space-sm); }
.metric + .metric { border-inline-start: 1px dashed var(--color-muted); }
.metric strong { display: block; font-family: var(--font-display); color: var(--color-accent); font-size: clamp(2.2rem, 8vw, 3.6rem); line-height: 0.9; }
.metric span { display: block; margin-block-start: var(--space-xs); color: var(--color-ink-2); font-size: var(--text-xs); }

.tabs { display: flex; gap: var(--space-xs); margin-block-end: var(--space-sm); overflow-x: auto; }
.tab { min-height: 2.75rem; padding-inline: var(--space-sm); border: 1px solid var(--color-rule); border-radius: var(--radius-pill); background: var(--color-paper-2); color: var(--color-ink-2); font-weight: 800; white-space: nowrap; }
.tab[aria-selected="true"] { background: var(--color-ink); color: var(--color-paper); }

.list { border-block-start: 1px dashed var(--color-muted); }
.list-row { width: 100%; min-height: 4.75rem; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-sm); align-items: center; padding: var(--space-sm) var(--space-xs); border: 0; border-block-end: 1px dashed var(--color-rule); background: transparent; color: var(--color-ink); text-align: start; cursor: pointer; }
.list-row__name { font-weight: 800; overflow-wrap: anywhere; }
.list-row__meta { display: block; margin-block-start: var(--space-2xs); color: var(--color-ink-2); font-size: var(--text-sm); }
.badge { display: inline-flex; align-items: center; min-height: 2rem; padding-inline: var(--space-xs); border: 1px solid currentColor; border-radius: var(--radius-control); color: var(--color-ink-2); font-family: var(--font-display); font-weight: 700; text-transform: uppercase; white-space: nowrap; }
.badge--good { color: var(--color-good); }
.badge--pending { color: var(--color-accent); }
.badge--danger { color: var(--color-danger); }
.empty { padding: var(--space-xl) var(--space-md); color: var(--color-ink-2); text-align: center; }

.section-title { margin: var(--space-xl) 0 var(--space-sm); font-size: var(--text-2xl); }
.ledger { border-block: 1px dashed var(--color-muted); }
.ledger-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-md); align-items: center; padding-block: var(--space-md); border-block-end: 1px dashed var(--color-rule); }
.ledger-row:last-child { border-block-end: 0; }
.ledger-row__name { font-weight: 800; }
.ledger-row__meta { color: var(--color-ink-2); font-size: var(--text-sm); overflow-wrap: anywhere; }
.ledger-row__count { font-family: var(--font-display); color: var(--color-accent); font-size: var(--text-2xl); font-weight: 900; text-align: end; }
.ledger-row__actions { display: flex; align-items: center; gap: var(--space-xs); }
.switch { position: relative; width: 3.75rem; height: 2.75rem; border: 1px solid var(--color-rule); border-radius: var(--radius-pill); background: var(--color-paper-3); cursor: pointer; }
.switch::after { content: ""; position: absolute; inset: 0.25rem auto 0.25rem 0.25rem; aspect-ratio: 1; border-radius: 50%; background: var(--color-muted); transition: transform var(--dur-short) var(--ease-out), background-color var(--dur-short) var(--ease-out); }
.switch[aria-checked="true"] { background: var(--color-accent); }
.switch[aria-checked="true"]::after { background: var(--color-accent-ink); transform: translateX(1rem); }
.switch:disabled { opacity: 0.5; }

.scanner { position: fixed; inset: 0; z-index: var(--z-toast); display: grid; grid-template-rows: auto 1fr auto; background: var(--color-slot); }
.scanner video { width: 100%; height: 100%; object-fit: cover; }
.scanner__bar { display: flex; justify-content: space-between; align-items: center; padding: max(var(--space-md), env(safe-area-inset-top)) var(--space-md) var(--space-sm); background: var(--color-paper); }
.scanner__frame { position: absolute; inset: 50% auto auto 50%; width: min(72vw, 19rem); aspect-ratio: 1; transform: translate(-50%, -50%); border: 0.25rem solid var(--color-accent); box-shadow: 0 0 0 100vmax var(--color-scrim); pointer-events: none; }
.scanner__message { padding: var(--space-lg); background: var(--color-paper); color: var(--color-ink); text-align: center; }

.report-view { position: fixed; inset: 0; z-index: var(--z-modal); overflow-y: auto; background: var(--color-paper); }
.report-view__inner { width: min(100%, 62rem); margin-inline: auto; padding: max(var(--space-lg), env(safe-area-inset-top)) var(--space-md) var(--space-2xl); }
.report-head { display: flex; justify-content: space-between; gap: var(--space-md); align-items: start; }
.journey { padding-block: var(--space-md); border-block-end: 1px dashed var(--color-rule); }
.journey__stops { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-block-start: var(--space-xs); }
.journey__stop { padding: var(--space-xs) var(--space-sm); border: 1px solid var(--color-rule); border-radius: var(--radius-control); color: var(--color-ink-2); font-size: var(--text-sm); }
.journey__stop time { color: var(--color-accent); margin-inline-start: var(--space-xs); }

@media (hover: hover) and (pointer: fine) {
  .button:hover, .icon-button:hover { transform: translateY(-1px); }
  .list-row:hover { background: var(--color-paper-2); }
}
@media (min-width: 40rem) {
  .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .admin-grid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(18rem, 0.8fr); gap: var(--space-xl); align-items: start; }
  .admin-grid .section-title:first-child { margin-block-start: 0; }
}
@media (max-width: 23rem) {
  .mast { grid-template-columns: 1fr; }
  .status { justify-self: start; }
  .ticket-id { grid-template-columns: 1fr; }
  .ticket-id__guest { border-inline-start: 0; border-block-start: 2px dashed var(--color-muted); padding: var(--space-sm) 0 0; }
  .stop { padding-inline: var(--space-md); }
  .stop__head { grid-template-columns: 3.75rem minmax(0, 1fr); }
  .stop__number { width: 3.5rem; }
  .swipe__label { font-size: 1.5rem; }
  .swipe__hint { display: none; }
  .route-footer { grid-template-columns: 1fr; }
  .route-footer__queue { text-align: start; }
  .metric-strip { grid-template-columns: 1fr; }
  .metric + .metric { border-inline-start: 0; border-block-start: 1px dashed var(--color-muted); }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 150ms !important; animation-iteration-count: 1 !important; transition-duration: 150ms !important; scroll-behavior: auto !important; }
  .stop__stamp { transform: rotate(-7deg) scale(1); }
}

/* ---- demo view switch (only present when the link carries ?demo=1) ---- */
.vtabs {
  position: fixed;
  z-index: var(--z-toast);
  inset-block-start: max(var(--space-xs), env(safe-area-inset-top));
  inset-inline-end: max(var(--space-xs), env(safe-area-inset-right));
  display: flex;
  width: fit-content;
  max-width: calc(100% - 1rem);
  overflow: hidden;
  border: var(--rule) solid var(--color-rule);
  border-radius: var(--radius-pill);
  background: var(--color-paper-2);
}
.vtabs__tab {
  padding: var(--space-2xs) var(--space-sm);
  color: var(--color-ink-2);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-sm);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.vtabs__tab.is-on { background: var(--color-accent); color: var(--color-accent-ink); }
/* Reset is a button inside a bar of links: strip the button chrome so it reads
   as the third tab, and rule it off so it is not mistaken for a view. */
.vtabs__reset { border: 0; border-inline-start: var(--rule) solid var(--color-rule); background: none; cursor: pointer; font: inherit; }
/* Keep the floating bar off the page masthead. */
.has-vtabs .shell { padding-block-start: calc(max(var(--space-lg), env(safe-area-inset-top)) + var(--space-lg)); }

/* ---- attendee ledger on the organizer view ---- */
.section-count { margin-inline-start: var(--space-xs); color: var(--color-muted); font-size: var(--text-sm); font-weight: 600; }
.searchbox--flush { margin-block-end: var(--space-sm); }
.pass-state { flex: none; font-family: var(--font-display); font-weight: 700; font-size: var(--text-xs); letter-spacing: 0.08em; text-transform: uppercase; padding: var(--space-2xs) var(--space-xs); border: var(--rule) solid currentColor; white-space: nowrap; }
.pass-state.is-good { color: var(--color-good); }
.pass-state.is-wait { color: var(--color-muted); }
.pass-state.is-void { color: var(--color-danger); }
.button--danger { border-color: var(--color-danger); color: var(--color-danger); background: transparent; }
.button--danger:hover { background: var(--color-danger); color: var(--color-danger-ink); }

/* ---- organizer: keep the action row to four, park the destructive pair ---- */
.night { margin-block-start: var(--space-xl); }
.night__card { padding: var(--space-lg); }
.danger-zone { margin-block-start: var(--space-md); padding-block-start: var(--space-md); border-block-start: 1px dashed var(--color-rule); }
.danger-zone summary { cursor: pointer; font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm); letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-danger); }
.danger-zone summary::marker { color: var(--color-danger); }
.danger-zone > p { margin-block: var(--space-xs) var(--space-sm); }
.danger-zone__actions { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.ledger-row.is-openable { cursor: pointer; }
.ledger-row.is-openable:hover { background: var(--color-paper-2); }
.ledger-row.is-openable:focus-visible { outline: 3px solid var(--color-focus); outline-offset: -3px; }
.button--small { min-height: 2.25rem; padding: var(--space-2xs) var(--space-sm); font-size: var(--text-xs); }
#guest-stops { margin-block: var(--space-md); }

/* ============================================================================
   ZOMBIE MARQUEE — the approved crawl skin (DESIGN.md: "Zombie Marquee").
   Poster language on top of the punch-card structure: a gold arch band and a
   blood edge under the stub, the event title in the poster's gold-to-ember
   gradient, every offer on an ember ribbon, and progress read as a segmented
   HUD bar. Nothing here changes markup, copy, or control semantics.
   ========================================================================= */

/* The bone stub stays bone; the arch band sits above the event name so the
   poster framing is the first thing read, the way the flyer reads. */
.ticket-id { position: relative; }
/* The arch band is a row in the stub, not an overlay: the event name can be any
   length at runtime and must never collide with it. */
.ticket-id::before {
  content: "Gaslamp Quarter";
  grid-column: 1 / -1;
  margin-block-end: var(--space-2xs);
  padding-block-end: var(--space-2xs);
  white-space: nowrap;
  border-block-end: 2px solid var(--color-gold);
  color: var(--color-blood);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: 900;
  letter-spacing: 0.3em;
  text-transform: uppercase;
}

/* Title treatment, once per screen. Gradient fill with a hard stroke so it
   survives the bone stock the way the poster's outlined type does. */
.ticket-id__event {
  display: inline-block;
  rotate: -1.5deg;
  background: linear-gradient(180deg, #FFD95E 4%, var(--color-gold) 38%, var(--color-accent) 96%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-stroke: 2px #190403;
  paint-order: stroke fill;
}

/* Blood running out of the stub and onto the route. One decorative element,
   drawn as a mask so it tints with the blood token rather than shipping art. */
.ticket-id::after {
  content: "";
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: 0;
  width: 100%;
  height: 2.4rem;
  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 430 42" preserveAspectRatio="none"><path fill="%23A80E18" d="M0 0h430v12c-17 2-19 15-29 15s-12-9-22-9-11 19-22 19-13-24-26-24-14 11-25 11-13-17-26-17-15 21-27 21-12-15-23-15-13 9-24 9-13-13-25-13-15 17-27 17-13-20-25-20-14 9-25 9-13-8-24-8S9 20 0 18Z"/></svg>') top center / 100% 100% no-repeat;
  pointer-events: none;
  z-index: calc(var(--z-base) + 1);
  animation: drip-settle 900ms var(--ease-out) both;
}

/* HUD: one segment per stop, spent segments drop to blood. Replaces the dots
   with the poster's health-bar read without touching the markup. */
.route-dots { gap: 3px; padding: 3px; border: 2px solid var(--color-accent-ink); background: var(--color-slot); }
.route-dots span {
  width: auto;
  flex: 1;
  aspect-ratio: auto;
  height: 0.85rem;
  border: 0;
  border-radius: 0;
  background: linear-gradient(180deg, #FFE083, var(--color-gold));
  box-shadow: inset 0 -3px 0 rgb(0 0 0 / 0.35);
  transition: background var(--dur-long) var(--ease-out);
}
.route-dots span.is-done { background: linear-gradient(180deg, #6B1D22, #2C0A10); }
.route-dots span.is-closed { opacity: 0.4; }

/* The offer is the thing a bartender and a guest both look for, so it rides a
   ribbon that bleeds to the card edges instead of sitting in the text column. */
.stop__offer {
  margin-inline: calc(var(--space-lg) * -1);
  padding: var(--space-xs) var(--space-lg);
  background: linear-gradient(90deg, var(--color-accent), #F08A00);
  color: var(--color-accent-ink);
  font-size: var(--text-md);
  line-height: 1.15;
}
.stop.is-done .stop__offer { background: linear-gradient(90deg, #4A2018, #36170F); color: var(--color-ink-2); }
.stop.is-closed .stop__offer { background: var(--color-paper-3); color: var(--color-muted); }

/* Numbers read as the poster's blood squares rather than punch wells. */
.stop__number {
  border-radius: 0;
  background: var(--color-blood);
  color: var(--color-ink);
  box-shadow: inset 0 0 0 2px var(--color-accent-ink);
}
.stop.is-done .stop__number { background: #2C0A10; color: var(--color-muted); }
/* Finished bars sit at the bottom of the route, greyed out so the open ones lead. */
.stop.is-done:not(.is-fresh) { filter: grayscale(0.9); opacity: 0.55; }

/* Motion, only where it carries meaning: the stamp slams, the handle nudges
   once in a while to teach the gesture. Both stop under reduced motion. */
.stop.is-done .stop__stamp { animation: stamp-slam 420ms var(--ease-out) both; }
@keyframes stamp-slam {
  0% { opacity: 0; transform: rotate(-16deg) scale(1.6); }
  55% { opacity: 1; transform: rotate(-4deg) scale(0.9); }
  75% { transform: rotate(-8.5deg) scale(1.04); }
  100% { opacity: 1; transform: rotate(-7deg) scale(1); }
}
.swipe[data-state="ready"] .swipe__handle:not(.is-dragging) { animation: handle-nudge 3.2s var(--ease-in-out) infinite; }
@keyframes handle-nudge {
  0%, 74%, 100% { transform: translateX(0); }
  84% { transform: translateX(8px); }
}
@keyframes drip-settle {
  from { transform: translateY(-0.6rem); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ticket-id::after, .swipe[data-state="ready"] .swipe__handle, .stop.is-done .stop__stamp { animation: none; }
}

/* ---- Poster art and the two moments that earn motion ---------------------
   The art sits behind the pass and fades out before the route, so the stub and
   the controls always read on flat colour. The pass opens once, and a stop
   splatters once, the first time it is stamped. Both are off under reduced
   motion. */
body::after {
  content: "";
  position: fixed;
  inset: 0 0 auto 0;
  height: min(78vh, 46rem);
  z-index: 0;
  pointer-events: none;
  background: url("./assets/pass-art.webp") top center / cover no-repeat;
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 42%, transparent 92%);
  mask-image: linear-gradient(180deg, #000 0%, #000 42%, transparent 92%);
}

/* First open: the stub drops in, the drip runs after it, the route deals in. */
body.is-opening .ticket { animation: stub-drop 620ms var(--ease-out) both; }
body.is-opening .ticket-id::after { animation: drip-run 700ms 260ms var(--ease-out) both; }
body.is-opening .stop { animation: stop-deal 520ms var(--ease-out) both; }
body.is-opening .stop:nth-child(1) { animation-delay: 300ms; }
body.is-opening .stop:nth-child(2) { animation-delay: 380ms; }
body.is-opening .stop:nth-child(3) { animation-delay: 460ms; }
body.is-opening .stop:nth-child(4) { animation-delay: 540ms; }
body.is-opening .stop:nth-child(n+5) { animation-delay: 620ms; }
@keyframes stub-drop {
  from { opacity: 0; transform: translateY(-1.5rem) rotate(-0.6deg); }
  to { opacity: 1; transform: none; }
}
@keyframes drip-run {
  from { clip-path: inset(0 0 100% 0); }
  to { clip-path: inset(0 0 0 0); }
}
@keyframes stop-deal {
  from { opacity: 0; transform: translateX(1.75rem); }
  to { opacity: 1; transform: none; }
}

/* Redeeming: blood hits the card, then the stamp lands in it. The splatter is
   decorative, so it is drawn on the stamp's own pseudo-element. */
.stop.is-fresh .stop__stamp::before {
  content: "";
  position: absolute;
  /* No negative z-index: that would drop the splatter behind the stop's own
     background and it would never be seen. */
  inset: -85% -55%;
  background: url("./assets/splatter.svg") center / contain no-repeat;
  opacity: 0;
  animation: splat 760ms var(--ease-out) both;
}
.stop.is-fresh .stop__stamp { animation: stamp-slam 420ms 180ms var(--ease-out) both; }
/* Keep the word readable on top of the splatter it lands in. The stamp itself
   stays absolutely positioned; only its own text is lifted. */
.stop__stamp { isolation: isolate; }
.stop__stamp small { position: relative; z-index: 1; }
@keyframes splat {
  0% { opacity: 0; transform: scale(0.35) rotate(-8deg); }
  35% { opacity: 0.95; transform: scale(1.06) rotate(2deg); }
  100% { opacity: 0.62; transform: scale(1) rotate(0deg); }
}

@media (prefers-reduced-motion: reduce) {
  body.is-opening .ticket,
  body.is-opening .ticket-id::after,
  body.is-opening .stop,
  .stop.is-fresh .stop__stamp,
  .stop.is-fresh .stop__stamp::before { animation: none; }
  .stop.is-fresh .stop__stamp::before { opacity: 0.62; }
}

/* ============================================================================
   FIGHT CARD — the top of the pass is the poster, moving.
   A looping Higgsfield render of the red and blue fighters under the Gaslamp
   arch, with smoke, embers and lightning layered over it in CSS so the scene
   keeps moving between the video's own beats. The HUD reads like the poster's
   PLAYER bars: the guest's bar fills as stops are stamped, the night's drains.
   ========================================================================= */
.fightcard {
  position: relative;
  margin-inline: calc(max(var(--space-md), env(safe-area-inset-left)) * -1);
  margin-block-start: calc(max(var(--space-lg), env(safe-area-inset-top)) * -1);
  aspect-ratio: 1;
  overflow: hidden;
  background: #120612 url("./assets/pass-art.webp") center 18% / cover no-repeat;
  isolation: isolate;
}
/* Fade into the page so the stub can sit on the street. */
.fightcard::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 22%;
  background: linear-gradient(180deg, transparent, var(--color-paper) 96%);
  z-index: 3;
  pointer-events: none;
}

/* Smoke: two soft banks rolling in opposite directions across the street. */
.fightcard__smoke {
  position: absolute;
  inset: 38% -40% 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(38% 30% at 25% 62%, rgb(210 180 255 / 0.28), transparent 70%),
    radial-gradient(34% 26% at 70% 70%, rgb(255 150 120 / 0.22), transparent 72%),
    radial-gradient(30% 22% at 48% 80%, rgb(240 230 255 / 0.2), transparent 70%);
  will-change: transform;
  animation: smoke-roll 14s var(--ease-in-out) infinite alternate;
}
.fightcard__smoke::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(36% 26% at 62% 58%, rgb(200 170 255 / 0.24), transparent 70%),
    radial-gradient(30% 22% at 18% 78%, rgb(255 120 90 / 0.2), transparent 72%);
  animation: smoke-roll 18s var(--ease-in-out) infinite alternate-reverse;
}
@keyframes smoke-roll {
  from { transform: translate3d(-9%, 0, 0) scale(1); }
  to { transform: translate3d(9%, -3%, 0) scale(1.08); }
}

/* Embers: a field of sparks drawn as gradients, tiled twice so the rise loops. */
.fightcard__embers {
  position: absolute;
  inset: 0 0 -100% 0;
  z-index: 2;
  pointer-events: none;
  --spark: radial-gradient(circle, rgb(255 170 60 / 0.95) 0 1.4px, transparent 2.4px);
  background-image: var(--spark), var(--spark), var(--spark), var(--spark), var(--spark);
  background-size: 140px 220px, 190px 260px, 110px 180px, 230px 300px, 160px 240px;
  background-position: 12px 40px, 80px 120px, 40px 10px, 150px 200px, 100px 90px;
  will-change: transform;
  animation: embers-rise 6s linear infinite;
}
@keyframes embers-rise { to { transform: translate3d(0, -50%, 0); } }

/* Lightning: the sky flashes white twice every nine seconds. */
.fightcard__flash {
  position: absolute;
  inset: 0 0 45% 0;
  z-index: 2;
  pointer-events: none;
  background: radial-gradient(70% 60% at 55% 20%, rgb(255 240 255 / 0.85), transparent 70%);
  mix-blend-mode: screen;
  opacity: 0;
  animation: lightning 9s linear infinite;
}
@keyframes lightning {
  0%, 61%, 63.5%, 65%, 67%, 100% { opacity: 0; }
  62% { opacity: 0.9; }
  66% { opacity: 0.6; }
}

/* HUD: the poster's PLAYER bars, red against blue, with a VS between them. */
.fightcard__hud {
  position: absolute;
  z-index: 4;
  inset: calc(max(var(--space-lg), env(safe-area-inset-top)) + 3.1rem) var(--space-md) auto;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: end;
  gap: var(--space-xs);
}
.hud { display: grid; gap: 3px; min-width: 0; }
.hud__tag {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--text-sm);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  text-shadow: 0 2px 0 #000;
}
.hud--p1 .hud__tag { color: #FF4A4A; }
.hud--p2 { text-align: end; }
.hud--p2 .hud__tag { color: #6FB6FF; }
.hud__bar {
  display: block;
  height: 0.95rem;
  padding: 2px;
  background: #0A0306;
  border: 2px solid #FFE7A0;
  box-shadow: 0 0 0 1px #000, 0 0 14px rgb(0 0 0 / 0.6);
}
.hud--p1 .hud__bar { clip-path: polygon(0 0, 100% 0, calc(100% - 9px) 100%, 0 100%); }
.hud--p2 .hud__bar { clip-path: polygon(0 0, 100% 0, 100% 100%, 9px 100%); }
.hud__bar i { display: block; height: 100%; transition: transform 900ms var(--ease-out); }
.hud--p1 .hud__bar i {
  background: linear-gradient(90deg, #8E0A12, #E11C22 55%, #FFB02E);
  transform-origin: left;
  transform: scaleX(max(0.06, var(--crawl-won, 0)));
  box-shadow: 0 0 12px rgb(225 28 34 / 0.8);
}
.hud--p2 .hud__bar i {
  background: linear-gradient(270deg, #0A2E8E, #2E7BFF 55%, #FFB02E);
  transform-origin: right;
  transform: scaleX(max(0.06, calc(1 - var(--crawl-won, 0))));
  box-shadow: 0 0 12px rgb(46 123 255 / 0.8);
}
.hud__vs {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 2.1rem;
  line-height: 0.8;
  padding-inline: 0.15rem;
  rotate: -6deg;
  background: linear-gradient(180deg, #FFE27A, #F5B91E 45%, #FF5A12);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-stroke: 1.5px #190403;
  filter: drop-shadow(0 0 10px rgb(255 120 30 / 0.85));
  will-change: transform;
  animation: vs-throb 1.8s var(--ease-in-out) infinite;
}
@keyframes vs-throb {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.14); }
}

/* The fighters. Each stands at its corner leaning in at the other, sized so
   the stub's top edge cuts them at the waist: heads, fists and shoulders above
   the ticket, legs behind it. The lean is a fixed rotate, the fight stance is
   an animated translate, and a jab every few seconds is an animated scale, so
   the three never fight over one transform. */
.fighter {
  position: absolute;
  z-index: 2;
  inset-block-start: 40%;
  width: 50%;
  aspect-ratio: 688 / 1024;
  background-position: top center;
  background-size: contain;
  background-repeat: no-repeat;
  will-change: transform, translate, scale;
}
.fighter--red {
  inset-inline-start: -3%;
  background-image: url("./assets/fighter-red.webp");
  rotate: 7deg;
  transform-origin: 30% 100%;
  filter: drop-shadow(0 0 14px rgb(255 50 50 / 0.75)) drop-shadow(0 0 40px rgb(200 10 20 / 0.5));
  animation: stance-bob 0.9s var(--ease-in-out) infinite, jab-red 4.2s var(--ease-out) infinite;
}
.fighter--blue {
  inset-inline-end: -3%;
  background-image: url("./assets/fighter-blue.webp");
  rotate: -7deg;
  transform-origin: 70% 100%;
  filter: drop-shadow(0 0 14px rgb(70 150 255 / 0.75)) drop-shadow(0 0 40px rgb(20 70 200 / 0.5));
  animation: stance-bob 0.9s 0.45s var(--ease-in-out) infinite, jab-blue 4.2s 2.1s var(--ease-out) infinite;
}
@keyframes stance-bob {
  0%, 100% { translate: 0 0; }
  50% { translate: 0 -4%; }
}
@keyframes jab-red {
  0%, 82%, 100% { scale: 1; }
  88% { scale: 1.07; }
}
@keyframes jab-blue {
  0%, 82%, 100% { scale: 1; }
  88% { scale: 1.07; }
}

/* The stub now hangs off the fight card instead of opening the page. */
.fightcard + .ticket-id { margin-block-start: -18%; position: relative; z-index: 5; box-shadow: 0 -10px 40px rgb(0 0 0 / 0.55); }
.ticket-id::before { padding-inline-start: 0.6rem; }

/* ============================================================================
   START vs REDEEM — two different controls, so a guest never confuses them.
   Start is the arcade cabinet's PRESS START: ember rail, the label blinks.
   Redeem is a finishing move: a segmented power meter that is always charging,
   a blood-red knob, and the fill burns gold to red as it is dragged.
   ========================================================================= */
.start-block .swipe__label { animation: press-start 1.2s steps(2, jump-none) infinite; }
@keyframes press-start { 0% { opacity: 1; } 100% { opacity: 0.35; } }

.stop .swipe {
  border: 2px solid #FFE7A0;
  border-radius: 4px;
  background-color: #12040A;
  background-image:
    repeating-linear-gradient(90deg, transparent 0 14px, rgb(0 0 0 / 0.85) 14px 17px),
    linear-gradient(90deg, #3A0A12, #6E0F18 55%, #8A4A10);
  background-size: auto;
  color: #FFF4E6;
  box-shadow: 0 0 0 2px #000, 0 0 22px rgb(225 28 34 / 0.45);
  overflow: hidden;
}
/* The charge: a hot band sweeps the meter on a loop, inviting the swipe. */
.stop .swipe::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(100deg, transparent 30%, rgb(255 200 90 / 0.55) 48%, rgb(255 90 18 / 0.35) 54%, transparent 70%);
  transform: translateX(-100%);
  will-change: transform;
  animation: meter-charge 2.4s var(--ease-in-out) infinite;
}
@keyframes meter-charge {
  0% { transform: translateX(-100%); }
  70%, 100% { transform: translateX(100%); }
}
.stop .swipe__fill {
  background:
    repeating-linear-gradient(90deg, transparent 0 14px, rgb(0 0 0 / 0.6) 14px 17px),
    linear-gradient(90deg, #E11C22, #FF5A12 60%, #FFD95E);
  box-shadow: 0 0 24px rgb(255 120 30 / 0.9);
}
.stop .swipe__label {
  z-index: 1;
  font-size: clamp(1.3rem, 5.6vw, 1.7rem);
  letter-spacing: 0.04em;
  background: linear-gradient(180deg, #FFE27A, #F5B91E 45%, #FF5A12);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-stroke: 1px #190403;
}
.stop .swipe__hint { color: #FFE7A0; z-index: 1; }
.stop .swipe__handle {
  border-radius: 6px;
  background: linear-gradient(180deg, #E11C22, #8E0A12);
  color: #FFE27A;
  box-shadow: inset 0 0 0 2px #190403, 0 0 18px rgb(225 28 34 / 0.85);
}
.stop .swipe__handle.is-dragging { box-shadow: inset 0 0 0 2px #190403, 0 0 34px rgb(255 140 40 / 1); }

/* The start rail keeps a slow ember pulse; it is the first thing to touch. */
.start-block .swipe[data-state="ready"] { box-shadow: 0 0 0 2px #190403, 0 0 26px rgb(255 90 18 / 0.55); animation: ember-pulse 2.6s var(--ease-in-out) infinite; }
@keyframes ember-pulse {
  0%, 100% { box-shadow: 0 0 0 2px #190403, 0 0 18px rgb(255 90 18 / 0.4); }
  50% { box-shadow: 0 0 0 2px #190403, 0 0 34px rgb(255 90 18 / 0.85); }
}

/* The stamp keeps a dull red burn after it lands. */
.stop.is-done .stop__stamp { text-shadow: 0 0 18px rgb(225 28 34 / 0.55); }

@media (prefers-reduced-motion: reduce) {
  .fightcard__smoke, .fightcard__smoke::after, .fightcard__embers, .fightcard__flash,
  .fighter, .hud__vs, .start-block .swipe__label, .stop .swipe::before,
  .start-block .swipe[data-state="ready"] { animation: none; }
}

/* ============================================================================
   THE CHARGE — the redeem rail gathers a shot, then throws it.
   Everything hangs off --charge (0 to 1), written by the drag handler, plus
   data-charge for the stage and data-launch for the throw. The rail's size,
   drag maths, validation, redemption call and redeemed state are untouched:
   this is paint. The blast is deliberately bigger than the rail and is allowed
   to spill out of it, so the card stops clipping while a rail is charging.
   ========================================================================= */
.stop .swipe { --charge: 0; overflow: visible; }
/* The two things that DO have to stay inside the rail clip themselves, now
   that the rail no longer clips its children. */
.stop .swipe::before, .stop .swipe__fill { clip-path: inset(0 round 3px); }
.stop[data-cast] { overflow: visible; }

/* The handle keeps its size and its touch target; while the shot is being
   charged it simply stops being drawn. */
.stop .swipe__handle { transition: background-color 160ms linear, box-shadow 160ms linear; }
.stop .swipe[data-charge] .swipe__handle { background: transparent; box-shadow: none; color: transparent; }
.stop .swipe[data-charge] .swipe__handle::before,
.stop .swipe[data-charge] .swipe__handle::after { opacity: 0; }

/* The blast itself: ~2.6x the rail's height, centred on the handle, blended so
   every overlapping blue layer adds up instead of flattening. */
.blast {
  position: absolute;
  z-index: calc(var(--z-base) + 3);
  inset: 50% auto auto 50%;
  width: 19rem;
  height: 11.9rem;
  margin: -5.95rem 0 0 -9.5rem;
  overflow: visible;
  pointer-events: none;
  will-change: transform, opacity;
  opacity: calc(var(--charge) * 1.2);
  scale: calc(0.5 + var(--charge) * 1.05);
  transition: opacity 180ms linear, scale 140ms var(--ease-out);
}
/* Ribbons: energy streaming in from the left, drawn as dashes running toward
   the core so the orb reads as being fed rather than simply lit. */
.blast__ribbons path {
  fill: none;
  stroke: url(#blastRibbon);
  stroke-width: calc(2 + var(--charge) * 7);
  stroke-linecap: round;
  stroke-dasharray: 26 46;
  animation: ribbon-feed 0.75s linear infinite;
  opacity: calc(0.35 + var(--charge) * 0.65);
}
.blast__ribbons path:nth-child(2) { animation-duration: 0.56s; }
.blast__ribbons path:nth-child(3) { animation-duration: 0.92s; }
.blast__ribbons path:nth-child(4) { animation-duration: 0.64s; }
@keyframes ribbon-feed { from { stroke-dashoffset: 216; } to { stroke-dashoffset: 0; } }

.blast__churn { opacity: calc(0.55 + var(--charge) * 0.45); transform-origin: 150px 100px; animation: churn 0.9s linear infinite; }
.blast__churn-b { transform-origin: 150px 100px; animation: churn 0.6s linear infinite reverse; }
@keyframes churn { to { rotate: 360deg; } }
.blast__halo { opacity: calc(0.5 + var(--charge) * 0.5); }
/* The core's glow is the halo behind it; a live drop-shadow here re-rendered
   on every pointer move. */
/* The crescent wraps the leading face of the orb, like the shock in front of
   the shot. */
.blast__crescent { transform-origin: 150px 100px; opacity: calc(var(--charge) * 0.95); }
.blast__crescent path {
  fill: none;
  stroke: rgb(225 248 255 / 0.8);
  stroke-width: calc(3 + var(--charge) * 11);
  stroke-linecap: round;
}
.blast__crescent .blast__crescent--thin { stroke-width: calc(1.5 + var(--charge) * 3.5); stroke: rgb(150 220 255 / 0.55); }
.blast__arcs path {
  fill: none;
  stroke: #E8FAFF;
  stroke-width: 2.4;
  stroke-linejoin: round;
  opacity: 0;
  transform-origin: 150px 100px;
}
.blast__sparks circle {
  fill: #DFF6FF;
  /* The sparks are pulled into the core as the charge climbs: a suction, not a
     sprinkle. */
  transform-origin: 150px 100px;
  scale: calc(1 - var(--charge) * 0.55);
  translate: calc((150px - var(--sx, 150px)) * var(--charge) * 0.6) 0;
  opacity: calc(var(--charge) * 1.1);
  animation: spark-jitter 0.5s steps(3, jump-none) infinite;
}
.blast__sparks circle:nth-child(odd) { animation-duration: 0.37s; }
@keyframes spark-jitter {
  0% { translate: 0 0; } 33% { translate: -3px 2px; } 66% { translate: 2px -3px; } 100% { translate: 0 0; }
}

/* Peak: the whole shot boils, the crescent turns, the rail shakes. */
.stop .swipe[data-charge="peak"] .blast__body { animation: blast-throb 0.42s var(--ease-in-out) infinite; transform-origin: 150px 100px; }
.stop .swipe[data-charge="peak"] .blast__crescent { animation: crescent-turn 2.1s linear infinite; }
.stop .swipe[data-charge="peak"] .blast__arcs path { opacity: 0.9; animation: arc-flicker 0.14s steps(2, jump-none) infinite; }
.stop .swipe[data-charge="peak"] { animation: rail-shake 90ms linear infinite; }
@keyframes blast-throb { 0%, 100% { scale: 1; } 50% { scale: 1.09; } }
@keyframes crescent-turn { from { rotate: 0deg; } to { rotate: 360deg; } }
@keyframes arc-flicker { 0% { opacity: 0.25; scale: 0.94; } 100% { opacity: 1; scale: 1.08; } }
@keyframes rail-shake {
  0%, 100% { translate: 0 0; } 25% { translate: -1px 1px; } 75% { translate: 1px -1px; }
}
/* The idle charge sweep gets out of the way once the shot is building. */
.stop .swipe[data-charge="draw"]::before, .stop .swipe[data-charge="peak"]::before { opacity: 0; }

/* The throw: the shot leaves the hand, stretches as it goes, and is gone. */
.stop .swipe[data-launch] .blast { animation: blast-throw 540ms cubic-bezier(.3,0,.1,1) forwards; }
.stop .swipe[data-launch] .blast__ribbons { opacity: 0; transition: opacity 120ms linear; }
@keyframes blast-throw {
  0% { translate: 0 0; scale: 1.1; opacity: 1; }
  22% { scale: 1.3 1.1; }
  70% { opacity: 1; }
  100% { translate: 86vw 0; scale: 2.1 0.55; opacity: 0; }
}
/* Impact at the right edge: a white-blue flash and a ring that opens outward. */
.stop .swipe[data-launch="hit"]::after {
  content: "";
  position: absolute;
  z-index: calc(var(--z-base) + 4);
  inset: 50% -1.5rem auto auto;
  width: 3rem;
  height: 3rem;
  margin-block-start: -1.5rem;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, #FFFFFF 0 28%, #9BE4FF 52%, rgb(46 123 255 / 0) 76%);
  mix-blend-mode: screen;
  animation: impact-ring 420ms var(--ease-out) forwards;
}
@keyframes impact-ring {
  0% { scale: 0.3; opacity: 1; }
  100% { scale: 3.4; opacity: 0; }
}
.stop .swipe[data-launch="hit"] { animation: impact-flash 260ms var(--ease-out) forwards; }
@keyframes impact-flash {
  0% { filter: brightness(1); } 22% { filter: brightness(2.8) saturate(0.5); } 100% { filter: brightness(1); }
}

@media (prefers-reduced-motion: reduce) {
  /* No turbulence and no throw: the rail lights blue as it is dragged, and the
     redemption lands on the stamp as usual. */
  .blast__ribbons, .blast__sparks, .blast__arcs, .blast__crescent { display: none; }
  .blast, .blast__body, .stop .swipe[data-charge="peak"], .stop .swipe[data-launch="hit"],
  .stop .swipe[data-launch] .blast { animation: none; }
  .blast { scale: 0.75; transition: opacity 220ms linear; }
  .stop .swipe[data-launch="hit"]::after { animation: none; opacity: 0; }
}

/* ---- The caster ----------------------------------------------------------
   A Higgsfield render of the crawl's blue fighter, keyed out and cut into two
   sheets: 31 frames of the wind-up, 17 of the throw. The wind-up is SCRUBBED,
   not played: --cast-frame is written by the drag, so pulling the handle draws
   his arms back and easing off puts them forward again. The throw plays once,
   on release, and he is gone by the time the stamp lands. He stands on the card
   beside the rail so he can be taller than it. */
.stop { --cast-frame: 0; }
.fighter-cast {
  position: absolute;
  z-index: calc(var(--z-base) + 4);
  inset: auto auto calc(var(--space-sm) * -1) calc(var(--space-sm) * -1);
  width: 160px;
  height: 238px;
  pointer-events: none;
  background-image: url("./assets/fighter-charge.webp");
  background-repeat: no-repeat;
  background-size: 4800px 238px;
  background-position-x: calc(var(--cast-frame) * -160px);
  opacity: 0;
  transition: opacity 180ms linear;
  will-change: opacity;
}
/* He appears as soon as the rail is touched and holds while it is charging. */
.stop[data-cast] .fighter-cast { opacity: 1; }

/* The throw: its own sheet, stepped through once, then he fades out. */
/* The throw plays on a copy that has been lifted onto the page, so the
   re-render that follows the redemption cannot take it down mid-swing. */
.fighter-cast--throw {
  z-index: calc(var(--z-modal) - 1);
  opacity: 1;
  background-image: url("./assets/fighter-throw.webp");
  background-size: 4480px 238px;
  background-position-x: 0;
  animation: cast-throw 640ms steps(27) forwards, cast-exit 280ms 900ms linear forwards;
}
/* 28 frames, so 27 steps from the first to the last: ending ON the last frame,
   not one frame past the end of the sheet, keeps the follow-through on screen. */
@keyframes cast-throw { to { background-position-x: -4320px; } }
@keyframes cast-exit { to { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  /* No wind-up and no throw: he holds the charged pose while the rail is
     dragged, which is the one frame that explains what the control does. */
  .fighter-cast { background-position-x: -4640px; }
  .fighter-cast--throw { animation: none; opacity: 0; }
}

/* ============================================================================
   ONE CARD PER BAR. A bar's perks (Free Entry, Welcome Shot, a BOGO) sit inside
   its card, each with its own ribbon and its own swipe, so any perk can be used
   first. The ribbon keeps the old position — starting under the name column and
   bleeding to the card edge — while the rail stays full width for the thumb.
   ========================================================================= */
.perk { margin-block-start: var(--space-md); }
.perk .stop__offer { margin-block-start: 0; margin-inline: 4.75rem calc(var(--space-lg) * -1); }
.perk .swipe { margin-block-start: var(--space-sm); }
.perk.is-done .stop__offer { background: linear-gradient(90deg, #4A2018, #36170F); color: var(--color-ink-2); }
.perk.is-closed .stop__offer { background: var(--color-paper-3); color: var(--color-muted); }
.perk__used { margin: var(--space-sm) 0 0; color: var(--color-ink-2); font-family: var(--font-display); font-size: var(--text-xs); font-weight: 900; letter-spacing: 0.12em; text-transform: uppercase; }
@media (max-width: 23rem) {
  .perk .stop__offer { margin-inline: 3.75rem calc(var(--space-md) * -1); }
}

/* The caster belongs to one perk's rail. He stands on that perk, shows only while
   that perk's rail is charging, and the card stops clipping him meanwhile. */
.perk { position: relative; }
.perk[data-cast] .fighter-cast { opacity: 1; }
.stop[data-cast] .perk:not([data-cast]) .fighter-cast { opacity: 0; }

/* Practice passes only: lets whoever is trying the pass wipe their swipes and go
   again. Sits over the fight card's top edge, out of the way of the HUD. */
.practice-reset {
  position: absolute;
  z-index: calc(var(--z-base) + 6);
  inset: max(var(--space-sm), env(safe-area-inset-top)) var(--space-md) auto auto;
  min-height: 2.75rem;
  padding: 0 var(--space-md);
  border: 1px solid var(--color-gold);
  border-radius: 999px;
  background: rgb(20 8 14 / 0.82);
  color: var(--color-gold);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 900;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
}
.practice-reset:disabled { opacity: 0.6; cursor: progress; }
.practice-reset[hidden] { display: none; }

/* No layout shift when a perk is used: the redeemed slot keeps the rail's exact
   height, so the card never shrinks mid-throw and nothing below it jumps. The
   finished card keeps its height too — the stamp lands over the last slot. */
.perk__used {
  display: grid; place-items: center;
  min-height: 5.25rem; box-sizing: border-box;
  border: var(--rule) dashed color-mix(in oklch, var(--color-ink-2), transparent 55%);
  border-radius: var(--radius-control);
  background: rgb(0 0 0 / 0.18);
}
.stop.is-done { padding-block-end: var(--space-xl); }
