/* Stringwords design system — clean, white, mobile-first. */

@font-face {
  font-family: "MS";
  src: url("fonts/material-symbols.woff2") format("woff2");
  font-display: block;
}

/* Filled icon variant (subset: nav icons only) — `.ms.is-fill` swaps to it. */
@font-face {
  font-family: "MSF";
  src: url("fonts/material-symbols-fill.woff2") format("woff2");
  font-display: block;
}
.ms.is-fill { font-family: "MSF"; }

:root {
  --bg: #f4f6f9;
  --card: #ffffff;
  --ink: #1a1d21;
  --muted: #6b7280;
  --line: #e8ebef;
  --accent: #e2493b;
  --accent-soft: #fdeeec;
  --bolt: #d99a26;
  --bolt-soft: #fdf4e3;
  --good: #16a34a;
  --radius: 16px;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 6px 20px rgba(16, 24, 40, 0.06);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter,
    "Helvetica Neue", Arial, sans-serif;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}

/* Paint the canvas on <html> (not <body>) and fill the dynamic viewport, so no
   white "chin" shows below the content if the webview is taller than the page.
   Body stays transparent so the in-game theme backdrop (a fixed z-index:-1
   layer behind body) shows through. */
html {
  background: var(--bg);
  min-height: 100dvh;
}

body {
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.45;
  overscroll-behavior: none;
  min-height: 100dvh;
}

/* A permanent full-viewport backdrop so the webview's native (white) background
   can never show as a "chin" below the content — on any screen, at any height.
   Themes paint their own fx-play-bg layer (z-index:-1) above this. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background: var(--bg);
  pointer-events: none;
}

/* Scroll-locked screens (the in-game play view) fit the viewport exactly, so the
   document itself must not scroll or rubber-band on touch. Inner scrollers — the
   letter reel — keep scrolling via their own overscroll-behavior: contain. */
html.scroll-lock,
body.scroll-lock {
  overflow: hidden;
  height: 100dvh;
}

#root {
  max-width: 600px;
  margin: 0 auto;
  min-height: 100dvh;
  padding: 12px 14px 32px;
}

/* Hide the native document scrollbar — a custom overlay capsule replaces it
   (see `.cscroll` and scrollbar.ts). */
html,
body {
  scrollbar-width: none; /* Firefox */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

/* Custom scrollbar: a solid black capsule on the right edge that fades in while
   scrolling. Positioned below the sticky top bar (top set in JS) since only the
   content beneath it scrolls. Sits under modals/nav (z-50) so overlays cover it. */
.cscroll {
  position: fixed;
  right: 3px;
  width: 5px;
  z-index: 40;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.45s ease;
}
.cscroll.on {
  opacity: 1;
}
.cscroll-thumb {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  border-radius: 999px;
  background: #000;
  will-change: transform;
  /* Interactive only while the bar is shown (below), so the thin edge strip
     never intercepts taps on content while it's faded out. */
  pointer-events: none;
  touch-action: none; /* dragging the capsule shouldn't also scroll the page */
  cursor: grab;
}
.cscroll.on .cscroll-thumb {
  pointer-events: auto;
}
.cscroll-thumb:active {
  cursor: grabbing;
}
/* A wider invisible grab zone so the slim capsule is easy to catch, esp. on
   touch, without making the visible capsule any thicker. */
.cscroll-thumb::before {
  content: "";
  position: absolute;
  inset: -8px -6px -8px -12px;
}

button {
  font-family: var(--font);
  font-size: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.grow { flex: 1; min-width: 0; }
.dim { color: var(--muted); }

/* --- view transitions ----------------------------------------------- */

.view { opacity: 0; transform: translateY(6px); }
.view-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.22s ease, transform 0.22s ease;
}

/* --- top bar ---------------------------------------------------------- */

.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 2px 14px;
}

/* Sticky, gradient-fade headers: the top bar (and the leaderboard's tab row,
   wrapped in `.sticky-head`) stay pinned while the page scrolls. The fade IS the
   section's own background — opaque page-bg behind the controls, dissolving to
   transparent at the bottom edge — so content scrolls up into it rather than
   meeting a hard opaque bar. The scroll-locked play view keeps its own fixed
   layout, so it's excluded. */
.screen:not(.play-screen) > .topbar,
.sticky-head {
  position: sticky;
  top: 0;
  z-index: 30;
  /* Edge-to-edge header: bleeds past #root's side padding to the screen edges,
     with the controls kept inset. The fade IS the background — opaque page-bg
     behind the controls, dissolving to transparent at the bottom edge so content
     scrolls up into it rather than meeting a hard bar. */
  margin: 0 -14px;
  padding-left: 14px;
  padding-right: 14px;
  background: linear-gradient(to bottom, var(--bg) calc(100% - 18px), transparent);
}

/* Extra bottom room so the fade tail clears the controls rather than washing
   over them. */
.screen:not(.play-screen) > .topbar { padding-bottom: 20px; }
.sticky-head { padding-bottom: 18px; }
.sticky-head > .tabs { margin-bottom: 0; }

.bar-title {
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bar-sub {
  font-size: 12.5px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.logo-dot,
.icon-btn {
  width: 42px;
  height: 42px;
  border-radius: 13px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  font-size: 17px;
}

/* 2.5D raised top-bar items: same depth language as the push buttons — a face on
   a darker edge with a static gloss. Per-item rules below tune face/edge/shine. */
.logo-dot,
.icon-btn,
.bits-chip {
  --tb-edge: #c8cdd5;
  --tb-shine: rgba(255, 255, 255, 0.5);
  --tb-lift: 3px;
  background-image: linear-gradient(to bottom, var(--tb-shine), rgba(255, 255, 255, 0) 50%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.25),
    0 var(--tb-lift) 0 var(--tb-edge),
    0 calc(var(--tb-lift) + 2px) 8px rgba(16, 24, 40, 0.16);
}

.logo-dot {
  background-color: #262a31;
  color: #fff;
  --tb-edge: #0a0c10;
  --tb-shine: rgba(255, 255, 255, 0.18);
}

.icon-btn {
  background-color: var(--card);
  transition: transform 0.07s ease, box-shadow 0.07s ease;
}

/* Push: drop the icon button onto its edge, collapse the edge, tighten the shadow. */
.icon-btn:active {
  transform: translateY(var(--tb-lift));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.25),
    0 0 0 var(--tb-edge),
    0 1px 3px rgba(16, 24, 40, 0.2);
}

.bits-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background-color: var(--bolt-soft);
  color: var(--bolt);
  font-weight: 800;
  font-size: 14px;
  border-radius: 999px;
  padding: 7px 13px 7px 9px;
  flex: none;
  --tb-edge: #ecd9ac;
  --tb-shine: rgba(255, 255, 255, 0.6);
}

/* --- cards ------------------------------------------------------------- */

.card {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
  margin-bottom: 12px;
}

.card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.card-title { font-weight: 800; font-size: 16px; }

.section-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 800;
  font-size: 15px;
  margin: 18px 4px 10px;
}

.section-head .ms { color: var(--muted); }

.section-head .grow { white-space: nowrap; }

@media (max-width: 430px) {
  .section-head .row-sub { display: none; }
}

.link-btn {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  color: var(--accent);
  font-weight: 700;
  font-size: 13.5px;
  padding: 6px 4px;
}

/* --- buttons ------------------------------------------------------------ */

/* 2.5D push buttons: a colored top face resting on a darker bottom "edge", with
   a static gloss across the top. Pressing drops the face onto its edge for a
   tactile push. Variants set --btn-face / --btn-edge / --btn-shine; the depth
   mechanics are shared. */
.btn {
  --btn-face: var(--card);
  --btn-edge: #c8cdd5;
  --btn-shine: rgba(255, 255, 255, 0.4);
  /* A 1px outline so light buttons stay visible against a white modal. */
  --btn-ring: transparent;
  --btn-lift: 4px;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 46px;
  padding: 0 18px;
  border-radius: 14px;
  font-weight: 800;
  font-size: 15px;
  color: var(--ink);
  background-color: var(--btn-face);
  /* Static shine: a fixed gloss fading out across the top ~half of the face. */
  background-image: linear-gradient(to bottom, var(--btn-shine), rgba(255, 255, 255, 0) 46%);
  box-shadow:
    inset 0 0 0 1px var(--btn-ring),
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 var(--btn-lift) 0 var(--btn-edge),
    0 calc(var(--btn-lift) + 3px) 12px rgba(16, 24, 40, 0.18);
  transition: transform 0.07s ease, box-shadow 0.07s ease, background-color 0.12s ease;
}

/* Push: the face drops onto its edge, the edge collapses, the drop shadow tightens. */
.btn:active {
  transform: translateY(var(--btn-lift));
  box-shadow:
    inset 0 0 0 1px var(--btn-ring),
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 0 0 var(--btn-edge),
    0 1px 4px rgba(16, 24, 40, 0.22);
}
.btn[disabled] { opacity: 0.5; pointer-events: none; }

/* A charcoal face (not pure ink) so the darker edge below it is actually visible
   — a true-black face hides its own side wall. */
.btn.primary {
  --btn-face: #262a31;
  --btn-edge: #0a0c10;
  --btn-shine: rgba(255, 255, 255, 0.2);
  color: #fff;
}

/* White face with a soft gray gradient; the gray outline keeps it distinct from
   the white modal behind it. */
.btn.ghost {
  --btn-edge: #cfd5de;
  --btn-ring: rgba(16, 24, 40, 0.16);
  background-image: linear-gradient(to bottom, #ffffff, #eaedf2);
  color: var(--ink);
}

.btn.danger {
  --btn-face: var(--accent-soft);
  --btn-edge: #f0c6c1;
  --btn-shine: rgba(255, 255, 255, 0.35);
  --btn-ring: rgba(190, 60, 50, 0.2);
  color: var(--accent);
}

/* Less gloss so the accent reads as a saturated red, not a washed-out pink. */
.btn.big {
  --btn-face: var(--accent);
  --btn-edge: #ab372c;
  --btn-shine: rgba(255, 255, 255, 0.14);
  width: 100%;
  min-height: 54px;
  font-size: 16.5px;
  color: #fff;
}

.btn.fav-on {
  --btn-face: var(--accent-soft);
  --btn-edge: #f0c6c1;
  --btn-shine: rgba(255, 255, 255, 0.35);
  --btn-ring: rgba(190, 60, 50, 0.2);
  color: var(--accent);
}

/* --- hub: featured daily -------------------------------------------------- */

.featured {
  position: relative;
  overflow: hidden;
  background: linear-gradient(150deg, #272b31 0%, #15171b 62%);
  color: #fff;
  padding: 20px 18px 18px;
}

/* Soft accent glow in the top-right corner for a bit of depth. */
.featured::before {
  content: "";
  position: absolute;
  top: -70px;
  right: -55px;
  width: 190px;
  height: 190px;
  background: radial-gradient(circle, rgba(226, 73, 59, 0.5), transparent 70%);
  z-index: 0;
  pointer-events: none;
}
.featured > * { position: relative; z-index: 1; }

.featured-head {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 14px;
}

.featured-headtext {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.featured-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  font-weight: 800;
  color: #f3c969;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.featured-date {
  font-size: 13.5px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.72);
}

.featured-num {
  margin-left: auto;
  font-size: 30px;
  font-weight: 900;
  letter-spacing: -0.02em;
  line-height: 1;
}

.featured-meta {
  display: flex;
  align-items: center;
  /* Reflow whole items to a second line if the row is tight, rather than
   * letting an item's text (e.g. the countdown) break across two lines. */
  flex-wrap: wrap;
  gap: 6px 16px;
  color: rgba(255, 255, 255, 0.75);
  font-size: 13.5px;
  margin-bottom: 16px;
}

.solved-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  padding: 3px 10px 3px 8px;
  border-radius: 999px;
  background: rgba(47, 174, 102, 0.18);
  color: #6fe0a2;
  font-size: 12.5px;
  font-weight: 800;
}
.solved-pill .ms { font-size: 15px; }

.strip-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* Keep each item (especially the countdown) on a single line. */
  white-space: nowrap;
}

