/* ---------------------------------------------------------------------------
   Seraphrasia launcher — design tokens
   Dark, brand-gold, "arcane" surface treatment. Every colour used in a
   component comes from a semantic token here, never a raw hex.
--------------------------------------------------------------------------- */
:root {
  color-scheme: dark;

  /* Surfaces */
  --bg: #0c0d12;
  --surface: #15171f;
  --surface-raised: #1d202a;
  --surface-sunken: #0f1016;
  --panel: var(--surface);
  --panel-soft: #262a36;
  --line: rgba(255, 255, 255, 0.09);
  --line-strong: rgba(255, 255, 255, 0.17);

  /* Content */
  --text: #f4f4f7;
  --muted: #a9adbb;

  /* Brand + semantic */
  --accent: #d5b36a;
  --accent-strong: #efd18a;
  --accent-wash: rgba(213, 179, 106, 0.13);
  --on-accent: #1b170e;
  --success: #86d3a2;
  --warn: #e7c07a;
  --info: #96bcea;
  --danger: #e88989;

  /* Typography */
  --font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Spacing — 4pt scale */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;
  --s-8: 64px;

  /* Radius + elevation */
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 22px;
  --r-pill: 999px;
  --e-1: 0 1px 2px rgba(0, 0, 0, 0.4);
  --e-2: 0 12px 34px rgba(0, 0, 0, 0.3);
  --e-3: 0 28px 80px rgba(0, 0, 0, 0.38);

  /* Motion */
  --dur-fast: 130ms;
  --dur: 220ms;
  --dur-slow: 340ms;
  /* Campaign-change rhythm. These three are deliberately close together so the
     backdrop, the panel tints and the centre content read as one dissolve
     rather than three separate events. */
  --dur-theme: 1000ms;
  /* One shared phase scale: whatever is changing fades fully out, holds empty
     for a beat, then fades fully in. Used by both the centre copy and the
     backdrop so the two stay in step. JS reads these to time the backdrop. */
  --dur-fade-out: 450ms;
  --dur-fade-gap: 60ms;
  --dur-fade-in: 600ms;
  --ease-out: cubic-bezier(0.22, 0.9, 0.28, 1);
  /* Symmetric, for cross-dissolves. `--ease-out` is heavily front-loaded, which
     makes a long dissolve still look abrupt. */
  --ease-dissolve: cubic-bezier(0.45, 0, 0.55, 1);

  /* Layering */
  --z-base: 0;
  --z-raised: 10;
  --z-overlay: 40;

  /* Campaign theming — see the palette blocks below. `--tint` is the colour
     the glass panels are stained with, `--halo` the glow inside the carousel,
     and the scrims darken the backdrop behind the whole page. */
  --tint: #1d202a;
  --halo: rgba(213, 179, 106, 0.13);
  --scrim-top: rgba(12, 13, 18, 0.36);
  --scrim-bottom: rgba(12, 13, 18, 0.72);
  /* Hero copy sits on the raw backdrop rather than on glass, so it carries a
     local scrim of its own. Bright campaigns need a much stronger one. */
  --hero-scrim: rgba(6, 7, 10, 0.5);
}

/* Registering these as colours lets them interpolate, so a campaign change
   crossfades the whole UI instead of snapping. */
