:root {
  --bg: #06070f;
  --text: #f5f7fb;
  --text-muted: #8b93a8;
  --accent-coral: #ff5f6d;
  --accent-coral-2: #e6435a;
  --accent-cyan: #4dd8e6;
  --accent-cyan-2: #2bb8c9;
  --accent-purple: #7c8cff;
  --accent-yellow: #ffd23f;
  --accent-green: #4ade80;
  --glass: rgba(255, 255, 255, 0.045);
  --glass-strong: rgba(255, 255, 255, 0.07);
  --border-soft: rgba(255, 255, 255, 0.09);
  --shadow-dark: rgba(0, 0, 0, 0.5);

  /* Tactile glossy-plastic card recipe, reused across the whole app */
  --card-sheen: linear-gradient(165deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.02) 32%, transparent 58%);
  --card-base: linear-gradient(160deg, #1c2138 0%, #12152a 55%, #0b0d18 100%);
  --card-base-soft: linear-gradient(160deg, #171b30 0%, #101323 88%, #0b0d18 100%);
  --card-border: rgba(255, 255, 255, 0.07);
  --card-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset, 0 16px 32px rgba(0, 0, 0, 0.45), 0 3px 6px rgba(0, 0, 0, 0.35);
  --card-shadow-soft: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 10px 22px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);

  /* --- Motion system -----------------------------------------------------
     One easing family and one timing scale for the whole app, so a modal,
     a screen change and a section swap all feel like the same gesture at
     different sizes. Nothing here is the `ease`/`linear` keyword: every
     curve either decelerates into its resting state (arrivals) or
     accelerates away from it (departures), which is what stops motion
     reading as robotic or stopping abruptly.

     The one deliberate exception is perpetual rotation (orbits, the
     galaxy, planet spin): a looping 0->360deg turn MUST be linear, because
     any eased curve restarts at full speed each cycle and visibly hitches
     once per lap. Those keep `linear` on purpose. */
  --ease-enter: cubic-bezier(0.16, 0.72, 0.24, 1);      /* arrives and settles */
  --ease-exit: cubic-bezier(0.45, 0.02, 0.75, 0.35);    /* leaves, accelerating away */
  --ease-soft: cubic-bezier(0.37, 0, 0.63, 1);          /* symmetric sine, for ambient loops */
  --ease-press: cubic-bezier(0.2, 0.72, 0.3, 1);        /* tap feedback */
  --ease-flight: cubic-bezier(0.5, 0, 0.15, 1);         /* something travelling a long way */

  --t-press: 120ms;        /* :active feedback */
  --t-hint: 180ms;         /* chrome: borders, focus rings, fades */
  --t-out: 190ms;          /* a screen or modal leaving */
  --t-in: 300ms;           /* a screen or modal arriving */
  --t-swap-out: 150ms;     /* in-place content leaving */
  --t-swap-in: 250ms;      /* in-place content arriving */
  --t-flight: 460ms;       /* the avatar's punch up / flight back */
  --t-fall: 1250ms;        /* a captured planet's spiral into the core */

  /* Morph travel. Restrained on purpose — a screen barely moves, a small
     element is allowed a little more. */
  --morph-scale-view: 0.97;
  --morph-scale-item: 0.94;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

html, body {
  background-color: var(--bg);
}

html {
  overflow-x: hidden;
  /* Scrolling itself stays enabled (needed for tall screens/keyboards) --
     this only hides the browser-drawn scrollbar track/thumb, top-to-bottom
     and side-to-side, everywhere in the app. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

html::-webkit-scrollbar {
  display: none;
}

body {
  color: var(--text);
  display: flex;
  justify-content: center;
  min-height: 100vh;
  position: relative;
  overflow-x: hidden;
  /* App-like feel: only vertical scrolling/panning on the page itself.
     Elements that are deliberately horizontally scrollable (session tabs,
     history round tabs) opt back into horizontal panning individually. */
  touch-action: pan-y;
  /* Both axes, not just x: Chrome's pull-to-refresh lives on the y
     overscroll, and in a WebView a downward swipe at the top of a round
     reloaded the whole app mid-game. */
  overscroll-behavior: none;
}

/* Starfield + nebula glow, fixed behind all content */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  background-image:
    radial-gradient(1.6px 1.6px at 20px 30px, #ffffffaa, transparent),
    radial-gradient(1.2px 1.2px at 90px 120px, #ffffff88, transparent),
    radial-gradient(1.8px 1.8px at 150px 60px, #ffffff66, transparent),
    radial-gradient(1.2px 1.2px at 210px 190px, #ffffff99, transparent),
    radial-gradient(1.4px 1.4px at 60px 170px, #ffffff77, transparent),
    radial-gradient(1.6px 1.6px at 260px 40px, #ffffff88, transparent);
  background-repeat: repeat;
  background-size: 280px 280px;
  opacity: 0.7;
  z-index: 0;
  pointer-events: none;
}

body::after {
  content: '';
  position: fixed;
  inset: -20%;
  background:
    radial-gradient(circle at 25% 8%, rgba(124, 140, 255, 0.22), transparent 55%),
    radial-gradient(circle at 85% 75%, rgba(77, 216, 230, 0.14), transparent 50%),
    radial-gradient(circle at 10% 85%, rgba(255, 95, 109, 0.10), transparent 45%);
  z-index: 0;
  pointer-events: none;
}

/* Occasional falling stars, staggered so only one streaks by at a time */
.shooting-stars {
  position: fixed;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
}

.shooting-stars span {
  position: absolute;
  width: 2px;
  height: 2px;
  background: #eaf6ff;
  border-radius: 50%;
  opacity: 0;
  box-shadow: 0 0 6px 1.5px rgba(210, 240, 255, 0.95), 0 0 16px 3px rgba(120, 190, 255, 0.45);
  animation-name: shooting-star-fall;
  animation-iteration-count: infinite;
  /* Per-keyframe curves below; nothing global, so the long invisible wait
     between streaks can't get eased. */
  animation-timing-function: linear;
}

.shooting-stars span::before {
  content: '';
  position: absolute;
  top: 50%;
  right: 0;
  width: 150px;
  height: 1px;
  background: linear-gradient(90deg, rgba(225, 245, 255, 0.9), rgba(150, 210, 255, 0.3) 45%, transparent 88%);
  transform: translateY(-50%) scaleY(1);
  filter: blur(0.4px);
}

/* The streak itself used to run on cubic-bezier(0.25, 0.1, 0.4, 1), whose
   end control point sits at y=1 — so the star decelerated to a near stop
   in the last third of its travel and then blinked out, which is the
   opposite of how a meteor reads. It also snapped from invisible to full
   brightness in a single step. Now it brightens in over its first stretch,
   keeps gaining speed the whole way across, and dims out while still
   moving. */
@keyframes shooting-star-fall {
  0%, 98.2% {
    opacity: 0;
    transform: translate(0, 0) rotate(var(--angle, 250deg));
    animation-timing-function: cubic-bezier(0.42, 0.04, 0.7, 0.62);
  }
  98.55% {
    opacity: 1;
    animation-timing-function: linear;
  }
  99.5% {
    opacity: 0.85;
    animation-timing-function: var(--ease-exit);
  }
  100% {
    opacity: 0;
    transform: translate(-168px, 358px) rotate(var(--angle, 250deg));
  }
}

/* Long, staggered cycles so at most one streaks by, only occasionally. */
.shooting-stars span:nth-child(1) { top: 4%;  left: 25%; --angle: 245deg; animation-duration: 26s; animation-delay: 3s; }
.shooting-stars span:nth-child(2) { top: 8%;  left: 70%; --angle: 257deg; animation-duration: 34s; animation-delay: 14s; }
.shooting-stars span:nth-child(3) { top: 3%;  left: 50%; --angle: 251deg; animation-duration: 41s; animation-delay: 7s; }
.shooting-stars span:nth-child(4) { top: 10%; left: 85%; --angle: 243deg; animation-duration: 48s; animation-delay: 24s; }

.app-container {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 480px;
  min-width: 0;
  padding: 12px 16px;
  /* Clears the fixed .top-bar above it. The bar's visible chrome (tab
     pills and the round icon buttons) bottoms out at ~60px -- the rest of
     its ~80px box is the padding that keeps a tab's glow and unread badge
     from being clipped -- so this leaves ~20px of real breathing room
     under it. Plus the status bar / cutout inset, since the bar above
     grew by the same amount when drawn edge-to-edge. */
  padding-top: calc(80px + env(safe-area-inset-top, 0px));
  /* And clear of the gesture bar at the bottom, so the primary action on
     every screen isn't sitting under it. */
  padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  display: flex;
  flex-direction: column;
}

/* --- One screen, no page scrolling ---------------------------------------
   Every screen is sized to exactly the visible area and the page itself
   never scrolls. Each view nominates ONE region as `.view-scroll`: the
   part whose content can grow without limit (the prompt list, the player
   list, the answer fields, the voting options, the results breakdown).
   That region absorbs whatever height the screen's fixed controls leave
   over and scrolls inside itself, so a growing list can never push a
   Submit/Start/Next button off the bottom -- which is what used to make
   every in-game screen a long scroll with the fixed top bar sliding over
   its content.
   The chain each `.view-scroll` depends on: .app-container is exactly one
   viewport tall -> .view.active grows into it -> the card (and the form
   inside it, for the submissions/voting screens) is a flex column that's
   allowed to shrink -> the scroll region is the single flex-growing child.
   Every link needs `min-height: 0`, or a flex item refuses to shrink below
   its content and the overflow simply moves one level up.
   Gated on a viewport tall enough to hold each screen's fixed controls
   plus a usable scroll region. Below 601px the chain has nowhere left to
   give and `min-height: 0` would let a list spill over the buttons rather
   than shrink, so the whole block stops applying and every screen falls
   back to an ordinary scrolling page -- nothing overlaps, nothing is
   unreachable. No current phone is that short in portrait. */
@media (min-height: 601px) {
  html, body {
    overflow: hidden;
    height: 100%;
  }

  .app-container {
    /* dvh where supported: in a browser tab (not the installed PWA) 100vh
       is taller than what's actually visible, which would push the bottom
       control under the address bar. */
    height: 100vh;
    height: 100dvh;
    min-height: 0;
  }

  .view.active {
    flex: 1;
    min-height: 0;
  }

  .view.active > .card {
    display: flex;
    flex-direction: column;
    min-height: 0;
    max-height: 100%;
  }

  /* A card containing a scroll region grows to fill the screen, so the
     region gets all the height that's going. Without this the card stays
     content-sized and the list ends up with a small share of it while
     empty space sits below the card -- with the scroll fade on the last
     row, which reads as content clipped for no reason. Cards with nothing
     scrollable in them (home, the queue) stay their natural size rather
     than stretching into a mostly-empty panel. */
  .view.active > .card:has(.view-scroll),
  .view.active > .view-scroll {
    flex: 1 1 auto;
  }

  /* The scroll region is the only part of a screen allowed to give up
     height. Everything else -- a room-code badge, the black hole stage,
     the space-fact box, a heading -- keeps its natural size instead of
     being squeezed by flexbox's default shrink, which is what turns a
     fixed-height decorative element into a squashed one. */
  .view.active > .card > *:not(.view-scroll),
  .view-form > *:not(.view-scroll) {
    flex: 0 0 auto;
  }

  /* The submissions/voting screens put their scroll region inside a form,
     so the form is another link in the chain. */
  .view.active > .card > .view-form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
  }

  .view-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Keep an overscroll inside the region: chaining it to the page is
       what let a flick at the top of the voting list trigger the
       browser's pull-to-refresh. */
    overscroll-behavior: contain;
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* A hard cut at the edge reads as content clipped by mistake; fading
       the last ~20px says "there's more below" instead. Only applied while
       the region is actually the flex-growing one -- in the short-screen
       fallback below it's just normal page content and a fade there would
       dim text for no reason. */
    mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent 100%);
  }

  .view-scroll::-webkit-scrollbar {
    display: none;
  }

  /* Create Room's prompt list sits one level deeper, inside the mode
     section, so that section has to pass the growth through too. */
  #classic-mode-section {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
  }

}

.top-bar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

/* Results is the one screen whose scroll region isn't inside the card:
   the scoreboard card and the per-prompt breakdown scroll together, with
   the host's actions pinned below them. */
.results-scroll {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.results-scroll > * {
  flex: 0 0 auto;
}

.results-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 0 0 auto;
}

/* With no buttons showing (a non-host watching the results) this row would
   still eat the .view's 15px gap above it. */
.results-actions:not(:has(.btn:not(.hidden))) {
  display: none;
}

/* Short phones: the fixed parts of this card (name, mode, rounds, action
   buttons) don't fit alongside a usable prompt list at ~667px tall, so
   trim their padding/margins rather than let anything get cut off. No
   content is dropped -- it's the same screen, just tighter. */
@media (max-height: 740px) {
  .app-container {
    padding-top: calc(72px + env(safe-area-inset-top, 0px));
  }

  /* The cluster scales itself to whatever stage it's given, so a shorter
     stage costs proportion, not content. */
  .score-constellation {
    height: 168px;
    margin-bottom: 10px;
  }

  #view-create .card {
    padding: 14px;
  }

  #view-create h2 {
    font-size: 19px;
    margin-bottom: 6px;
  }

  #view-create h3 {
    margin: 8px 0 4px;
  }

  #view-create .mode-btn {
    padding: 8px 6px;
  }

  #view-create .mode-btn-hint {
    font-size: 9.5px;
  }

  #view-create .btn.primary.mt-20 {
    margin-top: 10px;
    padding: 12px;
  }

  #view-create .categories-scroll {
    min-height: 48px;
  }

  #view-create input[type="text"] {
    padding: 9px 14px;
    margin-bottom: 6px;
  }

  #view-create .mode-toggle {
    gap: 8px;
    margin-bottom: 0;
  }

  #view-create .rounds-header-row {
    margin: 8px 0 4px;
  }

  #view-create .rounds-header-text .hint-fine {
    margin-top: 2px;
  }

  /* The waiting screen's fixed parts (title, hint, the black hole, the
     pending list's heading and the space fact) are what squeeze the
     pending list on a short phone -- trim the decoration rather than let
     the list collapse to nothing. */
  #view-waiting .black-hole-stage {
    height: 150px;
    margin-bottom: 10px;
  }

  #view-waiting .space-fact-box {
    margin-top: 10px;
    padding: 11px 13px;
  }

  #view-waiting h2,
  #view-lobby h2,
  #view-voting h2,
  #view-submissions h2 {
    font-size: 19px;
    margin-bottom: 6px;
  }

  #view-lobby .room-badge {
    padding: 11px;
  }

  #view-lobby .room-badge span {
    font-size: 26px;
  }
}