/* Mod-only analytics under the daily play button (on the dark featured card). */
.mod-analytics {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.ma-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.55);
  margin-bottom: 10px;
}
.ma-head .ms { font-size: 15px; color: rgba(255, 255, 255, 0.7); }
.ma-stats { display: flex; gap: 10px; }
.ma-stat {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.07);
}
.ma-stat .ms { font-size: 18px; color: rgba(255, 255, 255, 0.65); }
.ma-value { font-size: 18px; font-weight: 900; color: #fff; }
.ma-label {
  font-size: 12px;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.55);
}

.daily-strip {
  display: flex;
  gap: 16px;
  padding: 0 6px 10px;
  color: var(--muted);
  font-size: 13.5px;
}

.daily-strip .warn { color: var(--accent); font-weight: 700; }

/* --- hub: create CTA, quick links ---------------------------------------- */

.create-cta {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  border: 1.5px dashed #d4d9df;
  box-shadow: none;
}

.create-cta > .ms:first-child {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

/* --- daily rewards wheel ------------------------------------------------- */

/* Hub CTA: a free-spin nudge, gold-accented to read as a reward. */
.wheel-cta {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  background: linear-gradient(120deg, var(--bolt-soft), #fff 70%);
  border: 1.5px solid #f2e2bc;
}
.wheel-cta > .ms:first-child {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: var(--bolt-soft);
  color: var(--bolt);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.wheel-cta .row-sub { color: var(--bolt); font-weight: 700; }
.wheel-cta > .ms:last-child { color: var(--muted); flex: none; }

.wheel-sub { white-space: normal; text-align: center; margin: 2px 0 0; }

/* --- zen mode ------------------------------------------------------------ */

/* Hub CTA: a calm, teal-accented nudge so it reads as the relaxed mode. */
.zen-cta {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  background: linear-gradient(120deg, #e7f6f3, #fff 70%);
  border: 1.5px solid #cfe9e3;
}
.zen-cta > .svg-ic:first-child {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: #d8f0eb;
  color: #0f8e7f;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  font-size: 23px; /* the inline svg is sized in em, so this sets the leaf size */
}
.zen-cta > .ms:last-child { color: var(--muted); flex: none; }

/* The score chip in the Zen board bar — teal to set it apart from the clock. */
.board-chip.zen-score { color: #0f8e7f; background: #e7f6f3; }

/* The wheel itself, inside the spin modal. */
.wheel-wrap {
  position: relative;
  width: 100%;
  max-width: 290px;
  margin: 8px auto 16px;
  aspect-ratio: 1 / 1;
  cursor: pointer;
}
.wheel-svg {
  width: 100%;
  height: 100%;
  display: block;
  transform-origin: 50% 50%;
  filter: drop-shadow(0 6px 16px rgba(16, 24, 40, 0.2));
}
/* Pointer: a fixed tab biting into the top of the wheel. */
.wheel-pointer {
  position: absolute;
  top: -3px;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 12px solid transparent;
  border-right: 12px solid transparent;
  border-top: 20px solid var(--accent);
  filter: drop-shadow(0 2px 2px rgba(16, 24, 40, 0.25));
  z-index: 2;
}

.wheel-reward { text-align: center; }
.wheel-reward.show { animation: wheel-pop 0.4s cubic-bezier(0.2, 1.5, 0.4, 1); }
.wheel-prize {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-weight: 800;
  font-size: 18px;
  margin: 2px 0 10px;
}
.wheel-prize .ms { color: var(--bolt); font-size: 22px; }
.wheel-reward .swatch { max-width: 240px; margin: 0 auto; }
@keyframes wheel-pop {
  from { transform: scale(0.8); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* Room at the bottom of every (non-immersive) screen so the floating nav never
   covers content. */
.screen { padding-bottom: 92px; }
body.immersive .screen { padding-bottom: 0; }

/* Persistent frosted bottom nav: a main capsule (leaderboards, cosmetics,
   archive) with the profile button in its own capsule alongside it. Hidden on
   the immersive play/builder screens. */
.bottom-nav {
  position: fixed;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  display: flex;
  gap: 10px;
  z-index: 50;
}

body.immersive .bottom-nav { display: none; }

.nav-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* Frosted glass */
  background: rgba(255, 255, 255, 0.68);
  -webkit-backdrop-filter: blur(18px) saturate(1.6);
  backdrop-filter: blur(18px) saturate(1.6);
  border: 1px solid rgba(255, 255, 255, 0.6);
  box-shadow: 0 6px 24px rgba(16, 24, 40, 0.16);
  border-radius: 999px;
  padding: 6px;
}

.nav-btn {
  width: 46px;
  height: 46px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--ink);
  transition: background 0.12s ease, transform 0.12s ease;
}

.nav-btn .ms { font-size: 23px; }
.nav-btn:active { transform: scale(0.9); }

/* Active destination — accent color + (via .is-fill) a filled glyph. */
.nav-btn.on { color: var(--accent); }

/* Profile button wearing the user's snoovatar. */
.nav-btn.nav-pfp { padding: 0; }
.nav-avatar {
  width: 34px;
  height: 34px;
  font-size: 15px;
  border: 2px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 1px 3px rgba(16, 24, 40, 0.2);
}

/* Create is the primary action — filled accent so it stands out. */
.nav-btn.nav-create { background: var(--accent); color: #fff; }

@media (hover: hover) {
  .nav-btn:hover { background: rgba(16, 24, 40, 0.06); }
  .nav-btn.nav-create:hover { background: var(--accent); filter: brightness(1.06); }
}

.hub-foot {
  text-align: center;
  color: var(--muted);
  font-size: 12.5px;
  padding: 8px 0 4px;
}

/* --- stats ------------------------------------------------------------------ */

.stats-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.stat-box {
  background: var(--bg);
  border-radius: 12px;
  padding: 10px 6px;
  text-align: center;
}

.stat-box .ms { color: var(--muted); font-size: 16px; }

.stat-value {
  font-size: 18px;
  font-weight: 900;
  letter-spacing: -0.01em;
}

.stat-label {
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-top: 1px;
}

/* --- play screen --------------------------------------------------------------- */

.board-card { padding: 10px; }

.board-wrap {
  width: 100%;
  aspect-ratio: 1 / 1;
  max-height: min(78vw, 460px);
  position: relative;
}

.board-wrap canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* --- in-game board overlay ---------------------------------------------- */

/* The whole game fits the viewport — the board flexes, controls stay put,
 * so nothing scrolls during play. */
.play-screen {
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Fill the viewport minus #root's chrome padding. The dvh form subtracts the
     device safe areas (see `#root:has(.play-screen)` below) so the height stays
     in lockstep with the padding — otherwise the top bar slips under the status
     bar / app chrome on iPads and notched webviews. */
  height: calc(100vh - 44px);
  height: calc(
    100dvh - max(12px, env(safe-area-inset-top, 0px)) -
      max(32px, env(safe-area-inset-bottom, 0px))
  );
  /* Clip rather than spill: a too-tall board can't push the page into scroll. */
  overflow: hidden;
  /* No rubber-band: a drag starting on the board must never scroll the document
     or collapse the mobile browser toolbar (which reads as the field lurching). */
  overscroll-behavior: none;
}

/* Grow #root's top/bottom chrome padding to clear the device safe areas so the
   play/zen top bar never hides under the status bar or app chrome (keeps the
   base 12/32 as a floor). Sits BEFORE the replay override, which wants tighter
   padding, so that stays authoritative for the replay screen. */
#root:has(.play-screen) {
  padding-top: max(12px, env(safe-area-inset-top, 0px));
  padding-bottom: max(32px, env(safe-area-inset-bottom, 0px));
}

.play-screen .topbar,
.play-screen .board-bar {
  flex: none;
}

.play-board {
  margin-bottom: 8px;
}

.play-screen .play-board {
  flex: 1 1 auto;
  min-height: 0;
  /* Keep the board off the screen edges so pegs near the border have room to be
     grabbed and wrapped, and bottom pegs clear the phone's bottom gesture zone. */
  padding: 4px 12px calc(4px + env(safe-area-inset-bottom, 0px));
  /* Swallow touch gestures across the whole board area — including this padding,
     which is outside the canvas — so an edge-peg drag never scrolls the page. */
  touch-action: none;
  overscroll-behavior: none;
}

/* The wrapper fills the leftover space; the canvas centres a square board
 * inside it, so the game fits whatever rectangle is available. */
.play-screen .play-board .board-wrap {
  width: 100%;
  height: 100%;
  max-height: none;
  aspect-ratio: auto;
  touch-action: none;
}

/* Controls live below the canvas so they never cover letters. */
.board-bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}

.board-status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  min-height: 30px;
}

/* Desktop: two columns. The board takes the wide left column; the status chips
 * (time / words / hint), the buddy, and the letter reel stack in a narrower
 * right column. The top bar spans the full width above both. Same 700px
 * breakpoint as the #root widening below. */
@media (min-width: 700px) {
  .play-screen {
    display: grid;
    /* minmax(0, …) lets the board column shrink to its grid share instead of
     * blowing out to the canvas's intrinsic width (which would push the layout
     * off screen). The board's ResizeObserver then sizes the canvas to the cell.
     * The right column is kept just wide enough for the chips and letter reel so
     * the board gets the rest of the width. */
    grid-template-columns: minmax(0, 1fr) minmax(220px, 280px);
    grid-template-rows: auto 1fr;
    grid-template-areas:
      "top top"
      "board bar";
    column-gap: 16px;
  }
  .play-screen > .topbar { grid-area: top; }
  .play-screen .play-board {
    grid-area: board;
    min-width: 0;
    min-height: 0;
    margin-bottom: 0;
  }
  .play-screen .board-bar {
    grid-area: bar;
    align-self: stretch;
    justify-content: center;
    min-height: 0;
    margin-bottom: 0;
  }
  /* Chips and the word reel both center within the sidebar, lined up over the
   * letter canvas. */
  .play-screen .board-status { justify-content: center; }
  .play-screen .board-bar .lb-line { gap: 4px; }
  /* The buddy drops below the canvas here (instead of overlapping its top), so
   * reorder it last and clear the overlap margin. It's also bigger on desktop. */
  .play-screen .board-bar .buddy {
    order: 1;
    margin: 6px 0 0 6px;
    height: 76px;
  }
  .play-screen .board-bar .bd-char { width: 72px; height: 72px; }
  /* Lift the speech bubble so its tail still points at the larger character. */
  .play-screen .board-bar .bd-bubble { left: 60px; bottom: 42px; }
  /* The sidebar is narrower than the full-width mobile reel, so full-size boxes
   * would squish or overflow once a word runs long. Scale the boxes down (and
   * let them shrink to a smaller min) so long words stay proportional and fit. */
  .play-screen .board-bar .lb-box {
    flex-basis: 34px;
    min-width: 20px;
    height: 40px;
    font-size: 18px;
    border-radius: 10px;
  }
}

.board-status:empty { display: none; }
.board-info { display: flex; gap: 8px; }

.board-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--card);
  box-shadow: var(--shadow);
  border-radius: 999px;
  padding: 6px 12px;
  font-size: 13px;
  font-weight: 800;
  color: var(--muted);
}

.board-chip .ms { font-size: 16px; }
.board-chip.warn { color: var(--accent); background: var(--accent-soft); }
.board-chip.win { color: #1f9d57; background: #e6f6ec; }

/* Tappable hint chip — accent-tinted so it reads as an action, not a stat. */
.hint-chip {
  color: var(--accent);
  background: var(--accent-soft);
  cursor: pointer;
  transition: transform 0.12s ease;
}
.hint-chip:active { transform: scale(0.95); }
.hint-chip .svg-ic svg { width: 1.2em; height: 1.2em; }

/* Inline SVG glyph, aligned like the icon-font's `.ms`. */
.svg-ic { display: inline-flex; align-items: center; }
.svg-ic svg { width: 1.15em; height: 1.15em; display: block; }

/* Bits balance line in the hint modal. */
.hint-balance {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 6px 0 2px;
  color: var(--muted);
  font-weight: 700;
  font-size: 13px;
}
.hint-balance .ms { font-size: 17px; color: var(--accent); }

.live-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--bg);
  color: var(--muted);
  border-radius: 999px;
  padding: 6px 12px;
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.06em;
  margin-left: auto;
  max-width: 70%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.live-chip .ms { font-size: 16px; flex: none; }
.live-chip.hot { background: var(--good); color: #fff; }
.live-chip.dim { color: var(--muted); }

.board-dock {
  display: flex;
  justify-content: center;
  gap: 10px;
  padding: 2px 4px;
}

.dock-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: 46px;
  min-width: 46px;
  padding: 0 14px;
  border-radius: 15px;
  background: var(--card);
  box-shadow: var(--shadow);
  font-weight: 800;
  transition: transform 0.12s ease;
}

.dock-btn:active { transform: scale(0.94); }
.dock-btn .ms { font-size: 21px; }
.dock-btn.wide { padding: 0 14px 0 16px; }
.dock-label { font-size: 14.5px; }

.pill-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 12.5px;
  font-weight: 800;
}