@property --tint { syntax: "<color>"; inherits: true; initial-value: #1d202a; }
@property --accent { syntax: "<color>"; inherits: true; initial-value: #d5b36a; }
@property --accent-strong { syntax: "<color>"; inherits: true; initial-value: #efd18a; }
@property --accent-wash { syntax: "<color>"; inherits: true; initial-value: rgba(213, 179, 106, 0.13); }
@property --halo { syntax: "<color>"; inherits: true; initial-value: rgba(213, 179, 106, 0.13); }
@property --on-accent { syntax: "<color>"; inherits: true; initial-value: #1b170e; }
@property --scrim-top { syntax: "<color>"; inherits: true; initial-value: rgba(12, 13, 18, 0.36); }
@property --scrim-bottom { syntax: "<color>"; inherits: true; initial-value: rgba(12, 13, 18, 0.72); }
@property --hero-scrim { syntax: "<color>"; inherits: true; initial-value: rgba(6, 7, 10, 0.5); }

:root {
  transition:
    --tint var(--dur-theme) var(--ease-out), --accent var(--dur-theme) var(--ease-out),
    --accent-strong var(--dur-theme) var(--ease-out), --accent-wash var(--dur-theme) var(--ease-out),
    --halo var(--dur-theme) var(--ease-out), --on-accent var(--dur-theme) var(--ease-out),
    --scrim-top var(--dur-theme) ease, --scrim-bottom var(--dur-theme) ease, --hero-scrim var(--dur-theme) ease;
}

/* Veil of Sorrow — sun-bleached dunes. The brightest backdrop by far
   (mean lightness 55%), so it carries the heaviest scrim of the three. */
:root[data-campaign="gov"] {
  --tint: #3a2c1c;
  --accent: #e0b878;
  --accent-strong: #f7dfab;
  --accent-wash: rgba(224, 184, 120, 0.16);
  --halo: rgba(247, 223, 171, 0.16);
  --on-accent: #251a08;
  --scrim-top: rgba(10, 8, 5, 0.58);
  --scrim-bottom: rgba(8, 7, 4, 0.8);
  --hero-scrim: rgba(4, 3, 2, 0.85);
}

/* Echoes of War — deep teal cave. Accent picks up the bioluminescent
   mushrooms; jade rather than green so it stays distinct from the success
   colour used by the status dot. */
:root[data-campaign="world"] {
  --tint: #16302b;
  --accent: #63cbb8;
  --accent-strong: #9fe6d6;
  --accent-wash: rgba(99, 203, 184, 0.14);
  --halo: rgba(120, 224, 190, 0.13);
  --on-accent: #06201b;
  --scrim-top: rgba(6, 12, 12, 0.3);
  --scrim-bottom: rgba(4, 9, 9, 0.68);
  --hero-scrim: rgba(3, 7, 7, 0.5);
}

/* Flicker of Home — cold moonlit alley. The accent is the single lit window,
   deliberately warm against the blue tint so the CTA reads as the one warm
   thing on the page. */
:root[data-campaign="flicker-of-home"] {
  --tint: #1b2b3a;
  --accent: #efa646;
  --accent-strong: #ffc781;
  --accent-wash: rgba(239, 166, 70, 0.15);
  --halo: rgba(255, 190, 120, 0.14);
  --on-accent: #291705;
  --scrim-top: rgba(6, 9, 14, 0.34);
  --scrim-bottom: rgba(5, 8, 12, 0.7);
  --hero-scrim: rgba(3, 6, 10, 0.52);
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  position: relative;
  isolation: isolate;
  margin: 0;
  min-height: 100dvh;
  background:
    radial-gradient(70rem 40rem at 12% -8%, rgba(96, 116, 176, 0.16), transparent 70%),
    radial-gradient(50rem 34rem at 92% 4%, rgba(213, 179, 106, 0.09), transparent 70%),
    var(--bg);
  background-attachment: fixed;
  color: var(--text);
  font: 16px/1.6 var(--font-body);
  -webkit-font-smoothing: antialiased;
}

/* ------------------------------------------------------------- atmosphere */
.campaign-backdrop {
  position: fixed;
  z-index: var(--z-base);
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  transform: scale(1.025);
}
/* One plate per image. The incoming plate is stacked on top and fades up over
   the outgoing one, so the composite never dips toward the page background
   mid-transition the way two opposing fades would. */
.backdrop-plate {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    /* Top band: the hero copy sits on raw art rather than on glass. */
    linear-gradient(180deg, var(--hero-scrim) 0, transparent 58%),
    linear-gradient(180deg, var(--scrim-top), var(--scrim-bottom)),
    var(--backdrop-image, none) center / cover no-repeat,
    var(--bg);
  opacity: 0;
  /* The base rule times the fade OUT (when .is-active is removed); the class
     times the fade IN. */
  transition: opacity var(--dur-fade-out) var(--ease-dissolve);
}
.backdrop-plate.is-active { opacity: 1; transition-duration: var(--dur-fade-in); }
/* ::before would paint underneath the plates (it is the first child), so the
   vignette and the edge darkening are combined into ::after, which paints on
   top of them. */
.campaign-backdrop::after {
  position: absolute;
  inset: 0;
  content: "";
  pointer-events: none;
  background:
    radial-gradient(ellipse at center, transparent 32%, rgba(4, 5, 9, 0.26) 100%),
    linear-gradient(90deg, rgba(4, 5, 9, 0.22), transparent 26%, transparent 74%, rgba(4, 5, 9, 0.24));
}
/* ------------------------------------------------------------------ layout */
.shell { position: relative; z-index: var(--z-raised); width: min(1040px, calc(100% - 2 * var(--s-4))); margin: 0 auto; padding: var(--s-8) 0 var(--s-7); }
.hero { max-width: 760px; margin-bottom: var(--s-6); }
.topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--s-4); margin-bottom: var(--s-6); }
.topbar .eyebrow { margin: 0; }

/* -------------------------------------------------------------- typography */
h1, h2, h3, p { margin-top: 0; }
h1, h2, h3 { font-family: var(--font-display); font-weight: 600; }
h1 { margin-bottom: var(--s-3); font-size: clamp(38px, 6vw, 62px); line-height: 1.04; letter-spacing: -0.025em; }
h2 { margin-bottom: 0; font-size: 25px; letter-spacing: -0.015em; }
.eyebrow {
  margin: 0 0 var(--s-2);
  color: var(--accent-strong);
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.lede { max-width: 62ch; color: var(--muted); font-size: 18px; }
.muted, footer { color: var(--muted); }
/* Components below set display explicitly, so both hiding mechanisms need
   to outrank them. */
.hidden, [hidden] { display: none !important; }
footer { margin-top: var(--s-6); font-size: 13px; }
.text-link {
  border-radius: var(--r-sm);
  color: var(--accent-strong);
  font-size: 14px;
  font-weight: 600;
  text-underline-offset: 4px;
  transition: color var(--dur-fast) var(--ease-out);
}
.text-link:hover { color: var(--text); }

/* ------------------------------------------------------------------ panels */
/* Glass panels: a campaign-tinted wash over a dark floor. The floor is what
   keeps body text legible no matter how bright the backdrop behind it is. */
.status-card,
.gm-panel {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background:
    linear-gradient(155deg, color-mix(in srgb, var(--tint) 62%, transparent), color-mix(in srgb, var(--tint) 78%, transparent)),
    rgba(10, 11, 15, 0.55);
  backdrop-filter: blur(10px);
  box-shadow: var(--e-2);
  transition: border-color var(--dur-theme) var(--ease-out);
}
.status-card { padding: var(--s-5); color: var(--muted); }
.status-card strong { color: var(--text); }

.status-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--s-4); }
.status-heading .eyebrow { margin-bottom: var(--s-1); }
.status-title { display: flex; align-items: center; gap: var(--s-2); font-family: var(--font-display); font-size: 22px; color: var(--text); }

/* Portal location is carried by text + dot shape/colour, never colour alone. */
.status-dot {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--muted);
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.05);
}
.status-dot[data-phase="running"],
.status-dot[data-phase="world"] { background: var(--success); box-shadow: 0 0 0 4px rgba(134, 211, 162, 0.16); }
.status-dot[data-phase="starting"],
.status-dot[data-phase="stopping"],
.status-dot[data-phase="backing_up"],
.status-dot[data-phase="synchronizing"] { background: var(--warn); box-shadow: 0 0 0 4px rgba(231, 192, 122, 0.16); animation: dot-pulse 1.6s ease-in-out infinite; }
.status-dot[data-phase="setup"],
.status-dot[data-phase="astral_sea"] { background: var(--info); box-shadow: 0 0 0 4px rgba(150, 188, 234, 0.16); }
.status-dot[data-phase="error"],
.status-dot[data-phase="uncertain"] { background: var(--danger); box-shadow: 0 0 0 4px rgba(232, 137, 137, 0.16); }
@keyframes dot-pulse { 50% { opacity: 0.35; } }