#rounds-lock-hint {
  margin-bottom: 8px;
}

/* Positioning context for the idea-suggestion overlay below -- needed at
   every screen size, including the sub-661px fallback where this section
   isn't a flex container. */
#classic-mode-section {
  position: relative;
}

/* The sub-601px fallback: no locked layout anywhere, so every scroll
   region goes back to a fixed cap and the page scrolls as a whole. Capped
   rather than uncapped so one long list still can't own the entire screen
   and bury the control underneath it. */
@media (max-height: 600px) {
  .view-scroll {
    max-height: 240px;
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .view-scroll::-webkit-scrollbar {
    display: none;
  }
}

.account-header-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow-soft);
  font-size: 16px;
  cursor: pointer;
  transition: border-color var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press),
              box-shadow var(--t-hint) var(--ease-press);
}

/* Always-visible fixed strip: session tabs on the left, account button
   pinned to its right -- both stay reachable and in place regardless of
   scroll position or which screen/session is active. */
.top-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 480px;
  margin: 0 auto;
  padding: 10px 16px;
  /* Clear of the status bar / cutout when the app is drawn edge-to-edge. */
  padding-top: calc(10px + env(safe-area-inset-top, 0px));
  padding-left: calc(16px + env(safe-area-inset-left, 0px));
  padding-right: calc(16px + env(safe-area-inset-right, 0px));
  /* No background of its own -- sits directly on the page's starfield like
     it always did, just pinned in place now instead of scrolling away.
     A solid backdrop here read as a banner floating above the card. */
  pointer-events: none;
}

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

.account-header-btn:active {
  transform: scale(0.92);
}

.eyebrow {
  font-size: 11px;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--accent-cyan);
  font-weight: 700;
  margin-bottom: 6px;
}

/* --- Session tabs --- */
.session-tabs-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}

.session-tabs {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  /* overflow-x: auto clips on both axes, so this padding is what keeps a
     tab's unread badge (top: -3px) and active glow from being cut off. */
  padding: 12px 2px 14px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  touch-action: pan-x;
  scroll-behavior: smooth;
}

.session-tabs::-webkit-scrollbar {
  display: none;
}

.session-tab {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  border-radius: 999px;
  padding: 6px 14px 6px 6px;
  cursor: pointer;
  font-weight: 600;
  font-size: 13px;
  color: var(--text-muted);
  white-space: nowrap;
  box-shadow: var(--card-shadow-soft);
  transition: border-color var(--t-hint) var(--ease-press),
              color var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press);
}

.session-tab:active {
  transform: scale(0.97);
}

.session-tab.active {
  border-color: var(--accent-cyan);
  color: var(--text);
  box-shadow: 0 0 16px rgba(77, 216, 230, 0.25);
}

.session-tab .tab-avatar {
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
}

.session-tab .badge-dot {
  position: absolute;
  top: -3px;
  right: -3px;
  width: 11px;
  height: 11px;
  background: var(--accent-coral);
  border-radius: 50%;
  border: 2px solid var(--bg);
  box-shadow: 0 0 8px rgba(255, 95, 109, 0.8);
}

.session-tab.add-tab {
  background: transparent;
  border: 1px dashed rgba(255, 255, 255, 0.25);
  color: var(--text-muted);
  font-size: 16px;
  padding: 6px 16px;
}

/* A brand-new tab's arrival cue. Deliberately the same fade+scale morph as
   everything else, just travelling a little further because a tab is a
   small element: the previous version used a springy overshoot curve
   (cubic-bezier(0.34, 1.56, 0.64, 1)) from scale 0.6, which was the only
   bouncy motion in the app and read as a different, showier language than
   the restrained morph used everywhere around it. */
.session-tab.tab-pop-in {
  animation: view-morph-in var(--t-in) var(--ease-enter) both;
  --morph-scale: 0.84;
}

.view {
  display: none;
  flex-direction: column;
  gap: 15px;
  min-width: 0;
}

.view.active {
  display: flex;
}

/* The core morph. One keyframe pair drives every fade+scale in the app;
   how far it travels is per-element via --morph-scale, so a whole screen
   stays restrained while a small chip is allowed a bit more movement. */