/* --- in-game word reel ----------------------------------------------------- */

.lboxes {
  width: 100%;
  cursor: pointer;
  user-select: none;
}

/* Vertical, snap-scrolling reel of word rows; edges fade out so the rows above
   and below the centered one peek through. */
.lb-reel {
  height: 118px;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  overscroll-behavior: contain;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
}
.lb-reel::-webkit-scrollbar { display: none; }

/* Top/bottom padding lets the first and last rows snap to center. */
.lb-track {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 40px 0;
}

.lb-line {
  scroll-snap-align: center;
  width: 100%;
  display: flex;
  gap: 5px;
  justify-content: center;
  transform: scale(0.6);
  opacity: 0.55;
  transform-origin: center;
  /* Slight overshoot so a row "pops bigger" as it rises into focus. */
  transition: transform 0.28s cubic-bezier(0.2, 1.25, 0.4, 1), opacity 0.24s ease;
}
.lb-line.active { transform: scale(1); opacity: 1; }

.lb-box {
  flex: 0 1 44px;
  min-width: 28px;
  height: 50px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.45);
  -webkit-backdrop-filter: blur(8px) saturate(1.4);
  backdrop-filter: blur(8px) saturate(1.4);
  border: 1px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0 2px 8px rgba(16, 24, 40, 0.08);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
  font-size: 23px;
  color: var(--ink);
  transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}

/* A valid new word turns the active row green. */
.lb-line.valid .lb-box.filled {
  background: rgba(86, 200, 130, 0.6);
  border-color: #3fae66;
  color: #0f6b35;
}

/* A word you've already used turns the row red while you hold the string. */
.lb-line.used .lb-box.filled {
  background: rgba(229, 72, 77, 0.6);
  border-color: #e5484d;
  color: #fff;
}

/* Banked words keep a soft-green "found" tint. */
.lb-line.lb-done .lb-box {
  background: rgba(86, 200, 130, 0.42);
  border-color: rgba(63, 174, 102, 0.6);
  color: #0f6b35;
}