.status-message { margin: var(--s-4) 0 var(--s-2); color: var(--text); }
.status-current { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin: 0; color: var(--muted); font-size: 13px; }
.status-current strong { color: var(--text); }
.status-connections {
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--tint) 60%, rgba(6, 7, 10, 0.7));
  color: var(--muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.status-meta { margin: var(--s-3) 0 0; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.status-error { margin-top: var(--s-2); color: var(--danger); font-size: 13px; }

.progress-wrap { display: grid; gap: var(--s-2); margin-top: var(--s-3); color: var(--accent-strong); font-size: 13px; }
.progress-copy { display: flex; justify-content: space-between; gap: var(--s-3); }
.progress-copy span:last-child { color: var(--muted); }
.progress-track { height: 6px; overflow: hidden; border-radius: var(--r-pill); background: rgba(255, 255, 255, 0.08); }
.progress-bar {
  width: 38%;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent), var(--accent-strong));
  animation: progress-slide 1.5s var(--ease-out) infinite;
}
@keyframes progress-slide {
  0% { transform: translateX(-120%); }
  50% { transform: translateX(170%); }
  100% { transform: translateX(300%); }
}

/* ------------------------------------------------------------------ badges */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  width: fit-content;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  background: var(--panel-soft);
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.badge.active { background: var(--accent-wash); color: var(--accent-strong); }
.badge.blocked { background: rgba(232, 137, 137, 0.12); color: var(--danger); }