@keyframes view-morph-out {
  from { opacity: 1; transform: scale(1); }
  to { opacity: 0; transform: scale(var(--morph-scale, 0.97)); }
}

@keyframes view-morph-in {
  from { opacity: 0; transform: scale(var(--morph-scale, 0.97)); }
  to { opacity: 1; transform: scale(1); }
}

.view.active {
  animation: view-morph-in var(--t-in) var(--ease-enter) both;
  --morph-scale: var(--morph-scale-view);
}

/* Must out-specify `.view.active` above AND come after it: the outgoing
   screen keeps `.active` until the swap lands, so with the old, equally
   specific `.view.view-collapsing` the later `.view.active` rule won the
   cascade, animation-name never changed to view-morph-out, and no new
   animation started. The screen simply froze at full opacity for the whole
   outgoing duration and then hard-cut. Measured before this fix: opacity
   pinned at 1 and scale at 1 for all 220ms. */
.view.active.view-collapsing {
  animation: view-morph-out var(--t-out) var(--ease-exit) forwards;
}

/* Same morph vocabulary, sized for smaller in-place content swaps (mode
   sections, history round tabs) rather than whole screens. */
.morph-swap-out {
  animation: view-morph-out var(--t-swap-out) var(--ease-exit) forwards;
  --morph-scale: var(--morph-scale-item);
}

.morph-swap-in {
  animation: view-morph-in var(--t-swap-in) var(--ease-enter) both;
  --morph-scale: var(--morph-scale-item);
}

/* While a container is gliding between two content heights, nothing should
   spill past its new edge mid-flight. */
.morph-height {
  overflow: hidden;
}

.card {
  background: var(--card-sheen), var(--card-base);
  padding: 18px;
  border-radius: 28px;
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow), 0 24px 60px var(--shadow-dark);
}

/* Home screen: name + planet live in one bundle up top, Join and Create
   are their own nested sub-bundles below it inside the SAME outer card --
   so picking a name/planet visibly belongs to both, instead of Create
   floating outside the card like an unrelated, disconnected action. */
.identity-bundle {
  margin-bottom: 4px;
}

.action-bundle {
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--border-soft);
  border-radius: 20px;
  padding: 12px;
  margin-bottom: 4px;
}

.action-bundle > *:last-child {
  margin-bottom: 0;
}

h2 {
  font-size: 22px;
  font-weight: 800;
  margin-bottom: 10px;
  color: var(--text);
  letter-spacing: 0.2px;
}

h3 {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  margin: 12px 0 6px;
  color: var(--accent-cyan);
}

.hint {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 15px;
}

/* A validation message belongs tucked up against the field it's about --
   but only when it actually follows one. The negative top margin used to
   be unconditional, so the same rule also pulled the sign-in error up on
   top of the "Remember me" row above it (a measured 4px collision), and
   had to be cancelled by hand wherever a hint sat under a heading
   instead. Now the tuck is scoped to the case it was written for, and
   everything else gets ordinary spacing for free. */
.hint-error,
.hint-success {
  margin-top: 0;
  margin-bottom: 10px;
  font-weight: 600;
}

.hint-error {
  color: var(--accent-coral);
}

input + .hint-error,
input + .hint-success,
.password-field + .hint-error,
.password-field + .hint-success {
  margin-top: -6px;
}

/* A footnote-level aside (e.g. the rounds stall-timeout note) rather than
   a decision-relevant instruction -- smaller and tighter than a normal
   .hint so it doesn't compete with the actual controls around it. */
.hint-fine {
  font-size: 11px;
  margin-top: -12px;
  margin-bottom: 10px;
  line-height: 1.5;
}

.rounds-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 16px 0 8px;
}

.rounds-header-text {
  flex: 1;
  min-width: 0;
}

.rounds-header-row h3 {
  margin: 0;
}

.rounds-header-text .hint-fine {
  margin-top: 4px;
  margin-bottom: 0;
}

/* A scroll-to-change stepper, not a text field -- ns-resize on the cursor
   plus a hover highlight and tiny arrows are the only cues that this cell
   does anything, since there's no visible box/border to imply "type here". */
.rounds-stepper {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--border-soft);
  cursor: ns-resize;
  user-select: none;
  transition: border-color var(--t-hint) var(--ease-press),
              background-color var(--t-hint) var(--ease-press);
}

.rounds-stepper:hover,
.rounds-stepper:focus-visible {
  border-color: var(--accent-cyan);
  background-color: rgba(77, 216, 230, 0.10);
  outline: none;
}

.rounds-stepper-value {
  font-size: 17px;
  font-weight: 800;
  color: var(--text);
  min-width: 1.3em;
  text-align: center;
  cursor: ns-resize;
  /* This is the drag surface -- without it, a touch-drag here scrolls the
     page instead of reaching our pointermove handler. */
  touch-action: none;
}

.rounds-stepper-value.dragging {
  color: var(--accent-cyan);
}

.rounds-stepper-arrows {
  display: flex;
  flex-direction: column;
  gap: 1px;
  line-height: 0.7;
  font-size: 9px;
  color: var(--text-muted);
}

.rounds-stepper-arrow {
  cursor: pointer;
  padding: 1px 2px;
  transition: color var(--t-hint) var(--ease-press);
}

.rounds-stepper-arrow:hover {
  color: var(--accent-cyan);
}

.hint-success {
  color: var(--accent-green);
}

.input-error {
  border-color: var(--accent-coral) !important;
  box-shadow: 0 0 0 3px rgba(255, 95, 109, 0.15) !important;
}

input[type="text"], input[type="number"], input[type="email"], input[type="password"], select, textarea {
  width: 100%;
  padding: 11px 16px;
  margin-bottom: 10px;
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-size: 15px;
  font-weight: 500;
  outline: none;
  /* Focus/error rings used to appear and vanish in a single frame, the only
     interactive elements in the app that didn't ease. */
  transition: border-color var(--t-hint) var(--ease-press),
              box-shadow var(--t-hint) var(--ease-press);
}

input::placeholder, textarea::placeholder {
  color: var(--text-muted);
}

input[type="text"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="password"]:focus, textarea:focus {
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px rgba(77, 216, 230, 0.15);
}

/* Submission/prompt answers: grows in height (via JS, on input) instead of
   clipping or scrolling long text sideways — a full pill shape stops
   looking right once it's more than one line tall, so this gets its own,
   more modest corner radius. */
.answer-textarea {
  border-radius: 18px;
  resize: none;
  overflow: hidden;
  line-height: 1.4;
  min-height: 44px;
  font-family: inherit;
  /* Gaining a line used to be an instant jump of a whole line-height, which
     shoved everything below it down in one frame. Height is a layout
     property, but it's the honest one here — this is a box actually
     getting taller — and it's a single short-lived element, so the cost is
     a relayout of one card, not a per-frame transform substitute that
     would lie about where the text sits. See autoGrowTextarea, which
     measures without clobbering the live height so this has a real start
     value to animate from. */
  transition: border-color var(--t-hint) var(--ease-press),
              box-shadow var(--t-hint) var(--ease-press),
              height var(--t-swap-in) var(--ease-enter);
}

/* First paint sizes itself with no animation — a restored long answer
   should already be the right height, not grow into it. */
.answer-textarea.no-grow-anim {
  transition: none;
}

.btn {
  width: 100%;
  padding: 15px;
  border: none;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-press),
              box-shadow var(--t-press) var(--ease-press),
              filter var(--t-press) var(--ease-press);
}

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

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  filter: grayscale(0.4);
}

.btn:disabled:active {
  transform: none;
}

.btn.primary {
  background: linear-gradient(160deg, #ff7a85 0%, var(--accent-coral) 45%, var(--accent-coral-2) 100%);
  color: #ffffff;
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.45),
    inset 0 -4px 8px rgba(120, 10, 25, 0.35),
    0 14px 32px rgba(230, 67, 90, 0.45),
    0 0 26px rgba(255, 95, 109, 0.35);
}

.btn.primary:active {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.3),
    inset 0 -1px 3px rgba(120, 10, 25, 0.3),
    0 6px 16px rgba(230, 67, 90, 0.35);
}

.btn.secondary {
  background: linear-gradient(160deg, #8ff2ec 0%, var(--accent-cyan) 45%, var(--accent-cyan-2) 100%);
  color: #06070f;
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.5),
    inset 0 -4px 8px rgba(10, 70, 80, 0.3),
    0 10px 26px rgba(77, 216, 230, 0.35);
}

.btn.secondary:active {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.35),
    inset 0 -1px 3px rgba(10, 70, 80, 0.25),
    0 5px 14px rgba(77, 216, 230, 0.25);
}

.btn.small {
  width: auto;
  padding: 9px 16px;
  font-size: 13px;
  background: var(--card-sheen), var(--card-base-soft);
  color: var(--text);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow-soft);
}

.btn.link {
  background: none;
  box-shadow: none;
  color: var(--text-muted);
  text-decoration: underline;
  margin-top: 10px;
  font-weight: 500;
}

.btn.link:active {
  transform: none;
}

.divider {
  display: flex;
  align-items: center;
  text-align: center;
  color: var(--text-muted);
  margin: 6px 0;
}

.divider::before, .divider::after {
  content: '';
  flex: 1;
  border-bottom: 1px solid var(--border-soft);
}

.divider span {
  padding: 0 10px;
  font-size: 12px;
}