/* New box popping in as a word grows. */
.lb-box.lb-in { animation: lb-in 0.28s cubic-bezier(0.2, 1.3, 0.5, 1); }
@keyframes lb-in {
  from { transform: scale(0); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* The unused trailing box popping away on submit (before the word shifts back). */
.lb-box.lb-out { animation: lb-out 0.2s ease forwards; pointer-events: none; }
@keyframes lb-out {
  from { transform: scale(1); opacity: 1; }
  to { transform: scale(0); opacity: 0; }
}

/* Flash a banked word's panels red when the player tries to re-use it. */
.lb-line.lb-flash .lb-box { animation: lb-flash 0.85s ease; }
@keyframes lb-flash {
  15%, 65% {
    background: rgba(229, 72, 77, 0.62);
    border-color: #e5484d;
    color: #fff;
  }
}

/* Banking: the completed row pops up then shrinks before floating away. */
.lb-line.lb-pop .lb-box { animation: lb-pop 0.44s cubic-bezier(0.3, 1, 0.5, 1); }
@keyframes lb-pop {
  0% { transform: scale(1); }
  34% { transform: scale(1.16); }
  70% { transform: scale(1); }
  100% { transform: scale(1); }
}

/* On dark themes the frosted boxes read better with a touch more fill. */
.fx-dark .lb-box { background: rgba(255, 255, 255, 0.16); border-color: rgba(255, 255, 255, 0.35); color: #f3f1ff; }
.fx-dark .lb-line.valid .lb-box.filled,
.fx-dark .lb-line.lb-done .lb-box { background: rgba(86, 200, 130, 0.4); border-color: #5fd592; color: #d6ffe6; }
.fx-dark .lb-line.used .lb-box.filled { background: rgba(229, 72, 77, 0.5); border-color: #ff7b7e; color: #ffe3e3; }


.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 4px 4px;
}

.controls .btn.ghost { flex: 1 1 auto; }
.controls .btn.primary.grow { flex: 1 1 160px; }
.controls .hint { align-self: center; }

/* "+10 Bits" reward hint under the Publish button. */
.create-reward {
  flex: 1 0 100%;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 3px;
  margin-top: -2px;
  padding-right: 4px;
  color: var(--bolt);
  font-weight: 800;
  font-size: 12.5px;
}
.create-reward .ms { font-size: 15px; }
.center { text-align: center; }

.hud { padding: 12px 14px; }

.stat-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  text-align: center;
  border-bottom: 1px solid var(--line);
  padding-bottom: 10px;
  margin-bottom: 10px;
}

.stat-row .stat-value { font-size: 22px; }

.word-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  max-height: 132px;
  overflow-y: auto;
}

.word-chips.tight { max-height: 180px; margin-top: 10px; }

.chip {
  background: var(--bg);
  border-radius: 999px;
  padding: 5px 11px;
  font-weight: 800;
  font-size: 12.5px;
  letter-spacing: 0.04em;
}

.chip.pop {
  animation: chip-pop 0.34s cubic-bezier(0.2, 1.6, 0.4, 1);
  background: var(--accent-soft);
  color: var(--accent);
}

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

.live-label {
  font-size: 13px;
  font-weight: 700;
  color: var(--muted);
  margin-bottom: 8px;
}

.word-chips.found { max-height: 220px; }

.found-meta {
  display: flex;
  gap: 14px;
  color: var(--muted);
  font-size: 13px;
  margin: -4px 0 10px;
}

.found-meta .ms { color: var(--bolt); }

@keyframes chip-pop {
  0% { transform: scale(0.4); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

.hint { color: var(--muted); font-size: 13px; padding: 2px; }

/* --- rows / lists ------------------------------------------------------------------ */

.card.list { padding: 4px 6px; }

.row-card { display: flex; align-items: center; gap: 12px; }
.row-card > .ms { color: var(--bolt); font-size: 22px; }

.row-title {
  font-weight: 800;
  font-size: 14.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A tappable username — looks like the text around it, but reveals it's a link
   on hover/press. Used in leaderboard rows, challenge authors, etc. */
.user-link {
  display: inline-block;
  max-width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  vertical-align: bottom;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-link:hover { color: var(--accent); text-decoration: underline; }
.user-link:active { opacity: 0.6; }

/* User card: the standing line under the (header) username, above the stats. */
.uc-standing {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: -2px 0 12px;
}

.row-sub {
  font-size: 12.5px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.row-sub.pad { padding: 0 6px 10px; white-space: normal; }

/* Confirm-dialog body: let the full message wrap instead of truncating. */
.row-sub.confirm-body {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

.ch-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 12px 8px;
  border-radius: 12px;
  transition: background 0.12s ease;
}

/* Only rows wired to open a challenge get the affordance (mod-list rows reuse
   .ch-row without being tappable). */
.ch-row[role="button"] { cursor: pointer; }
.ch-row:active { background: var(--bg); }
.ch-row + .ch-row { border-top: 1px solid var(--line); }

.ch-stats {
  display: flex;
  align-items: center;
  gap: 11px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  flex: none;
}

.ch-stats span { display: inline-flex; align-items: center; gap: 3px; }
.ch-stats .ms { font-size: 14px; }

/* Likes + rating shown under a challenge's details (reuses .ch-stats styling). */
.ch-meta { margin-top: 3px; }
.ch-meta .ms.ms-favorite { color: #e2493b; }

/* "NEW" tag on a freshly published board. */
.new-badge {
  display: inline-block;
  margin-right: 6px;
  padding: 1px 5px;
  border-radius: 6px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.04em;
  vertical-align: 1.5px;
}

/* --- leaderboards ------------------------------------------------------------------- */

.tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
  overflow-x: auto;
  scrollbar-width: none;
  /* Soft-fade an overflowing edge instead of a hard cutoff. `edgeFade()` toggles
     fade-l / fade-r so only a side that actually scrolls fades. The widths are
     0 until then, so the mask is fully opaque (no fade) when everything fits. */
  --fade-l: 0px;
  --fade-r: 0px;
  -webkit-mask-image: linear-gradient(
    to right, transparent, #000 var(--fade-l),
    #000 calc(100% - var(--fade-r)), transparent);
  mask-image: linear-gradient(
    to right, transparent, #000 var(--fade-l),
    #000 calc(100% - var(--fade-r)), transparent);
}
.tabs::-webkit-scrollbar { display: none; }
.tabs.fade-l { --fade-l: 26px; }
.tabs.fade-r { --fade-r: 26px; }

.tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 14px;
  border-radius: 999px;
  background: var(--card);
  box-shadow: var(--shadow);
  font-weight: 700;
  font-size: 13.5px;
  color: var(--muted);
  flex: none;
}

.tab.on { background: var(--ink); color: #fff; }

.lb-unit {
  color: var(--muted);
  font-size: 12.5px;
  text-align: center;
  margin-bottom: 8px;
}

.lb-list { display: flex; flex-direction: column; }
.lb-list.card { padding: 6px 10px; }

.lb-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 4px;
}

.lb-row + .lb-row { border-top: 1px solid var(--line); }

.lb-row.mine {
  background: var(--accent-soft);
  border-radius: 10px;
  padding-left: 8px;
  padding-right: 8px;
  border-top: none;
}

.lb-rank {
  width: 30px;
  font-weight: 900;
  color: var(--muted);
  text-align: center;
  flex: none;
}

.lb-rank.r1 .ms { color: #d9a826; }
.lb-rank.r2 .ms { color: #9aa3ad; }
.lb-rank.r3 .ms { color: #b9722d; }

.lb-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Size to content (the flex spacer holds the score to the right); min-width:0
     keeps the ellipsis working when a long name has to shrink. */
  min-width: 0;
  flex: 0 1 auto;
}

.lb-extra { color: var(--muted); font-size: 12px; flex: none; }
.lb-score { font-weight: 900; flex: none; }
.lb-sep { text-align: center; color: var(--muted); padding: 2px 0; }

/* A player's custom buddy, sitting right after their username. The flex spacer
 * after it keeps the score on the right, so the buddy never reaches the
 * profile-theme scene that peeks in from the right edge. */
.lb-buddy {
  width: 30px;
  height: 30px;
  flex: none;
  margin: -6px 2px -6px 4px;
}
.lb-buddy .bd-svg { overflow: visible; }


/* Win modal page 1: the whole leaderboard block is tappable → full board. */
.win-lb {
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 2px 10px 8px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.win-lb:hover { border-color: var(--accent); }
.win-lb:active { background: var(--accent-soft); }
.win-lb .card-head { cursor: pointer; }

/* The "you placed here" row when the player is below the top 5: a dashed
 * capsule that stands apart from the solid top-5 rows. */
.lb-ghost {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border: 1px dashed var(--accent);
  border-radius: 999px;
  background: var(--accent-soft);
}
.lb-ghost .lb-rank { color: var(--accent); }

.rank-pill {
  background: var(--ink);
  color: #fff;
  font-weight: 800;
  font-size: 13px;
  border-radius: 999px;
  padding: 5px 11px;
  flex: none;
}

/* --- creator / rating ------------------------------------------------------------------ */

.creator-row { display: flex; align-items: center; gap: 10px; }

.creator-row > .ms {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  flex: none;
}

.desc { color: var(--ink); font-size: 14px; margin: 10px 2px 0; }

/* Play count sitting beside the like button in the challenge info card. */
.creator-row .meta-plays {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex: none;
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
}
.creator-row .meta-plays .ms { font-size: 17px; }

/* Win-flow reaction step: the Like label rolls vertically into the new count. */
.react-like .like-roll {
  position: relative;
  display: inline-block;
  overflow: hidden;
  height: 1.2em;
  line-height: 1.2em;
}
.react-like .like-label { display: block; }
.react-like .like-label.like-exit {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  animation: likeRollOut 0.2s ease forwards;
}
.react-like .like-label.like-enter { animation: likeRollIn 0.2s ease; }
@keyframes likeRollOut {
  to { transform: translateY(-100%); opacity: 0; }
}
@keyframes likeRollIn {
  from { transform: translateY(100%); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

/* --- builder ------------------------------------------------------------------------------ */

.builder-status {
  text-align: center;
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
  padding: 6px 4px 10px;
}

.builder-status.warn { color: var(--accent); }

.letter-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 8px;
  margin: 6px 0;
}

.letter-key {
  aspect-ratio: 1;
  border-radius: 12px;
  background: var(--bg);
  font-weight: 900;
  font-size: 17px;
  transition: transform 0.1s ease, background 0.1s ease;
}

.letter-key:active { transform: scale(0.9); }
.letter-key.on { background: var(--ink); color: #fff; }

.field {
  width: 100%;
  border: 1.5px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  font-family: var(--font);
  font-size: 15px;
  color: var(--ink);
  padding: 12px 14px;
  margin: 6px 0;
  outline: none;
}

.field:focus { border-color: var(--ink); }

/* Mod "create code" form: stacked fields + button. */
.code-form { display: flex; flex-direction: column; gap: 4px; }
.code-form .btn { margin-top: 6px; }

/* "OFF" tag on a deactivated code. */
.code-off {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 6px;
  border-radius: 6px;
  background: rgba(16, 24, 40, 0.08);
  color: var(--muted);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.04em;
  vertical-align: 1.5px;
}

.field-label {
  display: block;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--muted);
  margin-top: 6px;
}

/* Input + inline action button (mod join-prompt setting). */
.sub-field { display: flex; align-items: center; gap: 8px; }
.sub-field .field { flex: 1; margin: 0; min-width: 0; }
.sub-field .btn { flex: none; }

/* --- profile / shop -------------------------------------------------------------------------- */

.id-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
}

/* The equipped buddy, perched next to the player's name in their profile. */
.id-buddy { width: 52px; height: 52px; flex: none; }
.id-buddy .bd-svg { overflow: visible; }

.avatar {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-weight: 900;
  font-size: 21px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  overflow: hidden;
}

/* Snoovatars are tall full-body PNGs — cover-fit anchored to the top frames the
   head/shoulders inside the circle. A light fill sits behind the transparent
   art. */
.avatar.has-snoo { background: #dfe6ef; }
.avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* Profile-theme "effect" — a decorative scene peeking in from the right edge of
   a card the player's name occupies (profile header, user card, leaderboard
   rows). Mirrors Discord-style profile effects. */
/* Preference row with a sliding on/off switch (e.g. theme graphics toggle). */
.pref-row { display: flex; align-items: center; gap: 12px; }
/* Let the description wrap to multiple lines instead of truncating. */
.pref-row .row-sub { white-space: normal; overflow: visible; text-overflow: clip; }
.pref-row > .ms:first-child {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: var(--bg);
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.seg-toggle {
  position: relative;
  width: 46px;
  height: 28px;
  border-radius: 999px;
  background: var(--line);
  flex: none;
  transition: background 0.16s ease;
}
.seg-toggle.on { background: var(--good); }
.seg-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(16, 24, 40, 0.25);
  transition: transform 0.16s ease;
}
.seg-toggle.on .seg-knob { transform: translateX(18px); }

.fx-host { position: relative; overflow: hidden; }
.fx-host > *:not(.fx-decor) { position: relative; z-index: 1; }
.fx-decor {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  pointer-events: none;
  z-index: 0;
  /* Fade the scene out toward the card's content so text stays readable. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 55%);
  mask-image: linear-gradient(90deg, transparent, #000 55%);
}
.fx-decor svg { height: 100%; width: auto; display: block; }
.fx-row .fx-decor { width: 58px; opacity: 0.92; }
.fx-card .fx-decor { width: 132px; opacity: 0.95; }
.fx-swatch .fx-decor { width: 82px; opacity: 0.95; }
/* In-game backdrop: a fixed full-viewport layer behind the board, so the theme
   spans edge to edge (the play screen itself sits inside #root's padding). */
.fx-play-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
.fx-play-bg .fx-decor { height: 172px; width: 220px; opacity: 0.9; }
.fx-play-bg .fx-decor svg { margin-left: auto; }
/* Dark themes (Cosmos): lighten the otherwise-dark puzzle title. */
.fx-dark .bar-title { color: #efeaff; }

/* Full themed background for the popup shown when tapping a player's name. The
   background is painted on the card itself, so it always covers the whole
   scroll height (clipped to the rounded corners); the edge scene rides on top
   as a top-right banner. */
.modal-card.uc-themed { position: relative; }
.modal-card.uc-themed > *:not(.fx-decor) { position: relative; z-index: 1; }
.modal-card.uc-themed > .fx-decor { height: 132px; width: 168px; opacity: 0.95; }
.modal-card.uc-themed > .fx-decor svg { margin-left: auto; }
/* Frost the stat tiles so they read on the colored background. */
.modal-card.uc-themed .stat-box { background: rgba(255, 255, 255, 0.62); }

/* User-card snoovatar in the modal header. */
.uc-avatar { width: 30px; height: 30px; font-size: 14px; }

/* User-card "recent challenges" list. */
.uc-recent { margin-top: 14px; }
.uc-recent-head {
  font-size: 12px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin-bottom: 6px;
}
.uc-ch {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 10px 8px;
  border-radius: 12px;
  background: var(--bg);
  transition: background 0.12s ease;
}
.modal-card.uc-themed .uc-ch { background: rgba(255, 255, 255, 0.62); }
.uc-ch + .uc-ch { margin-top: 6px; }
.uc-ch:active { transform: scale(0.99); }
.uc-ch-title {
  font-weight: 700;
  font-size: 13.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.uc-ch-meta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  flex: none;
}
.uc-ch-meta .ms { font-size: 14px; }
.uc-ch-meta .ms-play-arrow { margin-left: 5px; }

/* Dark profile themes (e.g. Cosmos): the background is a dark surface, so flip
   text and tiles to light for legibility. */
.fx-dark .row-title,
.fx-dark .swatch-name { color: #efeaff; }
.fx-dark .row-sub { color: rgba(239, 234, 255, 0.7); }

.modal-card.uc-dark .card-title { color: #efeaff; }
.modal-card.uc-dark .row-sub,
.modal-card.uc-dark .uc-standing { color: rgba(239, 234, 255, 0.75); }
.modal-card.uc-dark .modal-head .ms { color: #b9a8ff; }
.modal-card.uc-dark .stat-box,
.modal-card.uc-dark .uc-ch { background: rgba(255, 255, 255, 0.1); }
.modal-card.uc-dark .stat-value,
.modal-card.uc-dark .uc-ch-title { color: #efeaff; }
.modal-card.uc-dark .stat-label,
.modal-card.uc-dark .uc-recent-head { color: rgba(239, 234, 255, 0.6); }
.modal-card.uc-dark .stat-box .ms,
.modal-card.uc-dark .uc-ch-meta { color: rgba(239, 234, 255, 0.7); }
/* Ghost buttons are a white-faced button on any background, so keep their dark
   ink even inside a dark themed card — light text here lands on white and
   vanishes (e.g. the user card on the Cosmos theme). */
.modal-card.uc-dark .btn.ghost {
  color: var(--ink);
}

.shop-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}

.shop-item {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  border: 2px solid transparent;
  padding: 12px;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 4px;
  transition: transform 0.12s ease;
}

.shop-item:active { transform: scale(0.97); }
.shop-item.equipped { border-color: var(--ink); }

.shop-name { font-weight: 800; font-size: 14px; }

.shop-price,
.shop-state {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 800;
  font-size: 13px;
}

.shop-price { color: var(--bolt); }
.shop-state { color: var(--good); }

.swatch {
  height: 56px;
  border-radius: 12px;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-bottom: 6px;
}

.swatch-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }

.swatch-string {
  width: 86%;
  height: auto;
  display: block;
}

.swatch-buddy { width: 48px; height: 48px; }
.swatch-buddy .bd-svg { width: 100%; height: 100%; overflow: visible; display: block; }

/* --- in-game buddy ------------------------------------------------------ */
.buddy {
  position: relative;
  z-index: 2;
  align-self: flex-start;
  display: flex;
  align-items: flex-end;
  height: 50px;
  /* Negative bottom margin (beats the board-bar's 8px gap) so the buddy drops
   * down to overlap the top of the letter canvas below it. The deeper it sits,
   * the shorter the control bar, leaving more room for a bigger board. */
  margin: 2px 0 -36px 4px;
}
.bd-char { width: 46px; height: 46px; flex: none; }
.bd-svg { width: 100%; height: 100%; overflow: visible; display: block; }

/* Idle life: a breathing squash-stretch bob, periodic blink, both arms moving,
 * tapping feet, and an occasional happy double-hop — so the buddy always feels
 * alive. The host (.bd-char) carries the hop so it composes with the rig's bob;
 * reactions override the hop with their own animation. */
.bd-rig { transform-box: fill-box; transform-origin: 50% 92%; animation: bd-bob 2.6s ease-in-out infinite; }
@keyframes bd-bob {
  0%, 100% { transform: translateY(0) scale(1, 1); }
  50% { transform: translateY(-2.5px) scale(0.97, 1.05); }
}
.bd-eye { transform-box: fill-box; transform-origin: center; animation: bd-blink 4.2s infinite; }
@keyframes bd-blink { 0%, 91%, 100% { transform: scaleY(1); } 94% { transform: scaleY(0.12); } 97% { transform: scaleY(1); } }
.bd-arm-r { transform-box: fill-box; transform-origin: 50% 18%; animation: bd-wave 3.6s ease-in-out infinite; }
@keyframes bd-wave { 0%, 72%, 100% { transform: rotate(0); } 80% { transform: rotate(-24deg); } 90% { transform: rotate(-8deg); } }
.bd-arm-l { transform-box: fill-box; transform-origin: 50% 18%; animation: bd-sway 2.9s ease-in-out infinite; }
@keyframes bd-sway { 0%, 100% { transform: rotate(0); } 50% { transform: rotate(11deg); } }
.bd-foot { transform-box: fill-box; transform-origin: 50% 100%; }
.bd-foot-l { animation: bd-tap 2.4s ease-in-out infinite; }
.bd-foot-r { animation: bd-tap 2.4s ease-in-out infinite 1.2s; }
@keyframes bd-tap { 0%, 68%, 100% { transform: translateY(0) scaleY(1); } 82% { transform: translateY(-2px) scaleY(0.8); } }
.bd-char { animation: bd-idle 5.5s ease-in-out infinite; }
@keyframes bd-idle {
  0%, 60%, 100% { transform: translateY(0); }
  70% { transform: translateY(-7px); }
  79% { transform: translateY(0); }
  86% { transform: translateY(-3px); }
  92% { transform: translateY(0); }
}

/* Bought animation packs (Closet) — retune the idle bob/hop. The `anim-*` class
 * sits on the in-game `.buddy` host AND on `.bd-preview` (closet cards/editor),
 * so the motion plays in both. They only touch idle motion, and the `:not(.is-*)`
 * guards keep in-game reactions (good/bad/win) winning over the idle override. */
.anim-bouncy .bd-rig { animation-duration: 1.5s; }
.anim-bouncy .bd-char { animation-duration: 3.4s; }
.anim-bouncy:not(.is-good):not(.is-bad):not(.is-win) .bd-char {
  animation-name: bd-bounce;
}
@keyframes bd-bounce {
  0%, 100% { transform: translateY(0) scale(1, 1); }
  40% { transform: translateY(-12px) scale(0.92, 1.1); }
  55% { transform: translateY(0) scale(1.08, 0.9); }
  70% { transform: translateY(-5px) scale(1, 1); }
}
.anim-chill .bd-rig { animation-duration: 4.2s; }
.anim-chill .bd-arm-l { animation-duration: 4.6s; }
.anim-chill .bd-arm-r { animation-duration: 5.2s; }
.anim-chill .bd-char { animation-duration: 9s; }
.anim-hyper .bd-rig { animation-duration: 0.95s; }
.anim-hyper .bd-arm-l { animation-duration: 0.7s; }
.anim-hyper .bd-arm-r { animation-duration: 0.8s; }
.anim-hyper .bd-foot-l,
.anim-hyper .bd-foot-r { animation-duration: 0.55s; }
.anim-hyper:not(.is-good):not(.is-bad):not(.is-win) .bd-char {
  animation: bd-jitter 0.5s ease-in-out infinite;
}
@keyframes bd-jitter {
  0%, 100% { transform: translateY(0) rotate(0); }
  25% { transform: translateY(-2px) rotate(-2deg); }
  50% { transform: translateY(0) rotate(0); }
  75% { transform: translateY(-2px) rotate(2deg); }
}
/* Float: hovers gently off the ground. */
.anim-float:not(.is-good):not(.is-bad):not(.is-win) .bd-char {
  animation: bd-float 3s ease-in-out infinite;
}
@keyframes bd-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-7px); }
}
/* Spin: a full twirl every few seconds. */
.anim-spin:not(.is-good):not(.is-bad):not(.is-win) .bd-char {
  animation: bd-spin 4s ease-in-out infinite;
}
@keyframes bd-spin {
  0%, 55% { transform: rotate(0); }
  85%, 100% { transform: rotate(360deg); }
}
/* Wobble: rocks side to side, pivoting at the feet. */
.anim-wobble .bd-rig { animation: bd-wobble 1.6s ease-in-out infinite; }
@keyframes bd-wobble {
  0%, 100% { transform: rotate(-7deg); }
  50% { transform: rotate(7deg); }
}

/* Mouth moods. Buddies smile a little at rest and grin wider when you score or
 * win; all three mouths stack in place and crossfade so the change eases in
 * instead of popping. */
.bd-m { transform-box: fill-box; transform-origin: center; transition: opacity 0.25s ease, transform 0.25s ease; }
.bd-m-grin, .bd-m-frown { opacity: 0; }
/* Good/win: fade the resting smile out and the big grin in (grin grows a touch). */
.buddy.is-good .bd-m-rest, .buddy.is-win .bd-m-rest { opacity: 0; }
.buddy.is-good .bd-m-grin, .buddy.is-win .bd-m-grin { opacity: 1; transform: scale(1.08); }
/* Bad: swap the smile for a frown. */
.buddy.is-bad .bd-m-rest { opacity: 0; }
.buddy.is-bad .bd-m-frown { opacity: 1; }

/* Reactions ride on the host so they compose with the rig's idle bob, and
 * override the idle hop above. */
.buddy.is-good .bd-char { animation: bd-hop 0.6s cubic-bezier(0.3, 1.4, 0.5, 1); }
@keyframes bd-hop {
  0% { transform: translateY(0) scale(1, 1); }
  25% { transform: translateY(-13px) scale(0.93, 1.09); }
  55% { transform: translateY(0) scale(1.07, 0.9); }
  78% { transform: translateY(-4px) scale(1, 1); }
  100% { transform: translateY(0) scale(1, 1); }
}
.buddy.is-bad .bd-char { animation: bd-shake 0.5s ease; }
@keyframes bd-shake { 0%, 100% { transform: translateX(0) rotate(0); } 20% { transform: translateX(-4px) rotate(-7deg); } 60% { transform: translateX(4px) rotate(7deg); } 80% { transform: translateX(-2px) rotate(-3deg); } }
.buddy.is-win .bd-char { animation: bd-party 0.5s ease-in-out infinite; }
@keyframes bd-party {
  0%, 100% { transform: translateY(0) rotate(-11deg) scale(1, 1); }
  50% { transform: translateY(-14px) rotate(11deg) scale(1.05, 0.96); }
}
/* On a win the arms grab and fling confetti up and out, on a loop that the
 * confetti bursts are timed to (see react("win") in buddies.ts). */
.buddy.is-win .bd-arm-r { animation: bd-throw-r 1.1s ease-in-out infinite; }
@keyframes bd-throw-r {
  0%, 100% { transform: rotate(0); }
  18% { transform: rotate(28deg); }     /* grab/wind down */
  40% { transform: rotate(-120deg); }   /* fling up */
  62% { transform: rotate(-95deg); }
  82% { transform: rotate(-18deg); }
}
.buddy.is-win .bd-arm-l { animation: bd-throw-l 1.1s ease-in-out infinite; }
@keyframes bd-throw-l {
  0%, 100% { transform: rotate(0); }
  18% { transform: rotate(-28deg); }
  40% { transform: rotate(120deg); }
  62% { transform: rotate(95deg); }
  82% { transform: rotate(18deg); }
}

/* Speech bubble. */
.bd-bubble {
  position: absolute;
  left: 40px;
  bottom: 26px;
  max-width: 180px;
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 5px 9px;
  font-size: 11.5px;
  font-weight: 800;
  line-height: 1.2;
  white-space: nowrap;
  box-shadow: var(--shadow);
  opacity: 0;
  transform: translateY(4px) scale(0.95);
  transform-origin: left bottom;
  transition: opacity 0.15s ease, transform 0.15s ease;
  pointer-events: none;
  z-index: 2;
}
.bd-bubble.show { opacity: 1; transform: none; }
.bd-bubble::after {
  content: "";
  position: absolute;
  left: 12px;
  bottom: -5px;
  width: 9px;
  height: 9px;
  background: var(--card);
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  transform: rotate(45deg);
}

/* Win confetti — flung from the hands: arcs up to a peak, then tumbles down.
 * Origin (left/bottom) and the --dx/--peak/--fall/--dr vars are set per-piece. */
.bd-confetti {
  position: absolute;
  width: 8px;
  height: 12px;
  border-radius: 1.5px;
  opacity: 0;
  pointer-events: none;
  animation: bd-throw-conf 2.3s cubic-bezier(0.2, 0.55, 0.4, 1) forwards;
}
/* Drawn celebrations (hearts/stars) reuse the same throw physics — a small SVG
 * shape instead of a chip. */
.bd-confetti.shape {
  width: 17px;
  height: 17px;
  border-radius: 0;
  background: none;
}
.bd-confetti.shape svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* Fireworks — glowing sparks that fan out radially from a detonation point,
 * fade, and drift down (per-spark --fx/--fy aim, --g gravity). The flash marks
 * the pop. Both use the element's `color` for fill + glow. */
.bd-spark {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 7px currentColor, 0 0 3px currentColor;
  opacity: 0;
  pointer-events: none;
  animation: bd-firework 1.7s ease-out forwards;
}
@keyframes bd-firework {
  0% { opacity: 0; transform: translate(0, 0) scale(0.3); }
  10% { opacity: 1; transform: translate(calc(var(--fx) * 0.32), calc(var(--fy) * 0.32)) scale(1.1); }
  100% { opacity: 0; transform: translate(var(--fx), calc(var(--fy) + var(--g, 12px))) scale(0.5); }
}
.bd-spark-flash {
  position: absolute;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 11px currentColor;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -50%);
  animation: bd-flash 0.55s ease-out forwards;
}
@keyframes bd-flash {
  0% { opacity: 0.9; transform: translate(-50%, -50%) scale(0.3); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(2.6); }
}
@keyframes bd-throw-conf {
  0% { opacity: 1; transform: translate(0, 0) rotate(0); }
  30% { opacity: 1; transform: translate(calc(var(--dx, 0) * 0.55), var(--peak, -40px)) rotate(180deg); }
  100% { opacity: 0; transform: translate(var(--dx, 0), var(--fall, 30px)) rotate(var(--dr, 480deg)); }
}

.swatch-peg {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1.5px solid;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
  font-size: 14px;
}

/* Profile-theme swatch: a mini name card with the bundled buddy on the left,
   keeping the right edge clear for the theme's .fx-decor graphic. */
.swatch.fx-swatch { justify-content: flex-start; gap: 8px; padding: 0 12px; }
.swatch.fx-swatch .swatch-buddy { width: 46px; height: 46px; }
.swatch-name { font-weight: 800; font-size: 14px; }

/* Animation/reaction swatches: a single glyph or sample line on the bg tile. */
.swatch.swatch-glyph { gap: 0; }
.swatch-ic { font-size: 30px; color: var(--ink); }
.swatch-react {
  font-weight: 900;
  font-size: 15px;
  color: var(--ink);
  background: var(--card);
  border-radius: 999px;
  padding: 5px 12px;
  box-shadow: var(--shadow);
}

/* --- Buddy closet ------------------------------------------------------- */
.closet-list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-top: 12px;
}
.closet-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-align: center;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  border: 2px solid transparent;
  padding: 14px 8px;
}
.closet-card.equipped { border-color: var(--ink); }

/* Ghost "Make new buddy" card — a dashed tile that opens the editor. */
.closet-new {
  background: transparent;
  box-shadow: none;
  border: 2px dashed var(--line);
  color: var(--muted);
  cursor: pointer;
  justify-content: center;
}
.closet-new:hover { border-color: var(--accent); color: var(--accent); }
.closet-new .closet-name { color: inherit; }
.closet-new .closet-preview {
  background: transparent;
  color: inherit;
}
.closet-new .closet-preview .ms { font-size: 40px; }
.closet-preview {
  width: 88px;
  height: 88px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border-radius: 14px;
}

/* Animated buddy preview (closet cards, editor, animation tiles): the rig in a
 * `.bd-char` host so the idle hop + bought animation packs play. Fills its
 * parent box; the parent sets the actual display size. */
.bd-preview {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.bd-preview .bd-char { width: 86%; height: 86%; }
.bd-preview .bd-svg { width: 100%; height: 100%; overflow: visible; display: block; }
.closet-name {
  font-weight: 800;
  font-size: 15px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
/* Actions sit at the bottom and stay on one row — compact so Equip + edit +
 * delete fit across a narrow mobile card. */
.closet-card-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: nowrap;
  gap: 5px;
  margin-top: auto;
}
.closet-card-actions .btn.sm { padding: 6px 8px; font-size: 12px; }
/* Icon-only edit/delete: square and tight. */
.closet-card-actions .btn.sm.icon { padding: 6px; }
.closet-card-actions .btn.sm.icon .ms { font-size: 18px; }
/* Equipped state shown as a compact green check (the card's outline also marks
 * it), so the row stays on one line. */
.closet-equipped {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #1f9d57;
}
.closet-equipped .ms { font-size: 24px; }
.btn.sm { padding: 6px 12px; font-size: 13px; }

/* --- Buddy editor (modal) ----------------------------------------------- */
/* The editor is a modal that becomes a 2-column workspace on desktop. The
 * card itself doesn't scroll — its inner regions do — so the preview/name
 * column stays put (pinned on mobile, a fixed column on desktop). */
.modal-card.buddy-editor-card {
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.buddy-editor-card .modal-head { padding: 16px 16px 10px; margin-bottom: 0; }

.buddy-editor {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
  overflow-y: auto; /* mobile: the body scrolls; the left column sticks */
}

/* Left column: preview + name. Sticky at the top on mobile so the preview is
 * always visible while the cosmetics scroll underneath. */
.editor-left {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--card);
  padding: 8px 16px 12px;
  border-bottom: 1px solid var(--line);
}
.editor-right { padding: 6px 16px 16px; }

.editor-preview {
  width: 104px;
  height: 104px;
  margin: 0 auto 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border-radius: var(--radius);
}
.editor-field { margin: 0; }
.editor-field .field { margin: 0; }

/* The live preview buddy fills the box (the in-game `.buddy` positioning is for
 * the game bar, so it's neutralized here). Confetti can spill out the top. */
.editor-preview { position: relative; overflow: visible; }
.editor-preview .buddy {
  position: relative;
  margin: 0;
  width: 100%;
  height: 100%;
  align-self: auto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.editor-preview .bd-char { width: 78%; height: 78%; }
.editor-preview .bd-bubble {
  left: 50%;
  right: auto;
  bottom: auto;
  top: 2px;
  max-width: none;
  white-space: nowrap;
  transform: translate(-50%, -4px) scale(0.95);
  transform-origin: center bottom;
}
.editor-preview .bd-bubble.show { transform: translateX(-50%); }
.editor-preview .bd-bubble::after { left: 50%; margin-left: -4px; }

/* Test buttons: round, colored icon buttons that fire the buddy's reactions. */
.editor-test-wrap { margin-top: 8px; }
.editor-test-head {
  text-align: center;
  font-weight: 800;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin-bottom: 7px;
}
.editor-test {
  display: flex;
  justify-content: center;
  align-items: start;
  gap: 8px;
}
.editor-test-item {
  flex: 1 1 0;
  max-width: 86px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
}
.editor-test-label {
  font-size: 10.5px;
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  color: var(--muted);
}
.editor-test-btn {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: var(--bg);
  transition: transform 0.12s ease;
}
.editor-test-btn:active { transform: scale(0.9); }
.editor-test-btn .ms { font-size: 20px; }
.editor-test-btn.good { background: rgba(22, 163, 74, 0.14); }
.editor-test-btn.good .ms { color: var(--good); }
.editor-test-btn.bad { background: var(--accent-soft); }
.editor-test-btn.bad .ms { color: var(--accent); }
.editor-test-btn.win { background: var(--bolt-soft); }
.editor-test-btn.win .ms { color: var(--bolt); }

/* Desktop: a roomy, near-fullscreen two-column workspace. Left holds the
 * preview + name (always visible); the wider right column scrolls the
 * cosmetics. */
@media (min-width: 700px) {
  .modal-card.buddy-editor-card {
    width: 94vw;
    max-width: 960px;
    height: 88vh;
    max-height: 88vh;
  }
  .buddy-editor { flex-direction: row; overflow: hidden; }
  .editor-left {
    position: static;
    flex: 0 0 290px;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    border-bottom: none;
    border-right: 1px solid var(--line);
    padding: 20px 22px;
  }
  .editor-right {
    flex: 1;
    overflow-y: auto;
    padding: 18px 22px 22px;
  }
  .buddy-editor-card .editor-preview {
    width: 200px;
    height: 200px;
    margin: 8px auto 16px;
  }
}
.editor-section {
  font-weight: 800;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin: 12px 2px 0;
}
.editor-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  /* overflow-x:auto makes overflow-y clip too, so leave room up top for the
   * selected tile's corner checkmark badge (top:-7px). */
  padding: 8px 2px 4px;
  scrollbar-width: none;
  /* Soft-fade an overflowing edge instead of a hard cutoff. edgeFade() toggles
     fade-l / fade-r so only a side that actually scrolls fades. */
  --fade-l: 0px;
  --fade-r: 0px;
  -webkit-mask-image: linear-gradient(
    to right, transparent, #000 var(--fade-l),
    #000 calc(100% - var(--fade-r)), transparent);
  mask-image: linear-gradient(
    to right, transparent, #000 var(--fade-l),
    #000 calc(100% - var(--fade-r)), transparent);
}
.editor-row.fade-l { --fade-l: 24px; }
.editor-row.fade-r { --fade-r: 24px; }
.editor-row::-webkit-scrollbar { display: none; }
.editor-tile {
  flex: none;
  width: 76px;
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: 12px;
  padding: 6px 4px 7px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  position: relative;
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.editor-tile:active { transform: scale(0.95); }
.editor-tile.on { border-color: var(--ink); }
.editor-tile-pv {
  width: 52px;
  height: 52px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.editor-tile-pv .bd-svg { width: 100%; height: 100%; overflow: visible; }
.editor-tile-pv .ms { font-size: 26px; color: var(--muted); }
.editor-tile-ic { width: 26px; height: 26px; color: var(--muted); display: flex; }
.editor-tile-ic svg { width: 100%; height: 100%; display: block; }
.editor-tile-label {
  font-weight: 700;
  font-size: 11px;
  line-height: 1.15;
  text-align: center;
  color: var(--ink);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.editor-tile-price {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-weight: 800;
  font-size: 11px;
  color: var(--bolt);
}
.editor-tile-price .ms { font-size: 13px; }
.editor-tile-check {
  position: absolute;
  top: -7px;
  right: -7px;
  background: var(--ink);
  color: #fff;
  border-radius: 50%;
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.editor-tile-check .ms { font-size: 13px; color: #fff; }
.editor-save { margin-top: 16px; }

/* Theme preview overlay: a full-screen in-game mock (themed board with blank
 * pegs + the bundled buddy, over the theme's backdrop) with a floating action
 * bar. Sits above the app but below modals (z-50), so the buy confirm shows on
 * top; the bottom nav is hidden via the `immersive` body class while it's open. */
.tpv-overlay {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  overflow: hidden;
}
/* The preview reuses the in-game `.play-screen` layout (so the desktop two-column
 * and mobile stacked arrangements come for free); these just size it inside the
 * overlay and leave room for the floating bar. */
.tpv-play {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
  padding: 20px 16px 104px;
}
.tpv-play .play-board {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  min-height: 0;
}
/* The themed backdrop (decorateProfile) makes the overlay an `fx-host`, whose
 * `.fx-host > *` rule forces children to position:relative — so the tag/bar/play
 * selectors below are scoped under `.tpv-overlay` to out-specify it and stay put. */
.tpv-overlay .tpv-play { position: relative; z-index: 1; }
/* Floating "Preview" label pinned to the top. */
.tpv-overlay .tpv-tag {
  position: absolute;
  top: calc(14px + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  padding: 5px 14px;
  border-radius: 999px;
  background: var(--card);
  color: var(--muted);
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  box-shadow: var(--shadow);
}
.tpv-overlay .tpv-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(20px + env(safe-area-inset-bottom));
  z-index: 2;
  display: flex;
  justify-content: center;
  gap: 12px;
  padding: 0 16px;
}
.tpv-bar .btn { white-space: nowrap; }

/* --- results / modals --------------------------------------------------------------------------- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(16, 20, 26, 0.45);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 50;
  opacity: 0;
  transition: opacity 0.18s ease;
  padding: 12px;
}

.modal-backdrop.open { opacity: 1; }

.modal-card {
  background: var(--card);
  border-radius: 22px;
  padding: 18px;
  width: 100%;
  max-width: 480px;
  max-height: 86vh;
  overflow-y: auto;
  transform: translateY(24px);
  transition: transform 0.2s cubic-bezier(0.2, 1.2, 0.4, 1);
}

.modal-backdrop.open .modal-card { transform: none; }

.modal-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.modal-head .ms { color: var(--accent); font-size: 22px; }

/* The player's equipped buddy, perched at the end of the user-card head. */
.uc-buddy { width: 46px; height: 46px; flex: none; }
.uc-buddy .bd-svg { overflow: visible; }

.modal-actions {
  display: flex;
  gap: 8px;
  margin-top: 16px;
}

.result-score { text-align: center; padding: 6px 0 12px; }

.result-number {
  font-size: 54px;
  font-weight: 900;
  letter-spacing: -0.03em;
  line-height: 1;
}

.result-label {
  color: var(--muted);
  text-transform: uppercase;
  font-size: 12px;
  letter-spacing: 0.08em;
  margin-top: 4px;
}

/* --- replay screen (re-uses the live .play-screen chrome) ---------------- */
/* Subtitle under the puzzle title naming whose run this is. */
.replay-sub {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  margin-top: 1px;
}
.replay-sub .ms { font-size: 14px; }

/* The scrubber: a gray capsule timeline. Each captured word is a dark-gray
 * clip placed at its real moment in the solve; the active clip goes black; a
 * vertical capsule playhead carries the current timestamp above it. */
.replay-scrub {
  flex: none;
  padding: 24px 6px 2px;
  cursor: pointer; /* the whole bar is a seek target */
}
.replay-track {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: var(--line);
}
.replay-clip {
  position: absolute;
  top: 0;
  height: 100%;
  border-radius: 999px;
  background: var(--muted);
  opacity: 0.5;
  transition: background 0.15s ease, opacity 0.15s ease;
}
.replay-clip.on {
  background: var(--ink);
  opacity: 1;
}
.replay-head {
  position: absolute;
  top: 50%;
  width: 5px;
  height: 20px;
  border-radius: 999px;
  background: var(--ink);
  transform: translate(-50%, -50%);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  transition: left 0.08s linear;
}
.replay-time {
  position: absolute;
  bottom: calc(100% + 7px);
  left: 50%;
  transform: translateX(-50%);
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--card);
  box-shadow: var(--shadow);
  font-size: 11px;
  font-weight: 800;
  color: var(--ink);
  white-space: nowrap;
}

/* Hover preview: a gray vertical line + timestamp tracking the cursor. */
.replay-hover {
  position: absolute;
  top: 50%;
  width: 2px;
  height: 16px;
  border-radius: 999px;
  background: var(--muted);
  transform: translate(-50%, -50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s ease;
}
.replay-hover.on { opacity: 0.7; }
.replay-hover-time {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--muted);
  color: var(--card);
  font-size: 11px;
  font-weight: 800;
  white-space: nowrap;
}

/* Shrink #root's chrome padding for the replay: a little breathing room above
 * the top bar, and almost none below so the controls sit near the bottom edge.
 * The screen height matches this exactly, so nothing overflows into a scroll. */
#root:has(.replay-screen) {
  padding-top: max(20px, env(safe-area-inset-top, 0px));
  padding-bottom: max(8px, env(safe-area-inset-bottom, 0px));
}
.play-screen.replay-screen {
  height: calc(100vh - 28px);
  height: calc(
    100dvh - max(20px, env(safe-area-inset-top, 0px)) -
      max(8px, env(safe-area-inset-bottom, 0px))
  );
}

/* Transport controls: a frosted capsule under the scrubber. */
.replay-dock {
  flex: none;
  display: flex;
  flex-direction: column;
}
.replay-controls {
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin: 2px auto 0;
  padding: 4px;
  border-radius: 999px;
  background: var(--card); /* fallback if backdrop-filter/color-mix unsupported */
  background: color-mix(in srgb, var(--card) 62%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.5);
  backdrop-filter: blur(14px) saturate(1.5);
  box-shadow: var(--shadow);
  border: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
}
.replay-ctrl {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  color: var(--ink);
  transition: background 0.12s ease, transform 0.08s ease;
}
.replay-ctrl:active { transform: scale(0.9); }
.replay-ctrl .ms { font-size: 24px; }
.replay-ctrl svg { display: block; }
/* Emphasize the primary play/pause control. */
.replay-ctrl.play {
  background: var(--ink);
  color: var(--card);
}
.replay-ctrl.play .ms { font-size: 26px; }
.replay-speed {
  width: auto;
  min-width: 44px;
  padding: 0 12px;
  font-weight: 800;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}
@media (hover: hover) {
  .replay-ctrl:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
  .replay-ctrl.play:hover { background: var(--ink); opacity: 0.9; }
}

.lb-replay-btn {
  width: 100%;
  margin: 4px 0 12px;
}

/* "No thanks" decline under the share-replay consent button. */
/* The "+N bits" reward shown under the share-replay button. */
.replay-reward {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  margin-top: 8px;
  font-size: 13px;
  font-weight: 800;
  color: var(--bolt);
}
.replay-reward .ms { font-size: 15px; }

.replay-decline {
  text-align: center;
  margin-top: 10px;
}
.replay-decline-link {
  color: var(--muted);
  font-size: 13px;
  text-decoration: underline;
  padding: 4px;
}

/* --- daily-streak celebration (fullscreen takeover) ---------------------- */
.streak-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 24px;
  text-align: center;
  color: #fff5e6;
  background:
    radial-gradient(120% 80% at 50% 28%, #3a1d0a 0%, #1a0f06 55%, #0c0703 100%);
  opacity: 0;
  transition: opacity 0.14s ease;
}
.streak-overlay.in { opacity: 1; }

/* Stage: a fixed box holding the layered glow + spark + flame, so the flame
 * grows out of the spark's position. */
.streak-stage {
  position: relative;
  width: 240px;
  height: 180px;
}

/* A warm glow that lights up with the flame, then breathes behind it. */
.streak-glow {
  position: absolute;
  left: 50%;
  bottom: -36px;
  width: 420px;
  height: 360px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 140, 30, 0.5), transparent 65%);
  filter: blur(8px);
  transform: translateX(-50%) scale(0.3);
  opacity: 0;
}
.streak-overlay.lit .streak-glow {
  animation: streakGlowIn 220ms ease-out 60ms forwards,
    streakGlow 2.6s ease-in-out 300ms infinite;
}
@keyframes streakGlowIn {
  from { opacity: 0; transform: translateX(-50%) scale(0.3); }
  to { opacity: 0.7; transform: translateX(-50%) scale(1); }
}
@keyframes streakGlow {
  0%, 100% { opacity: 0.6; transform: translateX(-50%) scale(1); }
  50% { opacity: 0.9; transform: translateX(-50%) scale(1.07); }
}

/* The spark: a bright ember that pops in, flares, then is consumed by the flame. */
.streak-spark {
  position: absolute;
  bottom: 13%;
  left: 50%;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: radial-gradient(circle, #fffde7 0%, #ffd54f 45%, rgba(255, 140, 30, 0) 72%);
  transform: translate(-50%, 0) scale(0);
  opacity: 0;
}
/* Pre-ignition sparks: a scattered mix of dots + straight/curved licks rising
 * from the bottom vertex up through the flame, flickering during the hold, then
 * swept up the moment the fire ignites. (position, `--rot`, `--sz`, and timing
 * are all set inline per spark.) */
.streak-ember {
  position: absolute;
  width: var(--sz, 12px);
  height: var(--sz, 12px);
  color: #ffd27a;
  transform: translate(-50%, 0) rotate(var(--rot, 0deg)) scale(0.3);
  opacity: 0;
  transition: opacity 0.18s ease;
}
.streak-ember .streak-spk {
  width: 100%;
  height: 100%;
  display: block;
  filter: drop-shadow(0 0 3px rgba(255, 160, 40, 0.7));
}
.streak-overlay.in .streak-ember {
  animation-name: emberRise;
  animation-iteration-count: infinite;
  animation-timing-function: ease-out;
}
.streak-overlay.lit .streak-ember {
  animation-name: none;
  opacity: 0;
}
@keyframes emberRise {
  0% { opacity: 0; transform: translate(-50%, 5px) rotate(var(--rot, 0deg)) scale(0.3); }
  35% { opacity: 1; transform: translate(-50%, -2px) rotate(var(--rot, 0deg)) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -16px) rotate(var(--rot, 0deg)) scale(0.5); }
}

.streak-overlay.lit .streak-spark { animation: streakSpark 340ms ease-out forwards; }
@keyframes streakSpark {
  0% { transform: translate(-50%, 0) scale(0); opacity: 0; }
  18% { transform: translate(-50%, 0) scale(1); opacity: 1; }
  45% { transform: translate(-50%, -3px) scale(1.4); opacity: 1; }
  100% { transform: translate(-50%, -12px) scale(0.2); opacity: 0; }
}

/* The flame: hidden until ignition, then whooshes up out of the spark (timed to
 * the woosh in streak.wav, ~0.06–0.22s), then settles into its idle flicker. */
.streak-fire {
  position: absolute;
  inset: 0;
  transform-origin: 50% 100%;
  transform: scaleX(0.16) scaleY(0.04);
  opacity: 0;
  filter: drop-shadow(0 0 18px rgba(255, 120, 20, 0.65));
}
.streak-overlay.lit .streak-fire {
  animation: streakIgnite 220ms cubic-bezier(0.2, 1.5, 0.35, 1) 60ms forwards;
}
@keyframes streakIgnite {
  0% { transform: scaleX(0.16) scaleY(0.04); opacity: 0; }
  22% { opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.streak-fire svg { width: 100%; height: 100%; display: block; }
.flame-outer { transform-origin: 50% 100%; animation: flameFlicker 0.42s ease-in-out infinite alternate; }
.flame-inner { transform-origin: 50% 100%; animation: flamePulse 0.62s ease-in-out infinite alternate; }
@keyframes flameFlicker {
  0% { transform: scaleY(1) scaleX(1) skewX(0deg); }
  100% { transform: scaleY(1.06) scaleX(0.96) skewX(2.5deg); }
}
@keyframes flamePulse {
  0% { transform: scaleY(0.92) translateY(2px); opacity: 0.9; }
  100% { transform: scaleY(1.05) translateY(0); opacity: 1; }
}

.streak-count {
  margin-top: 14px;
  display: flex;
  justify-content: center; /* stays centred; grows symmetrically when a digit appears */
  font-weight: 900;
  font-size: 84px;
  line-height: 1;
  letter-spacing: -0.02em;
  color: #fff;
}
.streak-club {
  margin-top: 18px;
  font-weight: 800;
  font-size: 17px;
  color: #fff5e6;
}
.streak-continue {
  margin-top: 28px;
  min-width: 180px;
}
/* "Streak freeze used" note under the counter on the celebration overlay. */
.streak-freeze-note {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 12px;
  font-weight: 700;
  font-size: 14px;
  color: #a9e6f5;
}
.streak-freeze-note .svg-ic svg { width: 18px; height: 18px; }

/* Streak loss: the flame starts alive, then dies down while the count rolls to
 * 0 and the repair offer fades in. Reuses the celebration overlay + stage. */
.streak-overlay.loss {
  background:
    radial-gradient(120% 80% at 50% 28%, #34200e 0%, #170d05 55%, #0b0704 100%);
  transition: background 1.4s ease, opacity 0.14s ease;
}
.streak-overlay.loss.dying {
  background:
    radial-gradient(120% 80% at 50% 28%, #1c1d22 0%, #101013 60%, #08080a 100%);
}
/* Flame + glow are lit from the start (no ignition), unlike the celebration. */
.streak-overlay.loss .streak-fire { opacity: 1; transform: scale(1); }
.streak-overlay.loss .streak-glow {
  opacity: 0.7;
  transform: translateX(-50%) scale(1);
}
.streak-overlay.loss.dying .streak-fire {
  animation: streakDie 1400ms ease-in forwards;
}
@keyframes streakDie {
  0% { opacity: 1; transform: scale(1); }
  70% { opacity: 0.5; transform: scaleX(0.6) scaleY(0.5) translateY(8px); }
  100% { opacity: 0; transform: scaleX(0.18) scaleY(0.05) translateY(14px); }
}
.streak-overlay.loss.dying .streak-glow {
  animation: streakGlowOut 1400ms ease-in forwards;
}
@keyframes streakGlowOut {
  from { opacity: 0.7; transform: translateX(-50%) scale(1); }
  to { opacity: 0; transform: translateX(-50%) scale(0.4); }
}
.streak-overlay.loss.dying .streak-count {
  color: #9aa2ad;
  transition: color 1.4s ease;
}
/* Repair / no-thanks buttons, revealed once the flame has died. */
.streak-loss-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-top: 26px;
  width: 100%;
  max-width: 300px;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.streak-loss-actions.show {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.streak-loss-actions .btn { width: 100%; }

/* Repair success: from a near-black screen the dead flame sparks back and grows
 * enormous. The flame itself — with its swelling red heat-glow — is the wash
 * that floods the whole screen red. No separate erupting disc. */
.streak-overlay.repairing {
  background:
    radial-gradient(120% 90% at 50% 60%, #1c0a05 0%, #0a0402 60%, #000 100%);
}
.streak-overlay.repairing .streak-count,
.streak-overlay.repairing .streak-club,
.streak-overlay.repairing .streak-loss-actions {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.streak-overlay.repairing .streak-spark {
  animation: streakSpark 360ms ease-out forwards;
}
/* The heat-glow swells red to fill the whole screen as the flame erupts. */
.streak-overlay.repairing .streak-glow {
  background:
    radial-gradient(circle, rgba(255, 45, 0, 0.95) 0%, rgba(205, 20, 0, 0.6) 42%, transparent 78%);
  animation: streakRepairGlow 1400ms ease-out 160ms both;
}
@keyframes streakRepairGlow {
  0% { opacity: 0; transform: translateX(-50%) scale(0.3); }
  28% { opacity: 0.9; transform: translateX(-50%) scale(1.4); }
  100% { opacity: 1; transform: translateX(-50%) scale(11); }
}
/* The flame reignites from the spark and grows giant. */
.streak-overlay.repairing .streak-fire {
  animation: streakRepairFlame 1400ms cubic-bezier(0.42, 0, 0.6, 1) 180ms both;
}
@keyframes streakRepairFlame {
  0% { opacity: 0; transform: scale(0.08); }
  14% { opacity: 1; transform: scale(0.32); }
  42% { transform: scale(2.2); }
  100% { opacity: 1; transform: scale(26); }
}
/* The restored streak, rolled up from 0 over the red flame, then popped in. */
.streak-restored {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: #fff;
  text-shadow: 0 3px 22px rgba(120, 0, 0, 0.55);
  opacity: 0;
  transform: scale(0.55);
  pointer-events: none;
  transition: opacity 0.35s ease, transform 0.5s cubic-bezier(0.2, 1.5, 0.4, 1);
}
.streak-restored.show { opacity: 1; transform: scale(1); }
.streak-restored .odo {
  font-weight: 900;
  font-size: 104px;
  line-height: 1;
  letter-spacing: -0.02em;
}
.streak-restored-label {
  font-weight: 800;
  font-size: 22px;
}

/* Streak shop tab: a 2-column grid of vertical cards, like the buddy closet. */
.streak-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-top: 12px;
}
.streak-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
  padding: 16px 12px;
}
.streak-item-ic {
  width: 52px;
  height: 52px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 14px;
  background: rgba(57, 199, 224, 0.16);
  color: #2ba7c4;
}
.streak-item-ic .svg-ic svg { width: 30px; height: 30px; }
.streak-card-name { font-weight: 800; font-size: 15px; }
.streak-card-desc {
  font-size: 12px;
  line-height: 1.35;
  color: var(--muted);
  flex: 1;
}
.streak-buy { width: 100%; margin-top: 2px; }
.streak-owned {
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
}

/* Reward screen shown after the streak screen: Bits earned + encouragement. */
.streak-reward {
  display: flex;
  flex-direction: column;
  align-items: center;
  animation: streakRewardIn 0.4s cubic-bezier(0.2, 1.2, 0.3, 1) both;
}
.streak-reward-amount {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 900;
  font-size: 76px;
  line-height: 1;
  color: #ffd54f;
}
.streak-reward-amount .ms {
  font-size: 60px;
  color: #ffd54f;
}
.streak-reward-text { margin-top: 16px; }
@keyframes streakRewardIn {
  0% { opacity: 0; transform: translateY(16px) scale(0.9); }
  100% { opacity: 1; transform: none; }
}

/* Odometer: each digit cell clips a stacked old/new pair that slides. */
.odo {
  display: inline-flex;
  font-variant-numeric: tabular-nums;
}
.odo-digit {
  position: relative;
  display: inline-block;
  height: 1em;
  width: 0.62em;
  overflow: hidden;
  transition: width 0.45s cubic-bezier(0.3, 0.9, 0.3, 1);
}
/* A digit that didn't exist in the previous value (the number grew, e.g.
 * 999→1000) starts collapsed so the value reads centred at its old width, then
 * expands when its turn comes — shifting the other digits aside. */
.odo-digit.isnew { width: 0; }
.odo-digit.isnew.reveal { width: 0.62em; }
.odo-old,
.odo-new {
  display: block;
  text-align: center;
}
.odo-new {
  position: absolute;
  inset: 0;
  transform: translateY(-100%); /* waiting above the window */
}
/* On roll: old digit drops out the bottom, new slides down into place. */
.odo-digit.roll .odo-old {
  transform: translateY(100%);
  transition: transform 0.5s cubic-bezier(0.3, 0.9, 0.3, 1);
}
.odo-digit.roll .odo-new {
  transform: translateY(0);
  transition: transform 0.5s cubic-bezier(0.3, 0.9, 0.3, 1);
}

/* On desktop the in-game layout becomes a two-column grid (board | sidebar);
 * without an explicit area the scrubber would drop into just the board column.
 * Give it a full-width bottom row so it spans the screen, with margin. */
@media (min-width: 700px) {
  .play-screen.replay-screen {
    grid-template-areas:
      "top top"
      "board bar"
      "dock dock";
    grid-template-rows: auto 1fr auto;
  }
  .replay-screen .replay-dock {
    grid-area: dock;
    margin: 0 8px 2px;
  }
}

.result-facts {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 12px;
}

.fact {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 6px;
  border-radius: 10px;
}

.fact:nth-child(odd) { background: var(--bg); }
.fact .ms { color: var(--muted); }

.bits-lines {
  background: var(--bolt-soft);
  border-radius: 14px;
  padding: 10px 12px;
  margin-bottom: 12px;
}

.bits-line {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 14px;
}

.bits-line.total {
  border-top: 1px solid rgba(217, 154, 38, 0.25);
  margin-top: 4px;
  padding-top: 8px;
  font-weight: 800;
}

.bits-amount {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--bolt);
  font-weight: 800;
}

.banner {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700;
  font-size: 13.5px;
  border-radius: 12px;
  padding: 10px 12px;
  margin-bottom: 12px;
}

.share-text {
  width: 100%;
  border: 1.5px solid var(--line);
  border-radius: 12px;
  font-family: var(--font);
  font-size: 14.5px;
  color: var(--ink);
  padding: 12px;
  resize: vertical;
  outline: none;
  margin-top: 8px;
}

.share-text:focus { border-color: var(--ink); }

/* Read-only comment preview (stats are auto-generated, not editable). */
.share-preview {
  width: 100%;
  box-sizing: border-box;
  margin: 8px 0 0;
  padding: 12px;
  border-radius: 12px;
  background: var(--bg);
  font-family: var(--font);
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--ink);
  white-space: pre-wrap;
  word-break: break-word;
}

/* --- misc ------------------------------------------------------------------------------------------- */

.empty {
  text-align: center;
  color: var(--muted);
  padding: 28px 16px;
  font-size: 14px;
}

.boot-error {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 60px 20px;
  text-align: center;
  color: var(--muted);
}

.boot-error .ms { font-size: 30px; color: var(--accent); }

#toast {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translate(-50%, 16px);
  background: var(--ink);
  color: #fff;
  font-weight: 700;
  font-size: 14px;
  border-radius: 999px;
  padding: 11px 20px;
  box-shadow: 0 10px 28px rgba(16, 24, 40, 0.3);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
  z-index: 99;
  max-width: 86vw;
  text-align: center;
}

#toast.show { opacity: 1; transform: translate(-50%, 0); }

/* Toast with an action: two separate capsules. The container and message stay
   pointer-events:none so only the action button captures taps (no clicking
   anything behind it); taps elsewhere fall through to the board. */
#toast.has-action {
  background: none;
  box-shadow: none;
  padding: 0;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 92vw;
}
.toast-msg {
  background: var(--ink);
  color: #fff;
  font-weight: 700;
  font-size: 14px;
  border-radius: 999px;
  padding: 11px 18px;
  box-shadow: 0 10px 28px rgba(16, 24, 40, 0.3);
}
.toast-action {
  pointer-events: auto;
  flex: none;
  background: var(--accent);
  color: #fff;
  font-weight: 800;
  font-size: 14px;
  border-radius: 999px;
  padding: 11px 18px;
  box-shadow: 0 10px 28px rgba(226, 73, 59, 0.32);
}
.toast-action:active { transform: scale(0.95); }

/* On phones, let toasts size to their content but allow them to grow to nearly
   the full screen width, so longer messages read on one or two lines instead of
   squishing into a narrow, many-line pill. */
@media (max-width: 480px) {
  #toast,
  #toast.has-action {
    max-width: calc(100vw - 24px);
  }
}

/* Loading indicator: the splash wordmark (lettered pegs strung in order), with
   the pegs gently bobbing out of sync. */
.loader {
  display: flex;
  justify-content: center;
  width: 100%;
  margin: 28px auto;
}

.loader.big { margin: 38vh auto; }

.loader-svg {
  /* Scale with the screen: small on phones, larger on bigger viewports. */
  height: clamp(44px, 8vw, 70px);
  width: 100%;
  max-width: 460px;
  overflow: visible;
}

/* The full-screen app loader scales a bit larger still. */
.loader.big .loader-svg {
  height: clamp(54px, 11vw, 96px);
  max-width: 540px;
}

.loader-peg {
  animation: loader-bob 1.4s ease-in-out infinite;
}

.loader-peg circle {
  fill: #fff;
  stroke: var(--ink);
  stroke-width: 2.5;
}

.loader-peg text {
  font-family: inherit;
  font-weight: 800;
  font-size: 18px;
  fill: var(--ink);
  text-anchor: middle;
  dominant-baseline: central;
}

.loader-thread {
  fill: none;
  stroke: var(--accent);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 1px 1px rgba(226, 73, 59, 0.3));
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: loader-thread 3.8s ease-in-out infinite;
}

@keyframes loader-thread {
  0% { stroke-dashoffset: 100; opacity: 1; }
  46% { stroke-dashoffset: 0; }
  88% { stroke-dashoffset: 0; opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 0; }
}

@keyframes loader-bob {
  0%, 100% { transform: translateY(3px); }
  50% { transform: translateY(-3px); }
}

@media (min-width: 480px) {
  body { font-size: 16px; }
  .stats-grid { grid-template-columns: repeat(4, 1fr); }
  .shop-grid { grid-template-columns: repeat(3, 1fr); }
  .closet-list { grid-template-columns: repeat(3, 1fr); }
  .streak-grid { grid-template-columns: repeat(3, 1fr); }
  .modal-backdrop { align-items: center; }
}

/* On desktop, let the play view use more width so the board can be larger. */
@media (min-width: 700px) {
  #root:has(.play-screen) { max-width: 860px; }
}

/* --- tutorial overlay --------------------------------------------------- */

.tut-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(16, 20, 26, 0.5);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.tut-backdrop.open { opacity: 1; }

.tut-card {
  width: 100%;
  max-width: 360px;
  background: var(--card);
  border-radius: 22px;
  box-shadow: var(--shadow);
  padding: 18px 18px 14px;
  transform: translateY(10px);
  transition: transform 0.2s ease;
}

.tut-backdrop.open .tut-card { transform: none; }

.tut-board {
  position: relative;
  width: 100%;
  max-width: 248px;
  margin: 0 auto 14px;
}

.tut-title {
  font-size: 19px;
  font-weight: 800;
  text-align: center;
  letter-spacing: -0.01em;
  margin-bottom: 6px;
}

.tut-body {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
  text-align: center;
  margin: 0 2px 14px;
  min-height: 66px;
}

.tut-dots {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-bottom: 14px;
}

.tut-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--line);
  transition: width 0.2s ease, background 0.2s ease;
}

.tut-dot.on { width: 18px; background: var(--accent); }

.tut-nav { display: flex; align-items: center; gap: 10px; }
.tut-back { flex: none; }

.tut-skip {
  width: 100%;
  margin-top: 8px;
  color: var(--muted);
  font-weight: 700;
}

/* Fake cursor for the builder tutorial demos. It sits inside the (relative)
 * board, so its left/top percentages map straight to board coordinates, and
 * shrinks toward its tip while "held down" (a click or drag). */
.tut-cursor {
  position: absolute;
  width: 26px;
  height: 26px;
  margin: -4px 0 0 -5px;
  pointer-events: none;
  transform-origin: 5px 4px;
  transition: transform 0.12s ease, opacity 0.2s ease;
  filter: drop-shadow(0 2px 3px rgba(16, 20, 26, 0.4));
  z-index: 8; /* above the .tut-picker overlay (z-6) so clicks show on it */
  will-change: left, top, transform;
}

.tut-cursor.down { transform: scale(0.7); }

/* The letter picker popping up over the demo board (same look as the builder's
 * real picker modal, scaled to the tutorial board). */
.tut-picker {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 16px;
  background: rgba(16, 20, 26, 0.34);
  opacity: 0;
  transition: opacity 0.16s ease;
}

.tut-picker.open { opacity: 1; }

.tut-picker-card {
  width: 100%;
  background: var(--card);
  border-radius: 16px;
  padding: 12px;
  box-shadow: var(--shadow);
  transform: translateY(8px) scale(0.95);
  transition: transform 0.2s cubic-bezier(0.2, 1.2, 0.4, 1);
}

.tut-picker.open .tut-picker-card { transform: none; }

.tut-picker .modal-head { margin-bottom: 8px; }
.tut-picker .modal-head .ms { font-size: 18px; }
.tut-picker .card-title { font-size: 14px; }
.tut-picker .letter-grid { gap: 5px; margin: 0; }
.tut-picker .letter-key { font-size: 13px; border-radius: 9px; }
.tut-picker .letter-key.on { background: var(--accent); color: #fff; }

/* --- Auth screen (web port) ------------------------------------------------ */
.auth-screen {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
}
.auth-card {
  width: 100%;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 26px 22px 22px;
}
.auth-brand {
  display: flex;
  align-items: center;
  gap: 10px;
}
.auth-brand h1 {
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin: 0;
}
.auth-logo {
  color: var(--accent);
  font-size: 30px;
}
.auth-tag {
  color: var(--muted);
  font-size: 14px;
  margin: -4px 0 8px;
  line-height: 1.4;
}
.auth-field { display: block; }
.auth-field .field { width: 100%; margin: 0; }
.auth-screen .btn.primary.big { width: 100%; margin-top: 4px; }
.auth-screen .btn.ghost { width: 100%; }
.auth-err {
  color: var(--accent);
  font-size: 13px;
  min-height: 16px;
  margin: 0;
}
.auth-note {
  color: var(--good);
  font-size: 13px;
  margin: 0;
  min-height: 16px;
}

/* --- Guest sign-up gate (web port) ----------------------------------------- */
.auth-screen { position: relative; }
.auth-back {
  position: absolute; top: 14px; left: 12px; z-index: 2;
}
.gate-screen { position: relative; }
.gate-wrap {
  position: relative;
  min-height: 68vh;
  overflow: hidden;
}
.gate-teaser {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 16px;
  filter: blur(3px);
  opacity: 0.45;
  pointer-events: none;
  user-select: none;
}
.gate-skel {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
}
.gate-skel-dot {
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--line); flex: none;
}
.gate-skel-bar {
  height: 12px; border-radius: 6px; background: var(--line); margin: 5px 0;
}
.gate-skel-bar.w70 { width: 70%; }
.gate-skel-bar.w40 { width: 40%; }
.gate-overlay {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 10px; text-align: center; padding: 28px 26px 184px;
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--bg) 25%, transparent) 0%,
    color-mix(in srgb, var(--bg) 70%, transparent) 34%,
    var(--bg) 62%
  );
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
}
.gate-lock {
  width: 62px; height: 62px; border-radius: 50%;
  background: var(--accent-soft, #fdeeec); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 4px;
}
.gate-lock .ms { font-size: 32px; }
.gate-title { font-size: 20px; font-weight: 800; letter-spacing: -0.01em; }
.gate-sub { color: var(--muted); font-size: 14px; line-height: 1.5; max-width: 300px; }
.gate-overlay .btn.primary.big { width: 100%; max-width: 300px; margin-top: 8px; }
.gate-overlay .btn.ghost { max-width: 300px; }

/* --- Email confirmation panel ---------------------------------------------- */
.auth-sent {
  display: flex; flex-direction: column; align-items: center;
  gap: 10px; text-align: center;
}
.auth-sent-ic {
  width: 60px; height: 60px; border-radius: 50%;
  background: var(--accent-soft, #fdeeec); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
}
.auth-sent-ic .ms { font-size: 30px; }
.auth-sent-title { font-size: 20px; font-weight: 800; letter-spacing: -0.01em; }
.auth-sent-msg { color: var(--muted); font-size: 14px; line-height: 1.5; margin: 0 0 6px; }
.auth-sent .btn.primary.big { width: 100%; }
.auth-sent .btn.ghost { width: 100%; }