/* --------------------------------------------------------- forms + buttons */
label { display: grid; gap: var(--s-2); color: var(--muted); font-size: 13px; font-weight: 600; }
input, select {
  width: 100%;
  min-height: 46px;
  padding: 11px var(--s-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--tint) 45%, rgba(6, 7, 10, 0.86));
  color: var(--text);
  font: inherit;
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
input::placeholder { color: rgba(169, 173, 187, 0.6); }
/* The accent ring is the focus indicator for fields — no second outline. */
input:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }

button {
  position: relative;
  min-height: 44px;
  padding: 11px var(--s-4);
  border: 0;
  border-radius: var(--r-sm);
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  touch-action: manipulation;
  /* `scale` is animated as its own property so it never fights a `translate`
     used for positioning (see .carousel-arrow). */
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), scale var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out);
}
button:hover:not(:disabled) { background: var(--accent-strong); box-shadow: var(--e-1); }
button:active:not(:disabled) { scale: 0.97; }
button:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent-strong) 72%, transparent); outline-offset: 3px; }
button:disabled { opacity: 0.42; cursor: not-allowed; }
button.secondary { border: 1px solid var(--line-strong); background: color-mix(in srgb, var(--tint) 84%, transparent); color: var(--text); }
button.secondary:hover:not(:disabled) { border-color: color-mix(in srgb, var(--accent) 40%, var(--line-strong)); background: color-mix(in srgb, var(--tint) 96%, white 8%); }
button.danger { background: rgba(232, 137, 137, 0.16); color: #ffc5c5; }
button.danger:hover:not(:disabled) { background: rgba(232, 137, 137, 0.32); }
button.is-busy { pointer-events: none; cursor: wait; }
button.is-busy::after {
  content: "";
  display: inline-block;
  width: 13px;
  height: 13px;
  margin-left: var(--s-2);
  vertical-align: -2px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: button-spin 0.7s linear infinite;
}
button.button-clicked { animation: button-click var(--dur) var(--ease-out); }
@keyframes button-click {
  0% { scale: 1; }
  45% { scale: 0.96; }
  100% { scale: 1; }
}
@keyframes button-spin { to { transform: rotate(360deg); } }

/* --------------------------------------------------------------- GM shared */
.gm-panel { margin-top: var(--s-6); padding: var(--s-5); }
.section-heading, .control-row { display: flex; align-items: end; justify-content: space-between; gap: var(--s-3); }
.login-form, .gm-controls { margin-top: var(--s-5); }
.login-form { display: grid; grid-template-columns: 1fr 1fr auto; align-items: end; gap: var(--s-3); }
.gm-controls { display: grid; gap: var(--s-4); }
.control-row { justify-content: flex-start; flex-wrap: wrap; }
.control-row label { min-width: 220px; }
.output {
  min-height: 28px;
  margin: 0;
  padding: var(--s-3);
  border-radius: var(--r-sm);
  background: var(--surface-sunken);
  color: var(--accent-strong);
  white-space: pre-wrap;
}
.output:empty { display: none; }

/* ------------------------------------------------------- campaign carousel */
.player-shell { width: min(960px, calc(100% - 2 * var(--s-4))); }
.campaign-section { margin-top: var(--s-6); }
.player-section-heading { align-items: end; margin-bottom: var(--s-4); }

.campaign-carousel {
  position: relative;
  min-height: 400px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  /* Thinner than the status card — this is the hero, so more of the campaign
     art is allowed through. */
  background:
    radial-gradient(36rem 22rem at 50% -10%, var(--halo), transparent 72%),
    linear-gradient(155deg, color-mix(in srgb, var(--tint) 52%, transparent), color-mix(in srgb, var(--tint) 70%, transparent)),
    rgba(10, 11, 15, 0.45);
  backdrop-filter: blur(8px);
  box-shadow: var(--e-3);
  transition: border-color var(--dur-theme) var(--ease-out);
}
/* Hairline top edge — reads as light catching the rim of the panel. */
.campaign-carousel::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--accent-strong) 45%, transparent), transparent);
}
.campaign-carousel.is-live { border-color: color-mix(in srgb, var(--accent) 34%, transparent); }