.room-badge {
  text-align: center;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  border-radius: 20px;
  padding: 16px;
  box-shadow: var(--card-shadow-soft);
}

.room-badge::before {
  content: 'ROOM CODE';
  display: block;
  font-size: 10px;
  letter-spacing: 2px;
  color: var(--text-muted);
  margin-bottom: 6px;
  font-weight: 700;
}

.room-badge span {
  font-size: 30px;
  font-weight: 800;
  letter-spacing: 6px;
  color: var(--accent-cyan);
  text-shadow: 0 0 20px rgba(77, 216, 230, 0.5);
}

.queue-badge::before {
  content: 'QUEUE POSITION';
}

.player-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* The pills carry a drop shadow and this is a scroll region, so without
     a little side padding the shadow gets sliced off at the edges. */
  padding: 0 2px;
}

.player-list li {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  box-shadow: var(--card-shadow-soft);
}

.player-name {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}

.player-avatar {
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
}

/* Host-only, and coral because it removes someone -- but small and quiet,
   since it sits on every row of a list the host reads far more often than
   they act on. */
.skip-player-btn {
  flex: 0 0 auto;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid rgba(255, 95, 109, 0.4);
  background: rgba(255, 95, 109, 0.1);
  color: var(--accent-coral);
  font-size: 11px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-hint) var(--ease-press),
              border-color var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press);
}

.skip-player-btn:hover {
  background: rgba(255, 95, 109, 0.2);
  border-color: rgba(255, 95, 109, 0.7);
}

.skip-player-btn:active {
  transform: scale(0.93);
}

/* Players feel alive in the lobby list instead of sitting still.
   Perpetual rotation stays `linear` on purpose — see the motion system note
   in :root. */
.player-avatar img {
  animation: planet-self-spin 16s linear infinite;
}

/* Animates the independent `rotate` property, NOT `transform`.
   avatarSvg() puts an inline `transform: scale(...)` on a few planets whose
   source art has extra padding (venus/jupiter/uranus/neptune). A
   `to { transform: rotate(360deg) }` keyframe has no `from`, so the browser
   filled it from the element's underlying value — that inline scale — and
   then interpolated scale(1.38) -> rotate(360deg) as decomposed matrices.
   The result was those four planets slowly shrinking from 1.38x to 1.0x
   over each spin and snapping back at the loop point: measured 41px vs
   30.6px rendered for two planets in the same 30px slot. `rotate` composes
   with `transform` instead of replacing it, so the zoom correction
   survives the spin. */
@keyframes planet-self-spin {
  from { rotate: 0deg; }
  to { rotate: 360deg; }
}

.player-list li:nth-child(2n) .player-avatar img { animation-duration: 12s; animation-direction: reverse; }
.player-list li:nth-child(3n) .player-avatar img { animation-duration: 20s; }
.player-list li:nth-child(4n) .player-avatar img { animation-duration: 9s; animation-direction: reverse; }
.player-list li:nth-child(5n) .player-avatar img { animation-duration: 15s; }

.checkbox-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 12px;
  cursor: pointer;
}

.checkbox-row input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--accent-cyan);
  margin: 0;
}

/* --- Black hole: submitted planets get sucked in one by one --- */
.black-hole-stage {
  position: relative;
  width: 100%;
  height: 190px;
  margin: 4px 0 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* Face-on spiral galaxy (matches the reference image: a top-down multi-arm
   spiral with a bright nucleus, not a tilted fiery accretion disk), built
   as an SVG so the arms can be real logarithmic-spiral curves instead of
   conic-gradient pie slices. Rotates as a plain, unscaled rotate() — no
   tilt/flatten anywhere in this scene, on purpose: a rotation sandwiched
   between a scale and its inverse doesn't commute, which is exactly what
   caused the orbiting planets to visibly warp before this was simplified. */
.galaxy-spiral {
  position: absolute;
  width: 220px;
  height: 220px;
  z-index: 1;
  overflow: visible;
}

.spiral-arms-group {
  animation: galaxy-rotate 46s linear infinite;
}

.spiral-arm {
  fill: none;
  stroke-width: 3;
  stroke-linecap: round;
}

.spiral-arm.thin {
  stroke-width: 2.2;
  opacity: 0.75;
}

/* The arms used to be softened with `filter: blur(0.5px)` on the rotating
   <g>, which forces the browser to re-run a blur over the whole 220px
   scene on every frame of a permanently-running rotation. The same soft
   edge comes free from a second, wider, fainter stroke drawn under each
   arm (see buildGalaxySpiral), leaving the rotation as a plain composited
   transform with nothing to rasterize per frame. */
.spiral-arm.halo {
  stroke-width: 7;
  opacity: 0.16;
}

@keyframes galaxy-rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.black-hole-core {
  position: relative;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, #f2fbff 0%, #dcefff 10%, #050914 48%, rgba(5, 9, 20, 0) 100%);
  box-shadow: 0 0 30px 8px rgba(190, 220, 255, 0.55), 0 0 64px 18px rgba(124, 140, 255, 0.28);
  z-index: 2;
  transition: box-shadow var(--t-in) var(--ease-enter), transform var(--t-in) var(--ease-enter);
}

.black-hole-core.pulse {
  animation: blackhole-pulse 620ms var(--ease-enter);
}

/* A slow, ever-present "breathing" glow on its own pseudo-layer — separate
   from .pulse's sharp capture flash above so the two never fight over the
   same transform/box-shadow — is what actually sells "this is pulling
   things in" even in the quiet moments when nobody has finished yet. */
.black-hole-core::before {
  content: '';
  position: absolute;
  inset: -14px;
  border-radius: 50%;
  box-shadow: 0 0 26px 8px rgba(200, 225, 255, 0.4);
  opacity: 0.5;
  animation: core-idle-glow 3.6s var(--ease-soft) infinite;
  pointer-events: none;
}

/* A swallowed planet makes the core flare. Per-keyframe curves so the
   swell snaps out and the settle is long and soft, instead of the old
   symmetric `ease` that made both halves the same speed and stopped dead
   on the way back. */
@keyframes blackhole-pulse {
  0% {
    box-shadow: 0 0 30px 8px rgba(190, 220, 255, 0.55), 0 0 64px 18px rgba(124, 140, 255, 0.28);
    transform: scale(1);
    animation-timing-function: cubic-bezier(0.16, 0.84, 0.32, 1);
  }
  34% {
    box-shadow: 0 0 52px 18px rgba(220, 238, 255, 0.85), 0 0 104px 30px rgba(150, 170, 255, 0.5);
    transform: scale(1.11);
    animation-timing-function: var(--ease-soft);
  }
  100% {
    box-shadow: 0 0 30px 8px rgba(190, 220, 255, 0.55), 0 0 64px 18px rgba(124, 140, 255, 0.28);
    transform: scale(1);
  }
}

@keyframes core-idle-glow {
  0%, 100% { transform: scale(1); opacity: 0.4; }
  50% { transform: scale(1.18); opacity: 0.8; }
}

/* Ambient debris that's always trickling inward, angle fixed per mote and
   only its distance from center animated — reads as the black hole
   actively feeding even before any planet starts orbiting it. */
.accretion-field {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  z-index: 2;
  pointer-events: none;
}

.accretion-mote {
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: #eaf6ff;
  box-shadow: 0 0 7px 2px rgba(200, 225, 255, 0.85);
  opacity: 0;
  /* Infalling dust speeds up as it gets closer, so the distance curve
     accelerates instead of the old `linear`, which read as a mote being
     dragged inward on a string at constant speed. Safe to ease here even
     though it loops: the mote is fully transparent at both ends, so the
     restart is never visible. */
  animation: accretion-fall 4.6s cubic-bezier(0.52, 0.02, 0.76, 0.28) infinite;
  animation-delay: var(--delay, 0s);
}

@keyframes accretion-fall {
  0% { transform: rotate(var(--angle, 0deg)) translateX(var(--from, 94px)); opacity: 0; }
  14% { opacity: 0.95; }
  72% { opacity: 0.8; }
  100% { transform: rotate(var(--angle, 0deg)) translateX(8px); opacity: 0; }
}

/* Everything that orbits or falls lives inside one 0x0 field pinned to the
   centre of the stage, so the whole system can be scaled to fit as a unit.
   Per-player orbit radius is still uncapped and strictly increasing (see
   createOrbitElement — capping it is what made 5th+ players share a ring
   and eclipse each other), but an unbounded radius inside a 190px-tall
   stage means the outer rings get clipped by the stage edge: measured with
   8 players, four planets crossed the top/bottom boundary and vanished
   once per lap.

   Scaling the field is the part that's safe to do and capping is not: this
   is a single UNIFORM scale on a parent, and a uniform scale commutes with
   rotation, so nothing warps. It is not the nested squash-then-counter-
   squash that broke the planets before — there is no inverse scale
   anywhere, and nothing non-uniform. It also only changes when the player
   count changes, never per frame. */
.orbit-field {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  z-index: 3;
  pointer-events: none;
  transform: scale(var(--orbit-fit, 1));
  transition: transform 520ms var(--ease-enter);
}

.falling-avatar {
  position: absolute;
  top: 0;
  left: 0;
  /* width/height/margin are set inline per-planet (see sizeAvatarEl) */
  z-index: 1;
  pointer-events: none;
  animation: fall-into-blackhole var(--t-fall) cubic-bezier(0.5, 0.01, 0.78, 0.32) forwards;
}

.falling-avatar .avatar-chip {
  width: 100%;
  height: 100%;
}

/* The planet keeps turning on its own axis while it's dragged in. On
   `rotate`, not `transform` — the wrapper's transform is what carries it
   around the core, and a few planets carry an inline scale correction. */
.falling-avatar img {
  animation: planet-self-spin 2.6s linear infinite;
}

.orbit-wrapper {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  animation-name: orbit-spin;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  pointer-events: none;
}

@keyframes orbit-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.orbit-avatar {
  position: absolute;
  top: 0;
  left: 0;
  /* width/height/margin are set inline per-planet (see sizeAvatarEl);
     transform (ring placement) is set inline too. */
}

.orbit-avatar .avatar-chip {
  width: 100%;
  height: 100%;
}

/* Independent inner layer: a rhythmic shrink + brighten "tugged by
   gravity" pulse, decoupled from the ring position above it so the pulse
   can never nudge a planet off its assigned radius. */
.orbit-pulse {
  width: 100%;
  height: 100%;
  animation: orbit-tug 2.8s var(--ease-soft) infinite;
}

/* A 0.8 scale was a 20% size swing on every planet every 2.4s, big enough
   that planets of genuinely different sizes became hard to tell apart and
   the whole ring read as jittery. Halved, and on a symmetric sine curve so
   it breathes rather than pumps. Still a pure uniform scale on a layer of
   its own, so it can never disturb the ring position above it. */
@keyframes orbit-tug {
  0%, 100% { transform: scale(1); filter: brightness(1); }
  55% { transform: scale(0.9); filter: brightness(1.2) saturate(1.12); }
}

/* An actual inward spiral rather than a straight line to the middle.
   Composed exactly like accretion-fall: rotate() sets the angle around the
   core, translateX() the distance from it, and both are interpolated at
   once, so the planet sweeps around as it closes in. The scale is uniform
   and applied after the rotation, never around it — this is not the
   squash/counter-squash pattern that warped the orbiting planets before.
   Start angle and radius are handed in per capture (see spawnFallingAvatar)
   so the planet peels off from exactly where it was orbiting. */
@keyframes fall-into-blackhole {
  0% {
    transform: rotate(var(--start-angle, 0deg)) translateX(var(--start-radius, 70px)) scale(1);
    opacity: 1;
  }
  72% {
    opacity: 1;
  }
  100% {
    transform: rotate(calc(var(--start-angle, 0deg) + var(--sweep, 170deg))) translateX(3px) scale(0.08);
    opacity: 0;
  }
}

.space-fact-box {
  background:
    linear-gradient(165deg, rgba(255, 255, 255, 0.08) 0%, transparent 55%),
    linear-gradient(160deg, rgba(124, 140, 255, 0.12) 0%, rgba(124, 140, 255, 0.04) 100%);
  border: 1px solid rgba(124, 140, 255, 0.28);
  border-radius: 18px;
  padding: 14px 16px;
  margin-top: 16px;
  box-shadow: var(--card-shadow-soft);
}

.space-fact-box .eyebrow {
  color: var(--accent-purple);
  margin-bottom: 4px;
}

.space-fact-box p {
  font-size: 13px;
  color: var(--text);
  line-height: 1.4;
}

.hidden {
  display: none !important;
}

.mt-20 {
  margin-top: 20px;
}

.mt-10 {
  margin-top: 10px;
}

/* --- Chips / badges --- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 11px;
  border-radius: 999px;
  background: var(--glass-strong);
  border: 1px solid var(--border-soft);
  font-size: 11px;
  font-weight: 700;
  color: var(--text-muted);
  white-space: nowrap;
}

.chip.host {
  color: var(--accent-yellow);
  border-color: rgba(255, 210, 63, 0.4);
  background: rgba(255, 210, 63, 0.1);
}

.chip.offline {
  color: var(--text-muted);
}

/* --- Avatar picker --- */
.avatar-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
  margin-bottom: 6px;
  /* Grid items default to min-width: auto, which lets a child's intrinsic
     content size push a track wider than its 1fr share -- min-width: 0
     forces every cell to actually respect the column width instead. */
  min-width: 0;
}

.planet-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.planet-header-row h3 {
  margin: 12px 0 6px;
}

.avatar-selected-name {
  text-align: right;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent-cyan);
  min-height: 18px;
  margin: 12px 0 6px;
}

.avatar-option {
  position: relative;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  border-radius: 16px;
  cursor: pointer;
  box-shadow: var(--card-shadow-soft);
  transition: transform var(--t-hint) var(--ease-enter),
              border-color var(--t-hint) var(--ease-enter),
              box-shadow var(--t-hint) var(--ease-enter);
  overflow: hidden;
  min-width: 0;
}

/* Square grid cells via the old padding-percentage trick instead of the
   `aspect-ratio` CSS property -- Safari/WebKit has real bugs sizing
   aspect-ratio inside a 1fr CSS Grid track (verified: cells came out
   91x106px, non-square, with the grid itself 17px wider than the
   viewport), while padding-top: 100% (percentage padding is always
   resolved against the element's own width) is rock solid everywhere. */
.avatar-option::before {
  content: '';
  display: block;
  padding-top: 100%;
}

.avatar-option .avatar-chip {
  position: absolute;
  inset: 6px;
}

/* Tapping Join/Start: the chosen tile's clone punches up to a full-screen
   hold, then flies back down onto the exact tile it came from (hidden via
   visibility while its clone is mid-flight, so there's no double image). */
.planet-zoom-backdrop {
  position: fixed;
  inset: 0;
  background: #000;
  opacity: 0;
  z-index: 199;
  pointer-events: none;
}

.avatar-chip.planet-zoom-avatar {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 200;
  pointer-events: none;
}

.avatar-option.selected {
  border-color: var(--accent-cyan);
  box-shadow: 0 0 18px rgba(77, 216, 230, 0.35);
  transform: scale(1.06);
}

.avatar-option.taken {
  cursor: not-allowed;
  opacity: 0.45;
  filter: grayscale(0.5) blur(1.5px);
}

.avatar-option.taken .avatar-chip {
  filter: blur(1.5px);
}

.avatar-option.taken::after {
  content: '✕';
  position: absolute;
  font-size: 18px;
  font-weight: 800;
  color: var(--text-muted);
}

/* --- Avatar ring chip (used everywhere an avatar renders) --- */
.avatar-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border-radius: 50%;
  overflow: hidden;
  background: transparent;
}

/* The colored ring is reserved for voting-results contexts (scoreboard,
   answer entries, voter avatars) — everywhere else (picker, lobby, tabs,
   the black hole) shows the plain planet. */
.rank-avatar,
.answer-avatar,
.voter-avatar,
.legend-avatar {
  border: 2px solid var(--ring-color, var(--accent-cyan));
  box-shadow: 0 0 10px color-mix(in srgb, var(--ring-color, var(--accent-cyan)) 65%, transparent);
}

.avatar-chip img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

/* --- Mode toggle --- */
.mode-toggle {
  display: flex;
  gap: 10px;
  margin-bottom: 4px;
}

.mode-btn {
  flex: 1;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  border-radius: 18px;
  padding: 12px;
  color: var(--text);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-size: 14px;
  font-weight: 700;
  box-shadow: var(--card-shadow-soft);
  transition: border-color var(--t-hint) var(--ease-press),
              box-shadow var(--t-hint) var(--ease-press);
}

.mode-btn.active {
  border-color: var(--accent-cyan);
  box-shadow: 0 0 16px rgba(77, 216, 230, 0.25);
}

/* Pro-only modes on the free tier. Still tappable (the tap is what opens
   the upsell), so this is a visual state only -- no pointer-events or
   disabled attribute. */
.mode-btn.locked {
  position: relative;
  border-style: dashed;
  color: var(--text-muted);
}

.mode-btn.locked::after {
  content: '🔒';
  position: absolute;
  top: 5px;
  right: 7px;
  font-size: 9px;
  line-height: 1;
}

.mode-btn-hint {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-muted);
}

/* --- Prompt label -------------------------------------------------------
   The same prompt text shows up on three screens -- answering it, voting
   on it, and reading it back in the breakdown -- and was styled three
   different ways: an inline-styled bold white <label>, a cyan uppercase
   letter-spaced <h3>, and the round card's own title. Uppercase plus
   letter-spacing is the worst of the three for the job, since a prompt is
   a sentence and regularly runs to two or three lines. One style for all
   three now, matching the round card's title. */
.prompt-label {
  display: block;
  color: var(--text);
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.2px;
  line-height: 1.35;
  margin: 16px 0 8px;
  /* Prompts are player-typed (and in Adventure mode written by whoever's
     playing), so one can be an unbroken run of characters with nowhere to
     wrap -- which overflows the card rather than the line. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.prompt-label:first-child {
  margin-top: 0;
}

/* --- Voting cards --- */
.vote-category {
  margin-bottom: 18px;
}

.vote-category:last-child {
  margin-bottom: 0;
}

.vote-option {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  background:
    linear-gradient(165deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.02) 32%, transparent 58%),
    linear-gradient(160deg, #1c2138 0%, #12152a 55%, #0b0d18 100%);
  border-radius: 20px;
  padding: 16px 18px;
  margin: 0 0 10px;
  cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.07);
  font-weight: 500;
  font-size: 15px;
  color: var(--text);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.05) inset,
    0 16px 32px rgba(0, 0, 0, 0.45),
    0 3px 6px rgba(0, 0, 0, 0.35);
  transition: transform var(--t-press) var(--ease-press),
              box-shadow var(--t-hint) var(--ease-press),
              border-color var(--t-hint) var(--ease-press);
}