.campaign-slide {
  display: flex;
  min-height: 400px;
  align-items: center;
  justify-content: center;
  padding: var(--s-7) 88px var(--s-8);
  text-align: center;
}
.campaign-content { display: grid; width: min(100%, 560px); justify-items: center; gap: var(--s-3); }
/* Applied to the slide container rather than the content element. The 10s
   status poll replaces the content's markup, which would restart or cancel an
   animation living on it; the container survives those re-renders. */
.campaign-slide.is-arriving {
  animation: slide-in var(--dur-fade-in) var(--ease-dissolve) calc(var(--dur-fade-out) + var(--dur-fade-gap)) backwards;
}
@keyframes slide-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}
/* A copy of the outgoing campaign, stacked over the incoming one so the two
   cross-dissolve. Carries .campaign-slide as well, to inherit its padding and
   the locked/mobile variants of it. Exit is shorter than entry. */
.campaign-ghost {
  position: absolute;
  inset: 0;
  pointer-events: none;
  animation: ghost-out var(--dur-fade-out) var(--ease-dissolve) forwards;
}
.campaign-ghost .campaign-content { animation: none; }
@keyframes ghost-out {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateY(-6px); }
}
.campaign-index {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
.campaign-live {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  padding: 5px 12px;
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  border-radius: var(--r-pill);
  background: var(--accent-wash);
  color: var(--accent-strong);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.campaign-live .status-dot { width: 8px; height: 8px; box-shadow: none; }
.campaign-content h3 { margin: 0; font-size: clamp(32px, 5vw, 50px); line-height: 1.06; letter-spacing: -0.03em; }
.campaign-status { margin: 0; max-width: 44ch; color: var(--muted); font-size: 17px; }
.campaign-code-field { width: min(100%, 360px); margin-top: var(--s-1); text-align: left; }
.campaign-code-field input { min-height: 48px; letter-spacing: 0.06em; }
.campaign-hint { margin: 0; color: var(--muted); font-size: 12px; font-weight: 400; }
.campaign-action { min-width: 190px; min-height: 50px; margin-top: var(--s-1); font-size: 16px; }
.campaign-action:hover:not(:disabled) { scale: 1.02; box-shadow: 0 10px 26px color-mix(in srgb, var(--accent) 26%, transparent); }
.campaign-error { max-width: 42ch; margin: 0; color: var(--danger); font-size: 13px; }
.campaign-empty { align-self: center; max-width: 440px; text-align: center; }
.campaign-empty h3 { margin-bottom: var(--s-2); font-size: 28px; }
.campaign-empty p { margin: 0; color: var(--muted); }

/* Arrows are centred with `translate`, so press feedback must not touch
   `transform` — otherwise the button jumps out of the vertical centre. */
.carousel-arrow {
  position: absolute;
  z-index: var(--z-raised);
  top: 50%;
  translate: 0 -50%;
  display: grid;
  place-items: center;
  width: 46px;
  min-height: 46px;
  height: 46px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: color-mix(in srgb, var(--tint) 64%, rgba(8, 9, 13, 0.6));
  color: var(--muted);
  backdrop-filter: blur(6px);
}
.carousel-arrow svg { width: 20px; height: 20px; }
.carousel-arrow:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-wash); color: var(--accent-strong); box-shadow: none; }
.carousel-arrow:active:not(:disabled) { scale: 1; background: color-mix(in srgb, var(--accent) 26%, transparent); }
.carousel-arrow.button-clicked { animation: none; }
.carousel-arrow.previous { left: var(--s-5); }
.carousel-arrow.next { right: var(--s-5); }