.vote-option:active {
  transform: scale(0.985) translateY(1px);
}

.vote-option:has(input:checked) {
  border-color: rgba(255, 95, 109, 0.55);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.08) inset,
    0 0 0 1px rgba(255, 95, 109, 0.2),
    0 16px 36px rgba(0, 0, 0, 0.5),
    0 0 28px rgba(255, 95, 109, 0.3);
}

.vote-option input[type="radio"] {
  accent-color: var(--accent-coral);
  width: 20px;
  height: 20px;
  margin: 0;
  flex: 0 0 auto;
}

/* A submitted answer is player-typed text, so it can be one long run with
   no spaces to break on (or just wider than the pill at any normal word
   break) -- without this, a flex item's default min-width:auto refuses to
   shrink below that unbroken run's full width and it overflows straight
   past the pill's rounded edge instead of wrapping inside it. */
.vote-option-text {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* --- Results / round history --- */
/* Round cards never appear alone -- two to four of them stack in the
   results breakdown, and in Round History they stack *inside* the modal's
   own card. The shared card gradient runs #1c2138 at the top down to
   #0b0d18 at the bottom, a shade off both the page (#06070f) and the card
   behind them, so each card's lower half dissolved into the gap under it
   and a stack read as one continuous dark slab with a few seams rather
   than as separate cards. A flat, evenly-lifted surface with a hairline
   that stays visible along the bottom edge keeps each card's own outline
   legible all the way round. */
.round-card {
  background:
    linear-gradient(165deg, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.025) 100%),
    #161a30;
  border: 1px solid rgba(255, 255, 255, 0.13);
  border-radius: 24px;
  padding: 16px;
  margin-bottom: 16px;
  box-shadow: var(--card-shadow);
}

/* The stack's own gap is the container's, so a trailing margin here just
   adds dead space above the next control (or an odd scroll overshoot). */
.round-card:last-child {
  margin-bottom: 0;
}

.round-card h3 {
  color: var(--text);
  font-size: 15px;
  letter-spacing: 0.2px;
  text-transform: none;
  font-weight: 800;
  margin-top: 0;
  /* A prompt is player-typed, so it can be one unbroken 60-character run
     with no space to wrap at -- which used to run straight out through
     the card's right edge and off the screen. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.answer-entry {
  background:
    linear-gradient(165deg, rgba(255, 255, 255, 0.05) 0%, transparent 55%),
    rgba(255, 255, 255, 0.03);
  border-radius: 14px;
  /* 6px/8px was noticeably tighter than every other boxed row in the app
     (.player-list li at 10px/14px, .vote-option at 16px/18px), which is
     what made a breakdown read as a cramped stack rather than a list of
     cards. */
  padding: 9px 12px;
  margin-bottom: 8px;
  display: flex;
  align-items: stretch;
  gap: 10px;
  border: 1px solid var(--border-soft);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 4px 10px rgba(0, 0, 0, 0.25);
}

.answer-entry:last-child {
  margin-bottom: 0;
}

.answer-entry.winner {
  border-color: var(--accent-yellow);
  box-shadow: 0 0 12px rgba(255, 210, 63, 0.18);
}

.answer-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 3px;
}

.answer-entry-head {
  display: flex;
  align-items: center;
  gap: 6px;
}

.answer-avatar {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
}

/* Player-typed text, so it can be one unbroken run with nothing to wrap
   on. A flex item's default min-width:auto refuses to shrink below that
   run's full width, so without these it pushed straight past the entry's
   rounded edge instead of wrapping inside it -- same reasoning as
   .vote-option-text, which already had them. */
.answer-text {
  font-weight: 600;
  font-size: 13px;
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}

.answer-score {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  padding-left: 10px;
  border-left: 1px solid var(--border-soft);
}

.score-value {
  font-size: 21px;
  font-weight: 800;
  line-height: 1;
  color: var(--accent-cyan);
}

.mult-tag {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.3px;
  color: var(--accent-yellow);
  margin-top: 2px;
}

.voters-line {
  display: flex;
  align-items: center;
  gap: 3px;
  flex-wrap: wrap;
  padding-left: 24px;
}

.voter-avatar {
  width: 14px;
  height: 14px;
}

/* --- Score constellation ------------------------------------------------
   The standings live here as a cluster of planets instead of a list of
   rows, so a score is something you see before you read it. Positions,
   sizes and the fit scale are all computed in renderConstellation(). */
.score-constellation {
  position: relative;
  height: 200px;
  margin: 2px 0 14px;
  /* The cluster is scaled to fit, so nothing should reach this -- it's the
     backstop that keeps a mis-measured stage (one laid out while the view
     was still display:none) from spilling over the card. */
  overflow: hidden;
}

/* Position comes from the simulation, written straight onto `transform`
   every frame (translate3d, so it stays off the layout path -- left/top
   would relayout the stage 60 times a second). */
/* Quiet by default: the cluster is the thing, this is an option. */
.tilt-btn {
  display: block;
  margin: -6px auto 10px;
  padding: 5px 13px;
  border-radius: 999px;
  border: 1px solid var(--border-soft);
  background: rgba(255, 255, 255, 0.035);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  transition: border-color var(--t-hint) var(--ease-press),
              color var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press);
}

.tilt-btn.is-on {
  border-color: rgba(77, 216, 230, 0.5);
  background: rgba(77, 216, 230, 0.12);
  color: var(--accent-cyan);
}

.tilt-btn:active {
  transform: scale(0.94);
}

.constellation-planet {
  position: absolute;
  top: 0;
  left: 0;
  /* width/height/transform set inline per planet */
  will-change: transform;
}

/* The pop-in scales, and the planet's own transform is its position, so
   they have to be separate elements. */
.constellation-pop {
  position: absolute;
  inset: 0;
  animation: planet-pop var(--t-in) var(--ease-enter) both;
  animation-delay: var(--pop-delay, 0ms);
}

@keyframes planet-pop {
  from { opacity: 0; transform: scale(0.55); }
  to { opacity: 1; transform: scale(1); }
}

.constellation-avatar {
  width: 100%;
  height: 100%;
}

/* --- Constellation legend ----------------------------------------------
   The cluster shows who's ahead; this is where you read by how much. The
   planets carry nothing on top of them, so the scores have to be properly
   legible here rather than incidental. Two columns on a phone, more as the
   card gets wider. */
#scoreboard-content {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 6px 12px;
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  padding: 4px 0;
}

.legend-avatar {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
}

/* The name stays small -- it's a key, and you already know your own
   planet. A 20-character name with no spaces has nothing to wrap on, and a
   flex item won't shrink below its content by default. */
.legend-name {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  font-weight: 700;
  overflow-wrap: anywhere;
}

/* The score, and now the only place it's written down, so it reads as the
   number it is. Tabular figures keep the column straight down the grid
   instead of 1s pulling their rows in. */
.legend-points {
  flex: 0 0 auto;
  font-size: 16px;
  font-weight: 800;
  line-height: 1.1;
  color: var(--accent-yellow);
  font-variant-numeric: tabular-nums;
  padding-left: 2px;
}

/* --- Stat boxes (bottom info row, e.g. lobby/voting metadata) --- */
.stat-row {
  display: flex;
  gap: 10px;
}

.stat-box {
  flex: 1;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border-soft);
  border-radius: 18px;
  padding: 12px;
  text-align: center;
}

.stat-box .stat-label {
  font-size: 10px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 700;
}

.stat-box .stat-value {
  font-size: 22px;
  font-weight: 800;
  color: var(--text);
  margin-top: 4px;
}

/* --- Round history modal --- */
/* Above .top-bar (z-index 20), which is a sibling of .app-container and so
   painted straight over any modal nested inside it -- see the note in
   index.html about why these live outside that container now. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 16px;
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.modal::-webkit-scrollbar {
  display: none;
}

/* The dimming/blur layer lives on its own pseudo-element rather than on
   .modal itself, so it can fade with the card instead of snapping in and
   out around it — previously the scrim appeared fully opaque on frame one
   while the card was still at opacity 0, and vanished in a single frame on
   close while the card was still fading. Fixed positioning keeps it out of
   the flex flow, so it changes no layout. */
.modal::before {
  content: '';
  position: fixed;
  inset: 0;
  background: rgba(2, 3, 8, 0.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: scrim-in var(--t-in) var(--ease-enter) both;
  pointer-events: none;
}

.modal.closing::before {
  animation: scrim-out var(--t-out) var(--ease-exit) forwards;
}

@keyframes scrim-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes scrim-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

.modal-content {
  width: 100%;
  max-width: 480px;
  margin-top: 4vh;
  margin-bottom: 4vh;
  animation: view-morph-in var(--t-in) var(--ease-enter) both;
  --morph-scale: 0.96;
}

.modal-content.closing {
  animation: view-morph-out var(--t-out) var(--ease-exit) forwards;
}

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}

.modal-head .eyebrow {
  margin-bottom: 0;
}

.modal.modal-center {
  align-items: center;
}

/* --- Host sign-in / register modal --- */
.host-auth-card {
  position: relative;
  text-align: center;
  padding-top: 30px;
}

.host-auth-close {
  position: absolute;
  top: 14px;
  right: 14px;
}

.host-auth-badge {
  width: 64px;
  height: 64px;
  margin: 0 auto 14px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 30px;
  background: radial-gradient(circle at 32% 28%, #b9f5f0 0%, var(--accent-cyan) 45%, var(--accent-purple) 100%);
  box-shadow:
    inset 0 2px 4px rgba(255, 255, 255, 0.55),
    inset 0 -6px 10px rgba(20, 10, 60, 0.35),
    0 0 30px rgba(124, 140, 255, 0.55);
  animation: planet-self-spin 14s linear infinite;
}

.host-auth-card .eyebrow {
  text-align: center;
}

.host-auth-card h2 {
  text-align: center;
}

.host-auth-card .hint {
  text-align: center;
}

.host-auth-card input {
  text-align: left;
}

/* --- Password visibility toggle --- */
.password-field {
  position: relative;
}

.password-field input {
  padding-right: 46px;
}

.password-toggle-btn {
  position: absolute;
  top: 0;
  bottom: 12px;
  right: 4px;
  width: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  font-size: 16px;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity var(--t-hint) var(--ease-press);
}

.password-toggle-btn:hover,
.password-toggle-btn:active {
  opacity: 1;
}

/* --- Remember me --- */
/* The row used to be pulled up 4px to tuck under the password field, but
   that field's own bottom margin sits on the <input> inside .password-field
   (the wrapper has none), so there was nothing to tuck into and the row
   landed 4px on top of the input's box instead -- measured as a 318x4px
   overlap. Sits below it now, at the same rhythm as the other fields. */
.remember-me-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
  margin: 2px 0 8px 2px;
}

.remember-me-row input {
  width: auto;
  margin: 0;
  accent-color: var(--accent-cyan);
}

/* --- Account modal: menu + sub-window navigation --- */
.account-view {
  text-align: center;
}

.account-back-btn {
  display: block;
  text-align: left;
  margin: 0 0 6px;
  padding: 0;
}

.account-menu-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 18px;
}

.account-menu-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 14px 16px;
  border-radius: 16px;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow-soft);
  color: var(--text);
  font-size: 14px;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press);
}

.account-menu-btn:active {
  transform: scale(0.98);
}

/* A status badge, not a button -- e.g. the Pro row once already on Pro */
.account-menu-btn-static {
  cursor: default;
  background: linear-gradient(160deg, rgba(255, 210, 63, 0.16) 0%, rgba(255, 210, 63, 0.05) 100%);
  border-color: rgba(255, 210, 63, 0.35);
  color: var(--accent-yellow);
}

.account-menu-btn-static:active {
  transform: none;
}

.account-menu-btn-arrow {
  color: var(--text-muted);
  font-size: 18px;
}

/* --- Plan card (inside the account modal) --- */
.plan-card-pro {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px;
  border-radius: 16px;
  background: linear-gradient(160deg, rgba(255, 210, 63, 0.18) 0%, rgba(255, 210, 63, 0.05) 100%);
  border: 1px solid rgba(255, 210, 63, 0.35);
  color: var(--accent-yellow);
  font-weight: 800;
  font-size: 15px;
}

.plan-offer {
  position: relative;
  padding: 16px;
  border-radius: 16px;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid rgba(255, 210, 63, 0.3);
  box-shadow: var(--card-shadow-soft);
  text-align: center;
  overflow: hidden;
}

.plan-offer-tag {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(255, 210, 63, 0.15);
  color: var(--accent-yellow);
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  margin-bottom: 10px;
}

.plan-price-row {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
}

/* .hint-fine's -12px tuck is for fine print sitting under a stepper, where
   it closes a gap. In here there's nothing above to close, so it pulled
   this line up into the price -- a measured 12px overlap. */
.plan-offer .hint-fine {
  margin-top: 4px;
  margin-bottom: 0;
}

.plan-price-old {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-color: var(--accent-coral);
}

#stripe-checkout-container {
  min-height: 420px;
  margin-top: 8px;
}

.plan-price-new {
  font-size: 26px;
  font-weight: 800;
  color: var(--accent-green);
  text-shadow: 0 0 18px rgba(74, 222, 128, 0.4);
}

.confirm-message {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 18px;
  line-height: 1.4;
}

.confirm-btn-row {
  display: flex;
  gap: 10px;
}

.confirm-btn-row .btn {
  flex: 1;
  width: auto;
}

/* --- Toast notifications (in-app, replaces native alert/confirm) --- */
.toast-container {
  position: fixed;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 90;
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
  width: calc(100% - 32px);
  max-width: 420px;
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-radius: 14px;
  background: var(--card-sheen), var(--card-base);
  border: 1px solid var(--card-border);
  border-left: 3px solid var(--accent-cyan);
  box-shadow: var(--card-shadow);
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  animation: toast-in var(--t-swap-in) var(--ease-enter) both;
}

.toast.error { border-left-color: var(--accent-coral); }
.toast.success { border-left-color: var(--accent-green); }
.toast.info { border-left-color: var(--accent-purple); }

.toast.leaving {
  animation: toast-out var(--t-out) var(--ease-exit) forwards;
}

.toast-icon {
  flex: 0 0 auto;
  font-size: 15px;
  line-height: 1;
}

.toast-text {
  flex: 1;
  line-height: 1.35;
}

/* Same fade+scale morph as every other arrival, with a short rise on top
   so it still reads as coming up from the bottom edge. It used to be a
   bare 10px slide with no scale, which was the one piece of motion in the
   app that didn't speak the morph language. */
@keyframes toast-in {
  from { opacity: 0; transform: translateY(12px) scale(0.96); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes toast-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to { opacity: 0; transform: translateY(8px) scale(0.97); }
}

/* --- Small circular icon buttons (close / exit), matches the app's chrome --- */
.icon-x-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow-soft);
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  transition: border-color var(--t-hint) var(--ease-press),
              color var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press),
              box-shadow var(--t-hint) var(--ease-press);
}

.icon-x-btn:hover {
  color: var(--text);
  border-color: var(--accent-coral);
}

.icon-x-btn:active {
  transform: scale(0.88);
}

.icon-x-btn.tab-close-btn {
  width: 18px;
  height: 18px;
  font-size: 10px;
  margin-left: 2px;
}

.category-input-row {
  display: flex;
  /* flex-start, not center: a grown multi-line prompt would otherwise
     leave the remove button floating at dead-centre of a tall box instead
     of sitting level with the first line. */
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 12px;
}

.category-input-row .category-input {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}

.category-input-row .category-remove-btn {
  margin-top: 13px;
}

/* --- Random idea suggestions (classic mode) --- */
.category-btn-row {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

/* The prompts list is Create Room's `.view-scroll` (see the one-screen
   block up top for the shared scrolling/fade behaviour). All that's left
   here is its own floor -- the list stays tall enough to show a row and a
   half even on a short screen, so it never collapses to a sliver -- plus
   room at the bottom for the idea-suggestion overlay when that's up. */
.categories-scroll {
  min-height: 64px;
}

/* The suggestion card floats over the bottom of this list, so the last
   prompt has to be able to scroll clear of it -- otherwise the card
   covers that row's text and swallows taps on its remove button. Matches
   the card's own height (~50px) plus the list's fade. */
#classic-mode-section:has(#idea-suggestion-card:not(.hidden)) .categories-scroll {
  padding-bottom: 62px;
}

/* A transient overlay rather than inline content: stacked into the card it
   competed with the prompt list for a fixed height budget and pushed the
   list out over the Start Room button on anything shorter than a Pro Max.
   Out of flow it costs the layout nothing at any screen size.
   Anchored to the bottom of the prompts section (which is
   position: relative for exactly this) rather than to the viewport with a
   hand-tuned offset -- that way it can never land on the Start Room
   button, whatever the screen height or how much the layout compacts.
   No transform is used for placement: the morph keyframes animate
   `transform` and would clobber it (see MOTION.md). */
.idea-suggestion-card {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  padding: 12px 14px;
  border-radius: 16px;
  background: var(--card-sheen), var(--card-base);
  border: 1px solid var(--accent-cyan);
  box-shadow: var(--card-shadow), 0 12px 32px var(--shadow-dark);
}

.idea-suggestion-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.idea-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  border-radius: 14px;
  padding: 9px 10px 9px 14px;
  box-shadow: var(--card-shadow-soft);
}

.idea-chip-text {
  flex: 1;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.idea-chip-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--card-border);
  background: rgba(255, 255, 255, 0.04);
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
  transition: border-color var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press),
              background var(--t-hint) var(--ease-press);
}

.idea-chip-btn:active {
  transform: scale(0.88);
}

.idea-chip-btn.keep {
  color: var(--accent-green);
}

.idea-chip-btn.keep:hover {
  border-color: var(--accent-green);
  background: rgba(74, 222, 128, 0.12);
}

.idea-chip-btn.skip {
  color: var(--accent-cyan);
}

.idea-chip-btn.skip:hover {
  border-color: var(--accent-cyan);
  background: rgba(77, 216, 230, 0.12);
}

.idea-chip-btn.close {
  color: var(--text-muted);
}

.idea-chip-btn.close:hover {
  border-color: var(--accent-coral);
  color: var(--accent-coral);
  background: rgba(255, 95, 109, 0.12);
}

/* --- About / how to play ------------------------------------------------
   The top-bar button carries the app icon rather than an emoji, so it reads
   as "this is DISH itself" next to the account button. The icon is a full
   square image, so it needs the button's own rounding to clip it. */
.brand-btn {
  padding: 0;
  overflow: hidden;
}

.brand-btn img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: 50%;
}

.about-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

.about-logo {
  width: 46px;
  height: 46px;
  border-radius: 14px;
  flex: 0 0 auto;
  border: 1px solid var(--card-border);
}