.campaign-dots { position: absolute; right: 0; bottom: var(--s-4); left: 0; display: flex; justify-content: center; gap: var(--s-1); }
/* 10px dot inside a 44px hit area — meets the touch-target minimum without
   a chunky visual. */
.campaign-dot {
  width: 44px;
  min-height: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--r-pill);
  background: none;
}
.campaign-dot::before {
  content: "";
  display: block;
  width: 10px;
  height: 10px;
  margin: 0 auto;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.26);
  transition: background-color var(--dur-fast) var(--ease-out), scale var(--dur-fast) var(--ease-out);
}
.campaign-dot:hover:not(:disabled) { background: none; box-shadow: none; }
.campaign-dot:hover:not(:disabled)::before { background: var(--accent-strong); }
.campaign-dot:active:not(:disabled) { scale: 1; }
.campaign-dot.selected::before { background: var(--accent-strong); scale: 1.15; box-shadow: 0 0 0 4px var(--accent-wash); }
.campaign-dot.button-clicked { animation: none; }

/* Locked to the live world: switching controls are removed, not just hidden,
   so keyboard and screen-reader users don't land on dead affordances. */
.campaign-carousel.is-locked .campaign-slide { padding-inline: var(--s-5); padding-bottom: var(--s-7); }

/* ------------------------------------------------------------- responsive */
@media (max-width: 820px) {
  .shell { padding-top: var(--s-6); }
  .login-form { grid-template-columns: 1fr; }
  .section-heading { flex-direction: column; align-items: flex-start; }
}
@media (max-width: 680px) {
  body { font-size: 16px; }
  .topbar { margin-bottom: var(--s-5); }
  .player-section-heading { align-items: flex-start; }
  .campaign-carousel, .campaign-slide { min-height: 380px; }
  .campaign-slide { padding: var(--s-6) var(--s-6) var(--s-7); }
  .carousel-arrow { width: 44px; height: 44px; }
  .carousel-arrow.previous { left: var(--s-2); }
  .carousel-arrow.next { right: var(--s-2); }
  .campaign-action { width: 100%; }
  .progress-copy { display: grid; gap: 2px; }
  .status-heading { flex-wrap: wrap; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    /* Delays must go too. The fade-in is delayed past the fade-out, and zeroing
       only the duration would leave the content held at opacity 0 for the whole
       delay — a blank slide rather than a reduced animation. */
    animation-delay: 0ms !important;
    transition-delay: 0ms !important;
  }
}