.about-title {
  margin: 0;
  font-size: 22px;
  letter-spacing: 3px;
  font-weight: 800;
}

/* This modal is the one screen that genuinely has more to say than fits:
   it scrolls inside its own card, with the head pinned, rather than making
   the whole modal a long scroll behind a floating close button. */
.about-body {
  max-height: min(64vh, 560px);
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding-right: 2px;
  /* Clears the fade below, so scrolling to the end doesn't leave the
     credits themselves dimmed as though they were cut off. */
  padding-bottom: 26px;
  mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent 100%);
}

.about-body::-webkit-scrollbar {
  display: none;
}

.about-lede {
  color: var(--text);
  font-size: 14px;
  line-height: 1.55;
  margin: 6px 0 0;
}

.about-section {
  margin-top: 20px;
}

.about-section h3 {
  font-size: 13px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--accent-cyan);
  margin: 0 0 10px;
}

.about-steps,
.about-list {
  margin: 0;
  padding-left: 20px;
  color: var(--text-muted);
  font-size: 13.5px;
  line-height: 1.55;
}

.about-steps li,
.about-list li {
  margin-bottom: 8px;
  /* Every one of these lines contains player-facing free text or a long
     mode name; without this a narrow phone pushes them past the card. */
  overflow-wrap: anywhere;
}

.about-steps li:last-child,
.about-list li:last-child {
  margin-bottom: 0;
}

.about-steps strong,
.about-list strong {
  color: var(--text);
}

.about-mode {
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid var(--border-soft);
  border-radius: 16px;
  padding: 12px 14px;
  margin-bottom: 10px;
}

.about-mode:last-child {
  margin-bottom: 0;
}

.about-mode-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.about-mode-name {
  font-size: 15px;
  font-weight: 800;
  color: var(--text);
}

.about-mode p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--text-muted);
}

.about-tag {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 999px;
  flex: 0 0 auto;
}

.about-tag.free {
  color: var(--accent-green);
  background: rgba(74, 222, 128, 0.12);
  border: 1px solid rgba(74, 222, 128, 0.35);
}

.about-tag.pro {
  color: var(--accent-yellow);
  background: rgba(255, 210, 63, 0.12);
  border: 1px solid rgba(255, 210, 63, 0.35);
}

.about-install-copy {
  margin: 0 0 12px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

.about-install-btn {
  /* Slightly shorter than a screen's primary action: this is a side offer
     inside a reference panel, not the thing the screen is for. */
  padding: 12px;
  font-size: 14px;
}

.install-steps {
  margin-top: 12px;
}

/* An aside rather than another step -- it only applies if you got here in
   the wrong browser. */
.install-steps .install-note {
  list-style: none;
  margin-left: -20px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border-soft);
  font-size: 12.5px;
  color: var(--text-muted);
}

.about-credits {
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid var(--border-soft);
  text-align: center;
}

.about-credit-role {
  margin: 0;
  font-size: 12px;
  color: var(--text-muted);
}

.about-credit-name {
  margin: 4px 0 0;
  font-size: 17px;
  font-weight: 800;
  color: var(--text);
  letter-spacing: 0.3px;
}

/* --- Rewarded ads -------------------------------------------------------
   The free-tier lock line is a button now, so it has to shed the browser's
   button chrome and keep reading as the hint it looks like. */
.hint-button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  /* One line at 375px. Measured rather than guessed: a second line here
     pushes the prompt list down on the shortest screen. 10.5px fit with
     zero slack, which a font fallback (Inter not yet loaded, or blocked)
     would turn into an ellipsis -- 10px leaves ~15px of room. */
  font-size: 10px;
  letter-spacing: 0.1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.ad-card {
  max-width: 380px;
  text-align: center;
}

/* Unmistakably an ad slot, and unmistakably DISH's own for now: a real
   network's creative would replace the contents of this box. */
.ad-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-height: 168px;
  margin: 12px 0 14px;
  padding: 18px 16px;
  border-radius: 20px;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
}

.ad-house-logo {
  width: 64px;
  height: 64px;
  border-radius: 18px;
  border: 1px solid var(--card-border);
}

.ad-house-fact {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
}

.ad-progress {
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}

.ad-progress-bar {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
}

.ad-countdown {
  margin: 8px 0 4px;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.unlock-panel-title {
  margin-top: 0;
  font-size: 20px;
}

.unlock-list {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.unlock-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 13px;
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid var(--border-soft);
}

.unlock-row.is-held {
  border-color: rgba(74, 222, 128, 0.35);
  background: rgba(74, 222, 128, 0.07);
}

.unlock-row-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.unlock-row-label {
  font-size: 13.5px;
  font-weight: 800;
  overflow-wrap: anywhere;
}

.unlock-row-detail {
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.unlock-row-btn {
  flex: 0 0 auto;
  padding: 7px 13px;
  border-radius: 999px;
  border: 1px solid rgba(77, 216, 230, 0.45);
  background: rgba(77, 216, 230, 0.12);
  color: var(--accent-cyan);
  font-size: 12px;
  font-weight: 800;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press);
}

.unlock-row-btn:hover {
  background: rgba(77, 216, 230, 0.22);
}

.unlock-row-btn:active {
  transform: scale(0.94);
}

.unlock-row-done {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--accent-green);
  white-space: nowrap;
}

.unlock-row-note {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-muted);
  white-space: nowrap;
}

/* --- Promo code, on the plan screen ------------------------------------
   Entered before Stripe rather than only inside it: a 100%-off code was
   otherwise reachable only by opening a payment form for something that
   turns out to be free. */
.promo-row {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

.promo-row input[type="text"] {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
  text-transform: uppercase;
  letter-spacing: 1px;
  font-weight: 700;
}

.promo-apply-btn {
  flex: 0 0 auto;
  padding: 0 16px;
  border-radius: 999px;
  border: 1px solid rgba(77, 216, 230, 0.45);
  background: rgba(77, 216, 230, 0.12);
  color: var(--accent-cyan);
  font-size: 13px;
  font-weight: 800;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-hint) var(--ease-press),
              transform var(--t-press) var(--ease-press);
}

.promo-apply-btn:hover { background: rgba(77, 216, 230, 0.22); }
.promo-apply-btn:active { transform: scale(0.95); }
.promo-apply-btn:disabled { opacity: 0.5; cursor: default; }

.promo-status {
  margin-top: 8px;
  font-weight: 700;
}

/* The struck-through full price, shown only once a code is applied. */
.plan-price-was {
  margin-right: 10px;
  color: var(--text-muted);
  text-decoration: line-through;
  font-weight: 700;
}

/* Deleting is the one destructive thing in here, so it reads as such
   rather than sitting in the list looking like Change Email. */
.account-menu-btn-danger span:first-child {
  color: var(--accent-coral);
}

.delete-list {
  margin: 8px 0 12px;
  font-size: 12.5px;
  /* The account card centres its text, which reads badly for a bulleted
     list -- the markers end up detached from their lines. */
  text-align: left;
}

.delete-list strong {
  color: var(--accent-coral);
}

.btn.danger {
  background: linear-gradient(135deg, var(--accent-coral), var(--accent-coral-2));
  color: #fff;
  box-shadow: 0 8px 20px rgba(255, 95, 109, 0.3);
}

.account-reserve-btn {
  margin-top: 8px;
  padding: 10px;
  font-size: 13px;
}

.history-round-label {
  color: var(--accent-purple);
  margin-top: 18px;
}

.history-round-label:first-of-type {
  margin-top: 8px;
}

.history-round-tabs {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 4px 2px 14px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  touch-action: pan-x;
}

.history-round-tabs::-webkit-scrollbar {
  display: none;
}

.history-tab {
  flex: 0 0 auto;
  background: var(--card-sheen), var(--card-base-soft);
  border: 1px solid var(--card-border);
  border-radius: 999px;
  padding: 8px 18px;
  color: var(--text-muted);
  font-weight: 700;
  font-size: 13px;
  cursor: pointer;
  box-shadow: var(--card-shadow-soft);
  transition: border-color var(--t-hint) var(--ease-press),
              color var(--t-hint) var(--ease-press),
              box-shadow var(--t-hint) var(--ease-press);
}

.history-tab.active {
  color: var(--text);
  border-color: var(--accent-purple);
  box-shadow: var(--card-shadow-soft), 0 0 16px rgba(124, 140, 255, 0.3);
}

/* --- Reduced motion -------------------------------------------------------
   Only for players whose OS says they want it. The app stays fully usable
   and keeps its fade/scale identity — screens, modals and sections still
   morph, just quickly enough not to register as travel — while everything
   that runs forever or flies across the screen (orbits, the galaxy, dust,
   shooting stars, the avatar's punch to full screen) stops. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --t-press: 1ms;
    --t-hint: 1ms;
    --t-out: 90ms;
    --t-in: 110ms;
    --t-swap-out: 80ms;
    --t-swap-in: 100ms;
    --t-flight: 1ms;
    --t-fall: 320ms;
    --morph-scale-view: 1;
    --morph-scale-item: 1;
  }

  .shooting-stars,
  .accretion-field {
    display: none;
  }

  .spiral-arms-group,
  .orbit-wrapper,
  .player-avatar img,
  .falling-avatar img,
  .host-auth-badge,
  .orbit-pulse,
  .black-hole-core::before {
    animation: none !important;
  }

  .orbit-field {
    transition: none;
  }
}
