/* ==========================================================================
   PETAKUMPET — design tokens
   Palette rule: warm cream base, low-saturation pastel accents.
   Nothing above ~60% saturation, nothing pure-black. Kid-friendly but calm.
   ========================================================================== */
:root {
  --bg:        #fdf6ec;
  --bg-soft:   #f7ecdd;
  --surface:   #fffdf9;
  --ink:       #4a4038;
  --ink-soft:  #8a7c6d;
  --line:      #e6d9c6;

  /* ACCENTS NOW MATCH THE COVER ART, not the old pastels. The 33 covers are
     drawn in a papercut palette (terracotta / deep teal / mustard / sage) with
     heavy ink outlines; the chrome used to be a separate, much paler language,
     so the app read as a generic UI that happened to have nice pictures glued
     on. These are the cover colours, so card and chrome belong together. */
  --c1: #3f7d74;  /* deep teal   — wordsearch */
  --c2: #4a6fa5;  /* deep blue   — block slide */
  --c3: #d9683f;  /* terracotta  — maze  (the covers' own background) */
  --c4: #c8912f;  /* mustard     — pipes */
  --c5: #8d6bb0;  /* plum        — snap */
  --c6: #6f9451;  /* olive       — shikaku */
  --c7: #c96a86;  /* rose        — atoms */
  --c8: #3f8b93;  /* sea teal    — sudoku */

  --good:  #4f8f78;
  --warn:  #c9702f;
  --star:  #e6b33c;

  /* the ink line every cover is drawn with — reused as the UI's outline */
  --ink-line: #2f2a26;

  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 22px;

  /* HARD OFFSET SHADOWS, NOT SOFT BLUR. This is the single change that makes
     the interface read as cut paper rather than as a stock SaaS card: real
     stacked paper throws a crisp displaced edge, and a big soft blur is the
     giveaway of a default component library. Blur stays only as a faint
     ambient wash underneath so nothing looks pasted on. */
  --shadow:    3px 3px 0 var(--ink-line), 0 4px 10px rgba(60,44,32,.10);
  --shadow-lg: 5px 5px 0 var(--ink-line), 0 10px 22px rgba(60,44,32,.13);
  --border:    2.5px solid var(--ink-line);

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

/* SELF-HOSTED, NOT GOOGLE FONTS. This is an offline-first PWA: a webfont
   loaded from fonts.gstatic.com simply does not arrive when the child has no
   signal, and the app would silently fall back to the system font — which is
   exactly the bug this fixes. One variable file covers weights 400-900.

   Nunito is SIL Open Font License: free to self-host and ship commercially. */
@font-face {
  font-family: 'Nunito';
  font-style: normal;
  font-weight: 400 900;               /* variable axis, one file, all weights */
  font-display: swap;
  src: url('../assets/fonts/nunito-latin.woff2') format('woff2');
}

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

html, body {
  margin: 0; height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: "Nunito", "Quicksand", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  font-size: 16px;
  -webkit-user-select: none; user-select: none;
  touch-action: manipulation;

  /* PAPER GRAIN, as an inline SVG rather than an image file: it costs no extra
     request, cannot fail to load offline, and scales to any screen. Two soft
     colour washes sit under it so the cream is never a flat digital fill —
     flatness is the other half of why the old chrome looked machine-made. */
  background-image:
    radial-gradient(circle at 15% 0%, rgba(217,104,63,.07), transparent 45%),
    radial-gradient(circle at 85% 12%, rgba(63,125,116,.06), transparent 42%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.05'/%3E%3C/svg%3E");
  background-attachment: fixed;
}

/* ---------- screens ---------- */
.screen {
  position: fixed; inset: 0;
  display: none;
  flex-direction: column;
  max-width: 560px; margin: 0 auto;
  padding-top: var(--safe-t);
}
.screen.active { display: flex; }

.scroll-area {
  flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 0 18px calc(28px + var(--safe-b));
}

/* ---------- app bar ---------- */
.app-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px;
  min-height: 56px;
}
.app-bar-title {
  flex: 1;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-weight: 800; font-size: 18px; letter-spacing: .2px;
}
.app-bar-emoji { font-size: 20px; }
.app-bar-actions { display: flex; gap: 6px; }

.icon-btn {
  width: 42px; height: 42px; flex: none;
  border: var(--border); border-radius: 14px;
  background: var(--surface);
  color: var(--ink);
  font-size: 20px; font-weight: 800;
  box-shadow: var(--shadow);
  cursor: pointer;
  transition: transform .1s ease, box-shadow .1s ease;
}
/* Press = the paper is pushed down onto the page, so the offset shadow
   shrinks as the button moves into it. Scaling alone read as a generic
   web tap; this reads as something physical. */
.icon-btn:active {
  transform: translate(2px, 2px);
  box-shadow: 1px 1px 0 var(--ink-line);
}
.icon-btn.is-off { opacity: .45; }

/* owner-mode badge — tap to drop back to a real player's view */
.prof-btn { font-size: 22px; }
.admin-chip { white-space: nowrap; }
.admin-chip.off { opacity: .4; filter: grayscale(1); }
.admin-chip {
  align-self: center;
  padding: 7px 11px; border: none; border-radius: 999px;
  background: var(--c4); color: #fff;
  font: inherit; font-weight: 800; font-size: 11px; letter-spacing: .8px;
  box-shadow: var(--shadow); cursor: pointer;
}
.admin-chip:active { transform: scale(.92); }

/* ---------- buttons ---------- */
.btn {
  border: none; border-radius: var(--r-md);
  padding: 15px 22px;
  font: inherit; font-weight: 800; font-size: 17px;
  cursor: pointer;
  transition: transform .12s ease, filter .12s ease;
}
.btn:active { transform: translateY(2px); }
.btn-block { display: block; width: 100%; margin-top: 10px; }
/* Any explicit `display` outranks the UA stylesheet's [hidden]{display:none},
   so el.hidden = true silently does nothing on these. Restore it globally
   rather than per-component — every future .btn-block would hit this too. */
[hidden] { display: none !important; }
.btn-primary {
  background: var(--c1); color: #fff;
  box-shadow: 0 4px 0 #6a9c90;
}
.btn-primary:active { box-shadow: 0 2px 0 #6a9c90; }
.btn-ghost {
  background: transparent; color: var(--ink-soft);
  font-size: 15px; padding: 11px;
}

/* ---------- gate ---------- */
#screen-gate { justify-content: center; padding: 24px; }
.gate-box { text-align: center; }
.gate-back { position: absolute; top: calc(14px + var(--safe-t)); left: 14px; z-index: 3; }
.gate-note a { color: var(--c1); font-weight: 800; text-decoration: underline; }
.gate-logo { font-size: 64px; }
.gate-title { font-size: 34px; margin: 6px 0 4px; letter-spacing: -.5px; line-height: 1.02; }
.gate-sub { color: var(--ink-soft); margin: 0 0 26px; line-height: 1.5; }
.gate-input {
  width: 100%; padding: 16px;
  border: 2px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: 22px; font-weight: 800;
  text-align: center; letter-spacing: 2px;
  -webkit-user-select: text; user-select: text;
}
.gate-input:focus { outline: none; border-color: var(--c1); }
.gate-error {
  color: #c4796a; font-weight: 700; font-size: 14px; margin: 12px 0 0;
}

/* ---------- feedback ---------- */
.fb-note {
  font-size: 12.5px; color: var(--ink-soft); text-align: center;
  line-height: 1.5; margin: 4px 0 16px;
}
.fb-type-row { display: flex; gap: 8px; margin-bottom: 12px; }
.fb-type {
  flex: 1; padding: 10px; border: var(--border); border-radius: var(--r-md);
  background: var(--surface); font: inherit; font-weight: 800; font-size: 13px;
  color: var(--ink-soft); cursor: pointer;
}
.fb-type.on { background: var(--c1); color: #fff; border-color: var(--c1); }
.fb-text {
  width: 100%; padding: 14px; border: 2px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink); font: inherit; font-size: 15px;
  resize: vertical; min-height: 90px; -webkit-user-select: text; user-select: text;
}
.fb-text:focus { outline: none; border-color: var(--c1); }
.fb-sent {
  color: var(--good); font-weight: 800; font-size: 15px; text-align: center; margin: 14px 0;
}
.gate-lang { display: flex; gap: 10px; margin-top: 28px; }
.lang-btn {
  flex: 1; padding: 11px; border: 2px solid var(--line); border-radius: var(--r-md);
  background: transparent; color: var(--ink-soft); font: inherit; font-weight: 700;
  cursor: pointer;
}
.lang-btn.active { border-color: var(--c1); background: var(--surface); color: var(--ink); }
.gate-note { color: var(--ink-soft); font-size: 12px; margin-top: 22px; line-height: 1.5; }

/* ---------- home ---------- */
/* No bottom padding: the gap under the stat row is owned by .daily-card's
   own top margin, so the space above and below that row is the same 16px. */
.hero { padding: 4px 0 0; }
/* 900 + tight tracking: Nunito's heaviest weight is the whole reason for
   shipping the font, so the one line the child reads first should use it. */
.hero-title { font-size: 28px; margin: 0 0 4px; font-weight: 900; letter-spacing: -.5px; }
.hero-sub { color: var(--ink-soft); margin: 0; font-weight: 600; }
.hero-stats { display: flex; gap: 12px; margin-top: 16px; }
/* Shorter than it was. These three boxes are a glance, not a headline, and at
   the old size they pushed the daily card — the thing people actually came to
   tap — most of the way off a phone screen. */
.stat {
  flex: 1; background: var(--surface);
  border: var(--border); border-radius: var(--r-md);
  padding: 9px 8px; text-align: center; box-shadow: var(--shadow);
}
.stat b { display: block; font-size: 21px; font-weight: 900; line-height: 1.15; }
.stat span {
  font-size: 11px; color: var(--ink-soft); font-weight: 800;
  letter-spacing: .2px; line-height: 1.2; display: block;
}
.stat-btn { font: inherit; color: inherit; cursor: pointer; }
.stat-btn:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink-line); }

/* ---------- Koleksi (badges) ---------- */
.bdg-note {
  font-size: 12.5px; color: var(--ink-soft); text-align: center;
  line-height: 1.5; margin: 4px 0 18px;
}
.badge-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
}
.badge-card {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  background: var(--surface); border: var(--border); border-radius: var(--r-md);
  box-shadow: var(--shadow); padding: 14px 8px; text-align: center;
}
.badge-card .bdg-icon { font-size: 32px; line-height: 1; }
.badge-card .bdg-name { font-size: 11.5px; font-weight: 800; line-height: 1.3; }
.badge-card .bdg-desc { font-size: 10px; color: var(--ink-soft); line-height: 1.3; }
.badge-card.locked { opacity: .38; filter: grayscale(1); }
.badge-card.locked .bdg-icon::after { content: '🔒'; display: block; font-size: 14px; margin-top: 2px; }

/* ---------- Level Otak bar ---------- */
.level-bar {
  display: flex; align-items: center; gap: 12px;
  width: 100%; margin-top: 16px;
  padding: 10px 14px;
  background: var(--surface);
  border: var(--border); border-radius: var(--r-md);
  box-shadow: var(--shadow);
  font: inherit; color: var(--ink); text-align: left; cursor: pointer;
  transition: transform .1s ease, box-shadow .1s ease;
}
.level-bar:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink-line); }

/* The ring is the rank. It has to be legible at avatar size, where a written
   tier name would be a grey smudge — so tier reads as colour first. */
.lv-face {
  flex: none; width: 46px; height: 46px; border-radius: 50%;
  background: var(--bg-soft);
  border: 3px solid var(--ring, var(--line));
  display: grid; place-items: center; overflow: hidden;
}
.lv-face img { width: 92%; height: 92%; object-fit: contain; display: block; }
.level-bar.glow .lv-face { box-shadow: 0 0 0 3px rgba(233,193,90,.35); }

.lv-main { flex: 1; min-width: 0; }
.lv-top { display: flex; align-items: baseline; gap: 8px; }
.lv-top b {
  font-size: 15px; font-weight: 900;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lv-top span {
  font-size: 11px; font-weight: 800; letter-spacing: .3px;
  color: var(--ink-soft); text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lv-track {
  height: 8px; margin: 5px 0 3px;
  background: var(--bg-soft);
  border: 1.5px solid var(--ink-line); border-radius: 999px; overflow: hidden;
}
.lv-track i { display: block; height: 100%; background: var(--ring, var(--c1)); }
.lv-sub { font-size: 11px; font-weight: 700; color: var(--ink-soft); }

/* ---------- daily puzzle card ---------- */
.daily-card {
  display: flex; align-items: center; gap: 10px;
  width: 100%; margin: 16px 0 2px;
  padding: 14px 16px; min-height: 76px;
  border: var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  font: inherit; color: #fff; text-align: left; cursor: pointer;
  position: relative; overflow: hidden;
  transition: transform .1s ease, box-shadow .1s ease;
}
.daily-card:active { transform: translate(3px, 3px); box-shadow: 2px 2px 0 var(--ink-line); }
.daily-card.has-cover { background-size: cover; background-position: center 20%; }
.daily-card.has-cover::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to right,
    rgba(20,16,12,.80) 0%, rgba(20,16,12,.58) 55%, rgba(20,16,12,.20) 100%);
}
.daily-card > * { position: relative; z-index: 1; }
.daily-card.done { filter: saturate(.7); }

.dc-body { flex: 1; min-width: 0; }
.dc-kicker { font-size: 10.5px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase; opacity: .9; }
.dc-game { font-size: 17px; font-weight: 900; margin-top: 1px; }
.dc-meta { font-size: 12px; font-weight: 700; opacity: .85; }
.dc-side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.dc-streak { font-size: 14px; font-weight: 900; }
.dc-cta {
  background: rgba(255,255,255,.22); border: 1.5px solid rgba(255,255,255,.5);
  border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 800;
  white-space: nowrap;
}

/* ---------- XP line in the win card ---------- */
.win-xp { margin: 6px 0 2px; font-size: 15px; font-weight: 800; color: var(--ink); }
.win-xp .wx-tag {
  font-size: 11px; font-weight: 800; color: var(--ink-soft);
  background: var(--bg-soft); border-radius: 999px; padding: 2px 8px; margin-left: 4px;
}
.win-xp .wx-up { margin-top: 4px; font-size: 13px; font-weight: 900; color: var(--good); }
.win-xp .wx-badge {
  margin-top: 4px; font-size: 13px; font-weight: 800; color: var(--c4, var(--c1));
}

.xp-rows { display: flex; flex-direction: column; gap: 12px; margin: 4px 0 14px; text-align: left; }
.xp-row { display: flex; gap: 10px; align-items: flex-start; }
.xp-row span { font-size: 20px; flex: none; }
.xp-row b { display: block; font-size: 14px; }
.xp-row em { display: block; font-style: normal; font-size: 12.5px; color: var(--ink-soft); line-height: 1.45; }
.xp-note { font-size: 12px; color: var(--ink-soft); line-height: 1.5; margin: 0 0 14px; text-align: left; }

/* ---------- Otong reactions ---------- */
/* Fixed, pointer-events:none and above the modal: he reacts ON TOP of the win
   card, and must never be able to swallow the Next button underneath him. */
.otong {
  position: fixed; left: 50%; bottom: 12px;
  width: 116px; height: 116px;
  transform: translateX(-50%) translateY(24px);
  opacity: 0; pointer-events: none; z-index: 58;
  transition: opacity .18s ease, transform .18s ease;
}
.otong.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.otong.pop { animation: otong-pop .5s cubic-bezier(.34,1.56,.64,1); }
@keyframes otong-pop {
  0%   { transform: translateX(-50%) translateY(30px) scale(.7); }
  60%  { transform: translateX(-50%) translateY(-6px) scale(1.06); }
  100% { transform: translateX(-50%) translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .otong.pop { animation: none; }
}

/* ---------- announcement carousel ---------- */
/* Scroll-snap rather than a JS slider: the browser already does momentum,
   rubber-banding and snapping natively on touch, and it keeps working if the
   script fails. JS only mirrors the position into the dots. */
/* ONE CARD, FULL WIDTH. This was a bleed-to-the-edge carousel with a fixed
   peek of the next slide — right when there were several banners, wrong now
   that only one is left: the peek made the single card narrower than the
   daily card and the stat row above it, which just read as a mistake. It
   still scrolls if more banners are ever added; each one is simply full
   width. */
.banners {
  display: flex; gap: 12px;
  overflow-x: auto; scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  margin: 16px 0 6px;
  padding: 2px 0 8px;
  scrollbar-width: none;
}
.banners::-webkit-scrollbar { display: none; }

.banner {
  scroll-snap-align: center;
  flex: 0 0 100%;
  position: relative; overflow: hidden;
  border: var(--border); border-radius: var(--r-lg);
  padding: 16px 18px; min-height: 104px;
  text-align: left; font: inherit; color: #fff;
  box-shadow: var(--shadow-lg); cursor: pointer;
  display: flex; flex-direction: column; justify-content: center;
  transition: transform .1s ease, box-shadow .1s ease;
}
.banner:active {
  transform: translate(3px, 3px);
  box-shadow: 2px 2px 0 var(--ink-line);
}
.banner.has-art { background-size: cover; background-position: center; }
.banner.has-art::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  /* sideways scrim: art on the right, words stay readable on the left */
  background: linear-gradient(to right,
    rgba(20,16,12,.82) 0%, rgba(20,16,12,.62) 45%, rgba(20,16,12,.15) 100%);
}
.banner > * { position: relative; z-index: 1; }
.banner .bn-title { font-weight: 800; font-size: 16px; }
.banner .bn-body { font-size: 12.5px; opacity: .9; margin-top: 3px; line-height: 1.45; }
.banner .bn-cta {
  align-self: flex-start; margin-top: 10px;
  background: rgba(255,255,255,.22); border-radius: 999px;
  padding: 5px 12px; font-size: 12px; font-weight: 800;
}

.banner-dots { display: flex; justify-content: center; gap: 6px; margin-bottom: 18px; }
.banner-dots i {
  width: 6px; height: 6px; border-radius: 999px;
  background: var(--line); transition: width .18s ease, background .18s ease;
}
.banner-dots i.on { width: 18px; background: var(--ink-soft); }

.game-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }

.game-card {
  position: relative;
  border: var(--border); border-radius: var(--r-lg);
  padding: 18px 14px 16px;
  text-align: left; font: inherit; color: #fff;
  cursor: pointer; overflow: hidden;
  box-shadow: var(--shadow-lg);
  transition: transform .1s ease, box-shadow .1s ease;
  /* ASPECT-RATIO, NOT A FIXED HEIGHT. With min-height the card kept its height
     while the width grew with the screen, so on a wide screen it became
     255x132 — and the square cover, scaled to 148% of HEIGHT, was only 195px
     wide and left 60px of bare card colour down each side. Locking the ratio
     keeps width/height at 1.25 on every device, comfortably under the 1.48
     zoom below, so the art always covers. */
  aspect-ratio: 5 / 4;
  min-height: 118px;
  display: flex; flex-direction: column; justify-content: space-between;
}
/* same physical press as the icon buttons, so the whole app shares one gesture */
.game-card:active {
  transform: translate(3px, 3px);
  box-shadow: 2px 2px 0 var(--ink-line);
}

/* COVER ART. The card keeps its solid colour as the base layer, so a missing
   or still-loading .webp degrades to exactly what shipped before rather than
   to a blank tile — art is an upgrade, never a dependency.

   The scrim is not optional. Card text is white and the art is generated, so
   nothing guarantees the area under the title stays dark: one pale corner and
   the game's name vanishes. A fixed bottom-up gradient makes legibility a
   property of the CARD rather than something all 34 images must each get right. */
.game-card.has-cover {
  /* Scaled past `cover` on purpose. At the real card size (~162x132) a plain
     `cover` on a 1:1 image leaves a wide margin of empty background showing
     above the subject — the source art always has some breathing room at the
     top, and a same-aspect crop can't cut into it. Sizing up creates crop
     slack, and `18%` position spends it mostly on the top margin while still
     showing the whole subject (Bena: "kayak count the cubes gitu nggak
     keliatan tumpukan cubesnya" — the stack was getting pushed out entirely). */
  background-size: auto 148%;
  background-position: center 18%;
  background-repeat: no-repeat;
}
/* With the icon hidden the card has a single flex child, and space-between
   parks a lone child at the START — the title jumped to the top of the card.
   Pin it to the bottom explicitly instead of relying on sibling count. */
.game-card.has-cover { justify-content: flex-end; }
.game-card.has-cover::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top,
    rgba(20, 16, 12, .78) 0%,
    rgba(20, 16, 12, .55) 30%,
    rgba(20, 16, 12, .10) 58%,
    rgba(20, 16, 12, 0) 78%);
}
/* lift the real content above the scrim */
.game-card > * { position: relative; z-index: 1; }
/* the art replaces the icon, so reclaim its space for the picture */
.game-card.has-cover .gc-emoji { display: none; }

.game-card .gc-emoji { font-size: 32px; line-height: 1; }
.game-card .gc-name { font-weight: 800; font-size: 17px; margin-top: auto; }
/* Hardest Wordsearch only: this cover has its own title burned into the
   art ("THE HARDEST WORDSEARCH" in flaming letters), so the usual CSS title
   would just repeat it — drop the overlay and let the art speak once.
   Position stays high (not the shared 18%) so that baked title isn't the
   part that gets cropped off. */
.game-card.gc-extreme .gc-name { display: none; }
.game-card.gc-extreme.has-cover {
  background-position: center 4%;
}
.game-card .gc-meta { font-size: 12px; opacity: .85; font-weight: 700; }
.game-card.locked { filter: grayscale(.55) opacity(.6); }
.gc-lock {
  position: absolute; top: 12px; right: 12px; font-size: 16px;
}
.gc-bar {
  height: 5px; border-radius: 3px; background: rgba(255,255,255,.35);
  margin-top: 8px; overflow: hidden;
}
.gc-bar i { display: block; height: 100%; background: #fff; border-radius: 3px; }

.footer-note {
  text-align: center; color: var(--ink-soft); font-size: 12px; margin-top: 26px;
}

/* ---------- tiers + levels ---------- */
.tier-tabs {
  display: flex; gap: 8px; margin: 4px 0 14px;
  overflow-x: auto; padding-bottom: 4px;
}
.tier-tab {
  flex: none; padding: 9px 16px; border-radius: 999px;
  border: var(--border); background: var(--surface);
  font: inherit; font-weight: 800; font-size: 14px; color: var(--ink-soft);
  cursor: pointer; white-space: nowrap;
  transition: transform .1s ease, box-shadow .1s ease;
}
.tier-tab.active {
  background: var(--accent, var(--c1)); color: #fff;
  box-shadow: var(--shadow);
}
.tier-desc { color: var(--ink-soft); font-size: 13px; margin: 0 0 16px; line-height: 1.5; }

.level-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.level-cell {
  aspect-ratio: 1; border: var(--border); border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; font-weight: 900; font-size: 19px; color: var(--ink);
  cursor: pointer; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  transition: transform .1s ease, box-shadow .1s ease;
}
.level-cell:active {
  transform: translate(2px, 2px);
  box-shadow: 1px 1px 0 var(--ink-line);
}
.level-cell.done { background: var(--accent, var(--c1)); color: #fff; }
/* A locked level reads as locked from across the room: dimmed, softened, and
   visibly not a button. `cursor: default` matters as much as the blur — the
   pointer changing on hover is what tells a desktop player it is dead before
   they bother tapping. */
.level-cell.locked {
  opacity: .38;
  filter: blur(1.1px) grayscale(.55);
  cursor: default;
  box-shadow: none;
  transform: none;
}
.level-cell.locked:active { transform: none; }

/* The tab for a locked tier gets the same treatment, minus the blur — a
   blurred word is unreadable, and the player still needs to know WHICH
   difficulty they are working towards. */
.tier-tab.locked { opacity: .45; filter: grayscale(.6); cursor: default; }

.win-unlock {
  margin: 10px 0 2px; padding: 10px 12px;
  border: 2px dashed var(--ink-line); border-radius: var(--r-sm);
  background: rgba(217,164,65,.14);
  font-size: 13.5px; line-height: 1.4; text-align: center;
}
/* Gold, with a dark edge so it still reads on the pale unsolved tile as well
   as on the saturated solved one — plain white stars vanished against Easy's
   pink and plain gold vanished against the unsolved cream. */
.level-cell .lc-stars {
  font-size: 11px; letter-spacing: 1px;
  color: #f2b736; text-shadow: 0 1px 0 rgba(47,42,38,.45);
}

/* ---------- play ---------- */
/* The stage MUST be able to scroll. It was `overflow: visible`, so on a short
   phone anything taller than the screen was simply cut off with no way to reach
   it — the 0 / backspace / tick row of the number pad sat 31px below a 640px
   viewport and was unreachable, on Easy as much as Expert.

   `safe center` is the important half: plain `center` on a flex column pushes
   overflow off BOTH ends, so the top becomes unscrollable too. `safe` falls
   back to flex-start the moment the content does not fit. The unprefixed
   `center` above it is the fallback for browsers that do not know `safe`. */
.play-stage {
  flex: 1; display: flex; flex-direction: column;
  align-items: center;
  justify-content: center;
  justify-content: safe center;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 4px 14px calc(14px + var(--safe-b)); min-height: 0;
}

/* ---------- board paper, one per category --------------------------------
   THE SURFACE VARIES, THE CONTROLS NEVER DO. Bena asked for boards that differ
   by category so they stop feeling flat. The tempting version — restyling the
   cells and buttons per category — is the wrong one: a six-year-old learns
   "tappable looks like THIS" once, and 33 dialects of that would undo it. So
   every game keeps identical controls, and only the paper underneath changes.

   All of these are gradients or one inline SVG: no image files, nothing to
   fetch, nothing that can fail offline.

   Opacity is deliberately tiny (3-7%). This sits UNDER a puzzle the child has
   to read; the moment the texture competes with the board it has failed. */
#screen-play {
  background-image:
    radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--accent, var(--c1)) 14%, transparent), transparent 60%);
}

/* words — ruled notebook paper */
#screen-play[data-cat="words"] .play-stage {
  background-image: repeating-linear-gradient(
    to bottom, transparent 0 27px, rgba(63,125,116,.16) 27px 28px);
}

/* logic — graph paper */
#screen-play[data-cat="logic"] .play-stage {
  background-image:
    repeating-linear-gradient(to right,  transparent 0 21px, rgba(74,111,165,.13) 21px 22px),
    repeating-linear-gradient(to bottom, transparent 0 21px, rgba(74,111,165,.13) 21px 22px);
}

/* number — squared exercise book, bigger squares than logic */
#screen-play[data-cat="number"] .play-stage {
  background-image:
    repeating-linear-gradient(to right,  transparent 0 33px, rgba(200,145,47,.16) 33px 34px),
    repeating-linear-gradient(to bottom, transparent 0 33px, rgba(200,145,47,.16) 33px 34px);
}

/* spatial — blueprint dots */
#screen-play[data-cat="spatial"] .play-stage {
  background-image: radial-gradient(circle, rgba(63,139,147,.22) 1.4px, transparent 1.5px);
  background-size: 22px 22px;
}

/* memory — soft felt, woven at 45° */
#screen-play[data-cat="memory"] .play-stage {
  background-image:
    repeating-linear-gradient( 45deg, rgba(201,106,134,.07) 0 6px, transparent 6px 12px),
    repeating-linear-gradient(-45deg, rgba(201,106,134,.07) 0 6px, transparent 6px 12px);
}

/* deduce — detective's corkboard */
#screen-play[data-cat="deduce"] .play-stage {
  background-image:
    radial-gradient(circle at 20% 30%, rgba(141,107,80,.13) 2px, transparent 2.5px),
    radial-gradient(circle at 70% 65%, rgba(141,107,80,.10) 2.5px, transparent 3px);
  background-size: 34px 34px, 47px 47px;
}

/* solve — workshop pegboard */
#screen-play[data-cat="solve"] .play-stage {
  background-image: radial-gradient(circle, rgba(111,148,81,.20) 2px, transparent 2.5px);
  background-size: 27px 27px;
}
/* Sits under the app bar, out of the thumb zone. Laid out as a compact row so
   it costs the board as little vertical space as possible. */
.play-toolbar {
  display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;
  padding: 2px 14px 8px;
}
.tool-btn {
  min-width: 56px; padding: 7px 12px;
  border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; font-weight: 800; font-size: 11.5px; color: var(--ink);
  cursor: pointer;
  display: flex; flex-direction: row; align-items: center; gap: 5px;
}
.tool-btn:active { transform: scale(.93); }
.tool-btn { position: relative; }
.tool-btn span.ico { font-size: 15px; }
.tool-btn[disabled] { opacity: .4; }
.tool-badge {
  position: absolute; top: -6px; right: -6px;
  min-width: 20px; height: 20px; padding: 0 5px;
  border-radius: 999px; background: var(--c4); color: #fff;
  font-size: 11px; font-weight: 800; line-height: 20px; text-align: center;
}
.tool-btn.spent .tool-badge { background: var(--line); color: var(--ink-soft); }

.play-hud {
  display: flex; gap: 14px; justify-content: center; align-items: center;
  font-weight: 800; font-size: 14px; color: var(--ink-soft);
  margin-bottom: 10px;
}
.play-hud b { color: var(--ink); }

/* Count the Cubes asks its question in the HUD, where the shared 14px is sized
   for short labels like "3 left" — not for a sentence that IS the puzzle's
   instruction. Bumped and given room to breathe on its own row. */
.sp-hud { font-size: 17px; color: var(--ink); flex-wrap: wrap; row-gap: 4px; }
.sp-hud span { line-height: 1.35; }

.loading {
  color: var(--ink-soft); font-weight: 800; font-size: 15px;
  animation: breathe 1.4s ease-in-out infinite;
}
@keyframes breathe { 0%,100% { opacity: .45 } 50% { opacity: 1 } }

/* ---------- word search ---------- */
.ws-grid { display: grid; gap: 3px; touch-action: none; margin: 0 auto; }
.ws-cell {
  aspect-ratio: 1;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); border-radius: 8px;
  font-weight: 800; color: var(--ink);
  box-shadow: 0 1px 0 rgba(120,100,80,.07);
  transition: background .12s ease, color .12s ease, transform .12s ease;
}
.ws-cell.sel   { background: var(--c1); color: #fff; transform: scale(1.06); }
.ws-cell.found { background: #d9ebe5; color: #4f8578; }
.ws-words {
  display: flex; flex-wrap: wrap; gap: 7px; justify-content: center;
  margin-top: 16px;
}
.ws-word {
  padding: 6px 12px; border-radius: 999px;
  background: var(--surface); box-shadow: var(--shadow);
  font-weight: 800; font-size: 13px; letter-spacing: .5px;
}
.ws-word.found { background: #d9ebe5; color: #7fa79b; text-decoration: line-through; box-shadow: none; }
.ws-word.hinted { outline: 2px dashed var(--c4); }

/* vocabulary card — pops up when a word is found, tap a found chip to re-read */
.ws-meaning {
  margin-top: 14px; padding: 12px 15px;
  background: var(--surface); border-radius: var(--r-md);
  box-shadow: var(--shadow); text-align: left;
  max-width: 420px; width: 100%;
}
.ws-meaning b { display: block; font-size: 15px; letter-spacing: 1px; color: var(--c1); }
.ws-meaning span { display: block; font-size: 13.5px; color: var(--ink-soft); font-weight: 600; margin-top: 3px; line-height: 1.45; }

/* ---------- count the cubes ---------- */
.sp-answer {
  margin-top: 10px; min-width: 110px; padding: 10px 18px;
  background: var(--surface); border-radius: var(--r-md); box-shadow: var(--shadow);
  font-size: 30px; font-weight: 800; text-align: center; letter-spacing: 2px;
}
.sp-answer.empty { color: var(--line); }
.sp-ok { background: var(--c1); color: #fff; }
.shake { animation: shake .38s ease; }
@keyframes shake {
  0%,100% { transform: translateX(0) }
  20% { transform: translateX(-7px) }
  40% { transform: translateX(7px) }
  60% { transform: translateX(-4px) }
  80% { transform: translateX(4px) }
}

#sp-canvas, #sq-canvas { display: block; touch-action: none; }
#sq-canvas { border-radius: var(--r-lg); box-shadow: var(--shadow-lg); }

/* ---------- block slide ----------
   The board read as a bare grey rectangle with a few slabs floating on it —
   "terlalu kosong". A faint grid now shows the lanes the blocks actually run
   in, which both fills the space and explains the movement, and the blocks
   themselves get a top highlight so they look like objects rather than
   flat fills. */
.bs-board {
  position: relative;
  background:
    repeating-linear-gradient(90deg, rgba(120,100,80,.10) 0 1px, transparent 1px var(--bs-cell, 56px)),
    repeating-linear-gradient(0deg,  rgba(120,100,80,.10) 0 1px, transparent 1px var(--bs-cell, 56px)),
    var(--bg-soft);
  background-position: 6px 6px;
  border-radius: var(--r-lg); padding: 6px;
  touch-action: none;
  box-shadow: inset 0 2px 10px rgba(120,100,80,.14), var(--shadow);
}
.bs-exit {
  position: absolute;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; font-weight: 800; color: var(--c3);
  animation: beckon 1.6s ease-in-out infinite;
}
@keyframes beckon { 0%,100% { transform: translateX(0); opacity: .55 } 50% { transform: translateX(4px); opacity: 1 } }
.bs-block {
  position: absolute; border-radius: 12px;
  background: linear-gradient(#c6d1de, #b0bccd);
  box-shadow: 0 3px 0 rgba(0,0,0,.13), inset 0 2px 0 rgba(255,255,255,.55);
  cursor: grab;
}
/* a groove along the block's long axis, so which way it slides is visible */
.bs-block::after {
  content: ''; position: absolute; inset: 34% 14%;
  border-radius: 99px; background: rgba(255,255,255,.35);
}
.bs-block.tall::after { inset: 14% 34%; }
.bs-block.hero {
  background: linear-gradient(#f0b3a6, var(--c3));
  box-shadow: 0 3px 0 #c98d80, inset 0 2px 0 rgba(255,255,255,.5);
}
.bs-block .grip {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 18px;
}
.bs-block.dragging { z-index: 5; filter: brightness(1.06); }

/* ---------- maze ---------- */
.maze-wrap { position: relative; }
#maze-canvas, #pipes-canvas, #flow-canvas {
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg); touch-action: none; display: block;
}
.dpad {
  display: grid; grid-template-columns: repeat(3, 56px); grid-template-rows: repeat(2, 56px);
  gap: 8px; justify-content: center; margin-top: 14px;
}
.dpad button {
  border: none; border-radius: var(--r-md); background: var(--surface);
  box-shadow: var(--shadow); font-size: 22px; font-weight: 800; color: var(--ink); cursor: pointer;
}
.dpad button:active { transform: scale(.92); }
.dpad .up   { grid-column: 2; grid-row: 1; }
.dpad .left { grid-column: 1; grid-row: 2; }
.dpad .down { grid-column: 2; grid-row: 2; }
.dpad .right{ grid-column: 3; grid-row: 2; }

/* ---------- sudoku ---------- */
.sk-grid {
  display: grid; gap: 3px; margin: 0 auto;
  background: var(--bg-soft); padding: 5px; border-radius: var(--r-md);
}
.sk-cell {
  aspect-ratio: 1;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); border-radius: 7px;
  font-weight: 800; color: var(--c8); cursor: pointer;
  transition: background .12s ease, transform .12s ease;
}
.sk-cell.fixed { color: var(--ink); background: #f4ece0; cursor: default; }
.sk-cell.sel { outline: 3px solid var(--c8); transform: scale(1.04); }
.sk-cell.bad { background: #f7dcd6; color: #c4796a; }
/* the extra gap marks where one box ends and the next begins — the how-to
   calls these the "bold boxes", so the split has to read at a glance */
.sk-cell.bl { margin-left: 9px; }
.sk-cell.bt { margin-top: 9px; }

.sk-pad {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 22px;
}
.sk-key {
  width: var(--sk-key-px, 46px); height: var(--sk-key-px, 46px);
  border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; font-weight: 800; font-size: 19px; color: var(--ink);
  cursor: pointer;
}
.sk-key:active { transform: scale(.92); }

/* calculator block — three columns, keys big enough for a 6-year-old's thumb */
/* Fluid, not fixed. At repeat(3, 58px) plus gaps the pad was 192px wide with a
   hard width:max-content, so on a narrow phone with a tall cube stack above it
   the last row — 0, backspace and the tick — was pushed out of view entirely.
   Bena hit this on Count the Cubes hard/expert. */
.calc-pad {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px; width: min(100%, 230px); margin: 16px auto 0;
}
.calc-pad .sk-key { width: auto; height: 46px; font-size: 20px; }
.calc-pad .sp-ok { background: var(--good, #7fbf7f); color: #fff; }
.sk-erase { color: var(--ink-soft); }

/* ---------- modals ---------- */
.modal {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(74,64,56,.42);
  display: flex; align-items: center; justify-content: center;
  padding: 22px;
  animation: fade .18s ease;
}
.modal[hidden] { display: none; }

/* ---------- win confetti: falling paper scraps ----------
   Sits ABOVE the win modal (z 55 vs 50) so the paper falls in front of the
   card rather than behind it — behind reads as a stuck background texture.
   pointer-events:none throughout, so it can never swallow the Next button. */
.confetti {
  position: fixed; inset: 0; z-index: 55;
  pointer-events: none; overflow: hidden;
}
.confetti i {
  position: absolute; top: -20px;
  border: 1.5px solid rgba(47,42,38,.55);   /* the same ink edge as everything else */
  border-radius: 1px;
  animation: paper-fall linear forwards;
}
@keyframes paper-fall {
  0%   { transform: translateY(0) translateX(0) rotate(0deg); opacity: 1; }
  85%  { opacity: 1; }
  100% {
    transform: translateY(105vh) translateX(var(--drift)) rotate(var(--spin));
    opacity: 0;
  }
}
.modal-card {
  position: relative;
  background: var(--surface); border-radius: var(--r-lg);
  padding: 26px 22px 20px; width: 100%; max-width: 400px;
  box-shadow: var(--shadow-lg); text-align: center;
  animation: pop .22s cubic-bezier(.2,1.2,.4,1);
  /* dvh, NOT vh. On iOS Safari `vh` is the height of the viewport WITHOUT the
     browser chrome, so 88vh is taller than what the user can actually see and
     the card runs off both ends of the screen. dvh tracks the visible area.
     The vh line stays first as a fallback for older browsers. */
  max-height: 88vh; max-height: 88dvh; overflow-y: auto;
}
/* Pinned via position:absolute against the scroll container itself (not its
   content), so it stays put in the same corner no matter how far a tall
   changelog or long feedback note pushes the rest of the card down. */
.modal-x {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  width: 30px; height: 30px; border-radius: 50%; border: none;
  background: var(--bg-soft); color: var(--ink-soft);
  font-size: 16px; font-weight: 800; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.modal-x:active { transform: scale(.92); }
.modal-title { margin: 8px 0 14px; font-size: 22px; }
/* The About header is one stacked identity block, so it is set tighter
   than a normal modal title: name, handle and version read as one unit. */
.about-card .modal-title { margin: 2px 0 2px; }
.howto-emoji { font-size: 46px; }
.howto-steps {
  text-align: left; margin: 0 0 20px; padding-left: 22px;
  line-height: 1.65; color: var(--ink-soft); font-weight: 600;
}
.howto-steps li { margin-bottom: 9px; }
.howto-steps b { color: var(--ink); }
.star-rules {
  text-align: left; background: var(--bg-soft); border-radius: var(--r-md);
  padding: 13px 15px; margin-bottom: 16px;
}
.sr-title { font-weight: 800; font-size: 13px; margin-bottom: 9px; }
.sr-row { display: flex; gap: 10px; align-items: baseline; margin-bottom: 5px; }
.sr-row span { flex: none; font-size: 11px; letter-spacing: -1px; width: 46px; }
.sr-row em { font-style: normal; font-size: 12.5px; color: var(--ink-soft); font-weight: 700; }

.play-hud .urgent, .play-hud.urgent { color: #c4796a; }
.play-hud span.urgent b { color: #c4796a; }

.win-burst { font-size: 56px; }
.win-stars {
  font-size: 34px; letter-spacing: 6px; margin: 6px 0 10px;
  color: var(--star); text-shadow: 0 2px 0 rgba(180,140,40,.25);
}
.win-msg { color: var(--ink-soft); margin: 0 0 18px; font-weight: 700; }

@keyframes fade { from { opacity: 0 } }
@keyframes pop  { from { opacity: 0; transform: scale(.9) translateY(10px) } }

/* gentle celebratory nudge, nothing flashy */
@keyframes nudge { 0%,100% { transform: none } 40% { transform: scale(1.12) } }
.pulse { animation: nudge .35s ease; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ---------- slide tiles ---------- */
.st-board {
  position: relative; margin: 0 auto;
  background: var(--bg-soft); border-radius: var(--r-lg);
  box-shadow: inset 0 2px 8px rgba(120,100,80,.10);
}
.st-tile {
  position: absolute; top: 0; left: 0;
  border: none; border-radius: 12px;
  background: var(--surface); color: var(--ink);
  font: inherit; font-weight: 800;
  box-shadow: 0 3px 0 rgba(120,100,80,.14);
  cursor: pointer;
  transition: transform .13s cubic-bezier(.3,1.1,.5,1);
}
.st-tile:active { filter: brightness(.96); }
.st-tile.home { background: #e2efe9; color: #4f8578; }

/* ---------- word builder ---------- */
.wb-clue {
  background: var(--surface); border-radius: var(--r-md);
  padding: 14px 16px; box-shadow: var(--shadow);
  max-width: 420px; width: 100%; text-align: left; margin-bottom: 20px;
}
.wb-label {
  display: block; font-size: 11px; font-weight: 800; letter-spacing: 1px;
  color: var(--c1); text-transform: uppercase; margin-bottom: 4px;
}
.wb-meaning { display: block; font-size: 15px; font-weight: 700; line-height: 1.45; }

.wb-slots {
  display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;
  margin-bottom: 22px; cursor: pointer;
}
.wb-slot {
  width: 1.9em; height: 2.2em;
  display: flex; align-items: center; justify-content: center;
  /* a visible empty box, not a faint underline — a child needs to see how many
     letters are still missing at a glance */
  background: rgba(255,253,249,.7);
  border: 2px dashed var(--line);
  border-bottom: 3px solid var(--line);
  border-radius: 8px;
  font-weight: 800; color: var(--ink);
}
.wb-slot.filled {
  background: var(--surface); border-style: solid;
  border-color: var(--c1); box-shadow: var(--shadow);
}
.wb-slots.ok .wb-slot { background: #d9ebe5; border-bottom-color: var(--good); color: #4f8578; }

.wb-pool { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.wb-letter {
  width: 2.1em; height: 2.1em;
  border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; font-weight: 800; color: var(--ink); cursor: pointer;
}
.wb-letter:active { transform: scale(.92); }
.wb-letter.used { opacity: .25; pointer-events: none; }

/* ---------- futoshiki ---------- */
.fu-grid { position: relative; margin: 0 auto; }
.fu-cell {
  position: absolute;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); border-radius: 9px;
  font-weight: 800; color: var(--c5); cursor: pointer;
  box-shadow: 0 1px 0 rgba(120,100,80,.08);
  transition: background .12s ease, transform .12s ease;
}
.fu-cell.fixed { color: var(--ink); background: #f2ecf5; cursor: default; }
.fu-cell.sel { outline: 3px solid var(--c5); transform: scale(1.05); }
.fu-cell.bad { background: #f7dcd6; color: #c4796a; }
.fu-sign {
  position: absolute;
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; color: var(--ink-soft);
  font-size: 15px; pointer-events: none;
}

#ak-canvas { border-radius: var(--r-lg); box-shadow: var(--shadow-lg); display: block; }

/* ---------- robot commands ---------- */
#rb-canvas { border-radius: var(--r-lg); box-shadow: var(--shadow-lg); display: block; }

.rb-msg {
  min-height: 20px; margin-top: 10px;
  font-size: 13px; font-weight: 800; text-align: center;
}
.rb-msg.bad { color: #c4796a; }
.rb-msg.warn { color: var(--warn); }

/* the program the child has written, left-to-right */
.rb-prog {
  display: flex; flex-wrap: wrap; gap: 5px; justify-content: center;
  margin: 8px 0 14px; min-height: 34px;
  max-width: 420px; width: 100%;
}
.rb-chip {
  width: 32px; height: 32px;
  border: none; border-radius: 9px;
  background: var(--surface); box-shadow: var(--shadow);
  font-size: 15px; cursor: pointer; color: var(--ink);
  display: flex; align-items: center; justify-content: center;
}
.rb-chip:active { transform: scale(.9); }
.rb-chip.bad { background: #f7dcd6; box-shadow: 0 0 0 2px #c4796a; }

.rb-pad { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.rb-key {
  min-width: 66px; padding: 9px 12px;
  border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; color: var(--ink); cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.rb-key:active { transform: scale(.94); }
.rb-ico { font-size: 18px; line-height: 1; }
.rb-lbl { font-size: 11px; font-weight: 800; }
.rb-run { background: var(--c1); color: #fff; box-shadow: 0 3px 0 #6a9c90; }

/* ---------- memory cards ---------- */
.me-grid { display: grid; margin: 0 auto; }
.me-card {
  position: relative; border: none; border-radius: 12px;
  background: var(--c7); box-shadow: var(--shadow);
  cursor: pointer; overflow: hidden; padding: 2px;
}
.me-card:active { transform: scale(.95); }
.me-back, .me-face {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  transition: opacity .16s ease;
  text-align: center; line-height: 1.1; word-break: break-word;
}
.me-back { color: #fff; font-size: 22px; font-weight: 800; opacity: 1; }
.me-face { opacity: 0; font-weight: 800; color: var(--ink); padding: 3px; }
.me-card.up { background: var(--surface); }
.me-card.up .me-back { opacity: 0; }
.me-card.up .me-face { opacity: 1; }
.me-card.done { background: #d9ebe5; box-shadow: none; }

/* ---------- number pyramid ---------- */
.py-wrap { display: flex; flex-direction: column; gap: 6px; align-items: center; }
.py-row { display: flex; justify-content: center; }
.py-brick {
  border: none; border-radius: 10px;
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; font-weight: 800; color: var(--c6); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.py-brick.fixed { background: #e7f0e4; color: var(--ink); cursor: default; }
.py-brick.sel { outline: 3px solid var(--c6); transform: scale(1.05); }
.py-brick.bad { background: #f7dcd6; color: #c4796a; }

/* ---------- crack the code ---------- */
.cb-history {
  width: 100%; max-width: 340px;
  max-height: 210px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px;
}
.cb-row {
  display: flex; align-items: center; justify-content: space-between;
  background: var(--surface); border-radius: var(--r-sm);
  padding: 5px 9px; box-shadow: 0 1px 0 rgba(120,100,80,.07);
}
.cb-reveal { background: #f4ece0; }
.cb-picks { display: flex; gap: 4px; }
.cb-chip { font-size: 19px; }
.cb-marks { display: flex; gap: 8px; font-size: 12px; font-weight: 800; }
.cb-exact { color: var(--good); }
.cb-near { color: var(--warn); }

.cb-current { display: flex; gap: 7px; justify-content: center; margin-bottom: 16px; }
.cb-slot {
  width: 46px; height: 46px;
  border: 2px dashed var(--line); border-radius: 12px;
  background: rgba(255,253,249,.7);
  font-size: 24px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.cb-slot.filled { border-style: solid; border-color: var(--c8); background: var(--surface); }
.cb-slot.locked { border-color: var(--star); background: #fbf3dd; }

.cb-pad { display: flex; gap: 7px; justify-content: center; flex-wrap: wrap; }
.cb-key {
  width: 46px; height: 46px;
  border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font-size: 24px; cursor: pointer;
}
.cb-key:active { transform: scale(.92); }
.cb-submit { background: var(--line); color: #fff; font-size: 20px; }
.cb-submit.ready { background: var(--c1); }
.cb-submit[disabled] { opacity: .5; }

/* puzzle type, so a parent can tell at a glance what a game trains */
.game-card .gc-cat {
  font-size: 10.5px; font-weight: 800; letter-spacing: .5px;
  text-transform: uppercase; opacity: .72; margin-top: 1px;
}

/* theme banner above the word grid — gives each level an identity */
.ws-theme {
  font-size: 12px; font-weight: 800; letter-spacing: 1.2px;
  color: var(--ink-soft); margin-bottom: 6px; text-transform: uppercase;
}
.ws-theme b { color: var(--c1); }

/* ---------- about ---------- */
/* THE CARD ITSELF MUST NOT SCROLL. .modal-card scrolls its whole body, and
   .modal-x is absolutely positioned inside it — so the close button scrolled
   off the top and there was no visible way out of this screen. Here the card
   is a fixed-height column that never scrolls; only the change log inside it
   does. The X stays where it is put. */
.about-card {
  text-align: center;
  display: flex; flex-direction: column;
  max-height: 86vh; max-height: 86dvh; overflow: hidden;
}
.about-logo { font-size: 30px; line-height: 1; }
.about-ver {
  display: inline-block; margin: 0 0 8px; padding: 2px 10px;
  border-radius: 999px; background: var(--bg-soft);
  font-size: 12px; font-weight: 800; color: var(--ink-soft);
}
.about-tag {
  color: var(--ink-soft); font-weight: 700; margin: 0 0 12px; font-size: 13px;
  white-space: pre-line;   /* the blank line before "- professor" is real */
}
.about-links { display: flex; gap: 8px; justify-content: center; margin-bottom: 12px; }
.about-links a {
  display: flex; align-items: center; gap: 6px;
  padding: 9px 15px; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  color: var(--ink); text-decoration: none; font-weight: 800; font-size: 13px;
}
.about-sub {
  text-align: left; font-weight: 800; font-size: 13px; flex: none;
  padding-top: 10px; border-top: 2px solid var(--line); margin-bottom: 8px;
}
/* The only scrolling region on this screen, and it is CAPPED rather than
   stretched. With flex:1 it grew to fill whatever the card's 86vh left over,
   so tightening the header above it changed nothing — the card stayed the
   same height and the log just got taller. A fixed cap lets the card shrink
   to its content instead. */
.about-log {
  text-align: left; flex: 0 1 auto; min-height: 0;
  max-height: 38vh; max-height: 38dvh; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.log-entry { margin-bottom: 14px; }
/* ONE LINE, ALWAYS. Wrapping to two lines cost ~30px of card height, which
   came straight out of the change log's budget. Smaller type and tighter
   padding than a normal .btn, plus nowrap so it can never wrap back. */
#btn-feedback {
  flex: none; margin-top: 10px;
  padding: 12px 14px; font-size: 15px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.log-head { display: flex; align-items: baseline; gap: 8px; }
.log-v {
  font-weight: 800; font-size: 12px; color: #fff;
  background: var(--c1); border-radius: 999px; padding: 2px 9px;
}
.log-date { font-size: 11px; color: var(--ink-soft); font-weight: 700; }
.log-title { font-weight: 800; font-size: 13.5px; margin: 5px 0 3px; }
.log-items { margin: 0; padding-left: 18px; }
.log-items li {
  font-size: 12.5px; color: var(--ink-soft); font-weight: 600;
  line-height: 1.5; margin-bottom: 3px;
}

/* ---------- worked-example demo ---------- */
.demo-bar {
  display: flex; align-items: center; gap: 10px;
  margin: 0 14px 6px; padding: 10px 14px;
  background: var(--c1); color: #fff; border-radius: var(--r-md);
  box-shadow: var(--shadow); font-weight: 800; font-size: 13px;
  animation: fade .2s ease;
}
.demo-bar[hidden] { display: none; }
#demo-say { flex: 1; line-height: 1.35; }
.demo-stop {
  flex: none; border: none; border-radius: 999px;
  background: rgba(255,255,255,.25); color: #fff;
  font: inherit; font-weight: 800; font-size: 12px;
  padding: 5px 12px; cursor: pointer;
}
/* the hand that shows where the demo is "touching" */
.demo-ghost {
  position: fixed; z-index: 40; pointer-events: none;
  width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border-radius: 50%; border: 3px solid var(--c1);
  background: rgba(127,181,168,.28);
  transition: left .28s ease, top .28s ease;
}

/* ---------- star store ---------- */
.wallet {
  display: flex; align-items: center; gap: 14px;
  background: var(--c1); color: #fff;
  border-radius: var(--r-lg); padding: 16px 18px; margin-bottom: 14px;
  box-shadow: var(--shadow-lg);
}
.wallet-main { flex: 1; }
.wallet-main b { display: block; font-size: 34px; line-height: 1; }
.wallet-main span { font-size: 12px; font-weight: 800; opacity: .9; }
.wallet-side { display: flex; flex-direction: column; gap: 6px; text-align: right; }
.wallet-side b { font-size: 15px; }
.wallet-side span { font-size: 10.5px; font-weight: 700; opacity: .8; margin-left: 5px; }


.store-note { color: var(--ink-soft); font-size: 12.5px; margin: 0 0 16px; line-height: 1.5; }
.store-list { display: flex; flex-direction: column; gap: 10px; }

.store-row {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border-radius: var(--r-md);
  padding: 12px 14px; box-shadow: var(--shadow);
}
.store-row.owned { opacity: .62; }
.sr-icon {
  width: 44px; height: 44px; flex: none; border-radius: 12px;
  display: flex; align-items: center; justify-content: center; font-size: 22px;
}
.sr-body { flex: 1; min-width: 0; }
.sr-name { font-weight: 800; font-size: 14.5px; }
.sr-cat {
  font-size: 10.5px; font-weight: 800; letter-spacing: .5px;
  text-transform: uppercase; color: var(--ink-soft); margin-top: 1px;
}
.sr-buy {
  flex: none; border: none; border-radius: var(--r-md);
  padding: 9px 14px; font: inherit; font-weight: 800; font-size: 13px;
  background: var(--c1); color: #fff; cursor: pointer;
  box-shadow: 0 3px 0 #6a9c90;
}
.sr-buy:active { transform: translateY(2px); box-shadow: 0 1px 0 #6a9c90; }
.sr-buy.cant { background: var(--line); color: var(--ink-soft); box-shadow: none; }
.sr-have { flex: none; font-size: 12px; font-weight: 800; color: var(--good); }
.sr-buy.equipped { background: var(--good); box-shadow: 0 3px 0 #4a8a5a; }
.sr-icon img.sr-glyph { width: 30px; height: 30px; object-fit: contain; }

/* the moment a purchase lands */
.buy-flash { animation: buyflash .6s ease; }
@keyframes buyflash {
  0% { transform: scale(1) }
  35% { transform: scale(1.04); box-shadow: 0 0 0 3px var(--c1) }
  100% { transform: scale(1) }
}

/* ---------- parent account ---------- */
#screen-account { justify-content: center; padding: 24px; }
.acct-box { text-align: center; width: 100%; }
.acct-tabs {
  display: flex; gap: 8px; margin: 0 0 16px;
  background: var(--bg-soft); border-radius: var(--r-md); padding: 4px;
}
.acct-tab {
  flex: 1; padding: 10px; border: none; border-radius: 12px;
  background: transparent; font: inherit; font-weight: 800; font-size: 14px;
  color: var(--ink-soft); cursor: pointer;
}
.acct-tab.on { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); }
.acct-input {
  font-size: 16px; letter-spacing: 0; text-align: left; margin-bottom: 10px;
}

/* ---------- kid picker ---------- */
.kid-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
.kid-card {
  display: flex; align-items: center; gap: 14px;
  background: var(--surface); border: none; border-radius: var(--r-lg);
  padding: 16px; box-shadow: var(--shadow); cursor: pointer;
  font: inherit; text-align: left; width: 100%;
}
.kid-card:active { transform: scale(.97); }
.kid-face {
  width: 54px; height: 54px; flex: none; border-radius: 16px;
  background: var(--bg-soft); display: flex; align-items: center;
  justify-content: center; font-size: 28px;
}
.kid-name { font-weight: 800; font-size: 16px; }
.kid-meta { font-size: 12px; color: var(--ink-soft); font-weight: 700; margin-top: 2px; }

/* ---------- nickname builder ---------- */
.nk-hint {
  font-size: 12px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
  color: var(--ink-soft); margin: 6px 0 10px;
}
/* four across, not five: the swatches now hold a whole character rather than a
   single emoji, and eight of them divide evenly into two tidy rows */
.nk-avatars {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 22px;
}
.nk-av {
  aspect-ratio: 1; border: var(--border); border-radius: 14px;
  background: var(--surface); box-shadow: var(--shadow);
  cursor: pointer; padding: 4px;
  display: grid; place-items: center;
  transition: transform .1s ease, box-shadow .1s ease;
}
.nk-av img { width: 100%; height: 100%; object-fit: contain; display: block; }
.nk-av.on {
  border-color: var(--c1);
  transform: translate(2px, 2px);
  box-shadow: 1px 1px 0 var(--ink-line);
  background: var(--bg-soft);
}
.nk-namebox {
  display: flex; align-items: center; gap: 6px;
  background: var(--surface); border-radius: var(--r-md);
  padding: 14px 8px; box-shadow: var(--shadow);
}
.nk-name { flex: 1; font-weight: 800; font-size: 20px; }
.nk-arrow {
  width: 38px; height: 38px; flex: none; border: none; border-radius: 12px;
  background: var(--bg-soft); font: inherit; font-size: 20px; font-weight: 800;
  color: var(--ink); cursor: pointer;
}
.nk-arrow:active { transform: scale(.9); }
.nk-row {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin-top: 8px;
}
.nk-sub { font-size: 12px; font-weight: 700; color: var(--ink-soft); }
.nk-note {
  font-size: 11.5px; color: var(--ink-soft); line-height: 1.5;
  margin: 14px 0 4px; text-align: center;
}

/* ---------- leaderboard ---------- */
.lead-list { display: flex; flex-direction: column; gap: 8px; }
.lead-row {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border-radius: var(--r-md);
  padding: 11px 14px; box-shadow: var(--shadow);
}
.lead-row.me { background: #e2efe9; box-shadow: 0 0 0 2px var(--c1); }
.lead-rank {
  width: 30px; flex: none; font-weight: 800; font-size: 14px;
  color: var(--ink-soft); text-align: center;
}
.lead-row:nth-child(1) .lead-rank { color: var(--star); font-size: 17px; }
/* ---------- weekly league ---------- */
.lead-head { margin: 2px 0 12px; }
.lg-top-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.lg-tier {
  font-size: 18px; font-weight: 900; color: var(--tier, var(--ink));
  display: flex; align-items: center; gap: 6px;
}
.lg-tier::before {
  content: ''; width: 12px; height: 12px; border-radius: 50%;
  background: var(--tier, var(--line)); border: 2px solid var(--ink-line);
}
.lg-week { font-size: 11.5px; font-weight: 800; color: var(--ink-soft); }
/* the explainer block: your level range, what's above you, and where you
   stand lifetime — added so the board answers "am I doing okay" without
   making the player go compute it from a level number themselves */
.lg-info {
  display: flex; flex-direction: column; gap: 2px;
  font-size: 11.5px; color: var(--ink-soft); margin-top: 6px;
}
.lg-next.lg-top { color: var(--star); font-weight: 800; }
.lg-xprank {
  font-size: 12px; font-weight: 800; color: var(--c1); margin-top: 6px;
}

/* the break between the leaders and your own neighbourhood */
.lead-gap {
  text-align: center; color: var(--ink-soft);
  font-size: 18px; letter-spacing: 4px; padding: 2px 0 6px;
}
.lead-rank.crowned { font-size: 20px; }

.lead-face { font-size: 22px; flex: none; }

/* League cosmetic frames — a ring around the avatar photo, not the row. Tier
   rings use the same three colours as the league board itself (see league.js
   TIERS); rank rings are a shade louder and get a small medal badge, since
   holding #1 in your tier should read as more than just "still gold". Uses
   box-shadow rather than border so it never nudges the image's own layout
   box — a ring drawn OUTSIDE the element's box, not added to it. */
.otong-frame {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; line-height: 0; position: relative;
}
/* AN EMOJI AVATAR HAS NO HEIGHT OF ITS OWN. The ring was written for the
   Otong image, which .lead-face sizes to 30x30, so the circle came out right.
   A player using an emoji renders a bare <span> inside a frame with
   line-height:0 — zero height, so the "ring" collapsed into a flat bar behind
   the emoji. That is what 2nd and 3rd place were showing. Giving the frame its
   own box makes it a circle whatever is inside it. */
.lead-face .otong-frame {
  width: 30px; height: 30px; line-height: 1; font-size: 18px;
}
.otong-frame-bronze { box-shadow: 0 0 0 2px #b3743a; }
.otong-frame-silver { box-shadow: 0 0 0 2px #9aa4ad; }
.otong-frame-gold   { box-shadow: 0 0 0 2px #d9a441; }
.otong-frame-rank1, .otong-frame-rank2, .otong-frame-rank3 { box-shadow: 0 0 0 3px; }
.otong-frame-rank1 { box-shadow: 0 0 0 3px #d9a441, 0 0 8px rgba(217,164,65,.65); }
.otong-frame-rank2 { box-shadow: 0 0 0 3px #9aa4ad, 0 0 6px rgba(154,164,173,.55); }
.otong-frame-rank3 { box-shadow: 0 0 0 3px #b3743a, 0 0 6px rgba(179,116,58,.55); }
/* NO corner medal on the avatar. The rank column at the left of every row
   already shows 🥇🥈🥉, so a second copy tucked into the ring said the same
   thing twice — and the coloured ring alone already reads as the podium. */
/* Rank 1 wears a crown. This is the ONLY crown in the app — the leaderboard
   row deliberately draws none of its own, and cosmetics (which once included a
   buyable gold crown) are gone, so 👑 now has exactly one meaning: holding
   first place right now. */
.otong-frame-rank1::before {
  content: '👑'; position: absolute; top: -13px; left: 50%;
  transform: translateX(-50%) rotate(-6deg); font-size: 15px; line-height: 1;
}

/* Cosmetic accessories (hats, crowns) sit ABOVE the body image, mostly
   outside its own box — Otong's head already touches the top edge of his own
   384x384 canvas with zero headroom, so there is nowhere to put a hat INSIDE
   it. .otong-wrap shrink-wraps to exactly the body <img>'s rendered size and
   allows overflow, so the accessory (sized and positioned as a % of that box)
   scales correctly whether the avatar is rendered at 16px or 92px. */
.otong-wrap { position: relative; display: inline-flex; overflow: visible; }
.acc-img {
  position: absolute; left: 50%; transform: translateX(-50%);
  pointer-events: none; display: block;
}

/* Otong now stands in for the old emoji avatar in five different places, each
   with its own box. One rule sizes him to whatever container he lands in, so a
   new avatar site never needs its own image CSS. */
.kid-face img, .lead-face img, .wc-face img,
#ci-face img, #btn-profile img {
  width: 1.35em; height: 1.35em;
  object-fit: contain; display: block;
}
#btn-profile img { width: 30px; height: 30px; margin: 0 auto; }
.lead-face img { width: 30px; height: 30px; }
.lead-name { flex: 1; font-weight: 800; font-size: 14px; }
.lead-stars { font-weight: 800; font-size: 13px; color: var(--c1); }
.lead-empty { text-align: center; color: var(--ink-soft); font-size: 13px; padding: 30px 0; }

.nk-city {
  width: 100%; padding: 13px 14px;
  border: 2px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink);
  font: inherit; font-weight: 700; font-size: 15px;
}
.lead-body { flex: 1; min-width: 0; }
.lead-sub { font-size: 11px; color: var(--ink-soft); font-weight: 700; margin-top: 1px; }
.lead-ig { font-weight: 700; font-size: 11.5px; color: var(--c2); }


/* ---------- game icons ---------------------------------------------------
   The <symbol>s carry their own colours, so these rules only size them. The
   emoji fallback keeps the same class, hence the two selectors.            */
.gc-emoji-svg { width: 46px; height: 46px; display: block; }
.sr-glyph { width: 30px; height: 30px; display: block; }
.sr-icon { display: grid; place-items: center; }


/* ---------- home app bar on a phone --------------------------------------
   Six 42px buttons plus a title do not fit across 375px, so the last one was
   sliding off the right edge. Rather than shrinking every target below what a
   child can reliably hit, the home bar wraps: title on its own line, actions
   centred underneath. Only the home bar needs this — every other screen has a
   back button, a title, and nothing else.                                   */
@media (max-width: 480px) {
  #screen-home .app-bar { flex-wrap: wrap; row-gap: 6px; }
  #screen-home .app-bar-title { flex: 1 0 100%; justify-content: flex-start; }
  /* Right-aligned, not centred: the wordmark above starts hard left, so a
     centred row of buttons lined up with neither edge. */
  #screen-home .app-bar-actions { flex: 1 0 100%; justify-content: flex-end; }
}


/* ---------- wordmark ------------------------------------------------------
   The credit sits under the name rather than beside it, so it reads as a
   signature and never competes with the title for width on a phone.        */
.gate-by {
  margin: -2px 0 6px; text-align: center;
  font-size: 13px; font-weight: 700; letter-spacing: .3px;
  color: var(--ink-soft); opacity: .8;
}
.bar-name { display: flex; flex-direction: column; line-height: 1.05; }
.bar-by {
  font-size: 10px; font-weight: 700; letter-spacing: .4px;
  color: var(--ink-soft); opacity: .75;
}


/* Three controls per kid card: ✎ edit, ↺ reset progress, 🗑 delete. Sized to
   a comfortable tap target (36px) rather than the 28px they started at —
   these sit close together and two of the three are destructive, so a
   mis-tap is expensive. Ordered least to most destructive, left to right. */
.kid-card { position: relative; }
.kid-wipe, .kid-edit, .kid-del {
  position: absolute; top: 6px;
  width: 36px; height: 36px; display: grid; place-items: center;
  border-radius: 50%; font-size: 18px; line-height: 1;
  color: var(--ink-soft); opacity: .6;
}
.kid-del  { right: 8px; }
.kid-wipe { right: 48px; }
.kid-edit { right: 88px; }
.kid-wipe:active, .kid-edit:active, .kid-del:active {
  opacity: 1; transform: scale(.9); background: var(--bg-soft);
}

/* the free-text alternative under the word picker — same boxed shape as the
   word picker above it, so it reads as an equal option, not an afterthought */
.nk-prefix { font-weight: 800; opacity: .7; margin-right: 2px; }
.nk-or {
  text-align: center; font-size: 12px; opacity: .55; margin: 10px 0 6px;
}
.nk-custom-input {
  flex: 1; border: none; background: transparent; font: inherit;
  font-weight: 800; font-size: 20px; color: var(--ink); min-width: 0;
}
.nk-custom-input:focus { outline: none; }

.nk-toggle {
  display: flex; align-items: center; gap: 8px;
  margin: 10px 2px 0; font-size: 13px; font-weight: 700; color: var(--ink-soft);
}
.nk-toggle input { width: 18px; height: 18px; accent-color: var(--c1); }


/* ==========================================================================
   Paket A/B — Tebak Emoji, Acak Huruf, Urutan Pola, Rotasi Bentuk,
   Bayangan Cermin, Jaring Kubus, Puzzle Potong
   Shared "multiple choice" building blocks: .right / .wrong / .ruled-out
   are reused across Emoji, Rotasi, and Jaring Kubus so picking a correct
   answer, a wrong one, or a hint-eliminated one always look the same.
   ========================================================================== */
.right { outline: 3px solid var(--good); background: #d9ebe5 !important; }
.wrong { outline: 3px solid var(--warn); background: #f7e3d1 !important; }
.ruled-out { opacity: .3; text-decoration: line-through; }

/* ---------- tebak emoji ---------- */
.em-clue { font-size: 52px; text-align: center; margin: 10px 0 26px; line-height: 1.3; }
.em-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.em-opt {
  padding: 16px 8px; border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; font-weight: 800; font-size: 14px; color: var(--ink); cursor: pointer;
}
.em-opt:active { transform: scale(.96); }
.em-opt:disabled { cursor: default; }

/* type-in mode: intermediate and up spell the answer instead of picking it */
.em-scaffold {
  text-align: center; font-weight: 800; font-size: 22px; letter-spacing: 6px;
  color: var(--ink-soft); margin-bottom: 18px;
}
.em-answer-wrap { display: flex; gap: 10px; align-items: center; }
.em-answer-input { flex: 1; margin: 0; }

/* ---------- acak huruf ---------- */
.an-clue {
  text-align: center; font-size: 13px; color: var(--ink-soft); font-weight: 700;
  margin-bottom: 18px;
}
.an-clue b { color: var(--c3); font-size: 15px; }
.an-tiles { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-bottom: 24px; }
.an-tile {
  width: 2.1em; height: 2.1em;
  border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font: inherit; font-weight: 800; color: var(--ink); cursor: pointer;
}
.an-tile:active { transform: scale(.92); }
.an-tile.sel { outline: 3px solid var(--c3); transform: scale(1.06); }
.an-tiles.ok .an-tile { background: #d9ebe5; color: #4f8578; }
.an-check { display: block; margin: 0 auto; }

/* ---------- urutan pola ---------- */
.pt-row { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin: 18px 0 26px; }
.pt-chip {
  min-width: 2.4em; height: 2.4em; padding: 0 8px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); box-shadow: var(--shadow); border-radius: var(--r-md);
  font-weight: 800; font-size: 20px; color: var(--ink);
}
.pt-chip.blank { border: 2px dashed var(--c4); background: rgba(255,253,249,.7); color: var(--ink-soft); }
.pt-chip.blank.typing { border-style: solid; border-color: var(--c4); color: var(--ink); }
.pt-chip.blank.ok { background: #d9ebe5; border-color: var(--good); color: #4f8578; }
.pt-chip.blank.shake { animation: shake .38s ease; }

/* ---------- rotasi bentuk ---------- */
.rt-target-wrap { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-bottom: 22px; }
.rt-deg { font-weight: 800; font-size: 15px; color: var(--c1); }
.rt-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.rt-opt {
  border: none; border-radius: var(--r-md); padding: 10px; min-width: 0;
  background: var(--surface); box-shadow: var(--shadow); cursor: pointer;
}
.rt-opt:active { transform: scale(.96); }
.rt-opt:disabled { cursor: default; }
.rt-grid { display: grid; gap: 3px; width: 100px; height: 100px; max-width: 100%; margin: 0 auto; }
.rt-target-grid { width: 96px; height: 96px; }
.rt-cell { background: rgba(255,253,249,.6); border: 1px solid var(--line); border-radius: 3px; }
.rt-cell.filled { background: var(--c1); border-color: var(--c1); }

/* ---------- bayangan cermin ---------- */
.mr-axis {
  text-align: center; font-weight: 800; font-size: 13px; color: var(--c2);
  margin-bottom: 14px; text-transform: uppercase; letter-spacing: .4px;
}
.mr-grid { display: grid; gap: 3px; margin: 0 auto 20px; width: min(100%, 320px); aspect-ratio: 1; }
.mr-cell { border: 1px solid var(--line); border-radius: 3px; background: rgba(255,253,249,.5); }
.mr-cell.given.filled { background: var(--c2); border-color: var(--c2); }
.mr-cell.blank { background: rgba(255,253,249,.85); cursor: pointer; }
.mr-cell.blank.filled { background: var(--c8); border-color: var(--c8); }
.mr-cell.blank.hinted { outline: 2px dashed var(--star); }
.mr-grid.ok .mr-cell.blank.filled { background: var(--good); border-color: var(--good); }
.mr-grid.shake { animation: shake .38s ease; }
/* the mirror line itself, so the axis is never just implied by a label */
.mr-grid.mr-axis-v { border-left: 3px solid var(--c2); padding-left: 2px; }
.mr-grid.mr-axis-h { border-top: 3px solid var(--c2); padding-top: 2px; }

/* ---------- jaring kubus ---------- */
.cn-net-wrap { display: flex; justify-content: center; margin: 10px 0 26px; }
.cn-net { display: grid; grid-template-columns: repeat(4, 44px); grid-template-rows: repeat(3, 44px); gap: 4px; }
.cn-sq { border-radius: 6px; box-shadow: var(--shadow); }
.cn-opts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.cn-opt {
  border: none; border-radius: var(--r-md); padding: 6px;
  background: var(--surface); box-shadow: var(--shadow); cursor: pointer;
}
.cn-opt:active { transform: scale(.96); }
.cn-opt:disabled { cursor: default; }
.cn-cube { width: 100%; height: auto; display: block; }

/* ---------- puzzle potong ---------- */
.tg-board {
  display: grid; gap: 3px; margin: 0 auto 18px; width: min(100%, 300px);
  /* aspect-ratio is set inline per level to match the silhouette's shape */
}
.tg-cell { border-radius: 3px; }
.tg-cell.inside {
  border: 2px dashed var(--line); background: rgba(255,253,249,.5); cursor: pointer;
}
.tg-cell.inside.filled { border-style: solid; }
.tg-board.ok .tg-cell.inside { border-color: var(--good); }
.tg-board.shake { animation: shake .38s ease; }
.tg-tray {
  display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;
  padding-top: 6px; border-top: 2px dashed var(--line);
}
.tg-piece {
  border: none; border-radius: var(--r-sm); padding: 6px;
  background: var(--surface); box-shadow: var(--shadow); cursor: pointer;
}
.tg-piece.sel { outline: 3px solid var(--c4); transform: scale(1.06); }
.tg-mini { display: grid; gap: 2px; width: 52px; height: 52px; }
.tg-mc { border-radius: 3px; }


/* ==========================================================================
   Paket C/D — Jalan Bayangan, Tirukan Nada, Ingat Tempatnya
   ========================================================================== */

/* ---------- jalan bayangan ----------
   Two mazes have to sit side by side even on the narrowest phone, so the pair
   is a 2-column grid of equal fractions and each maze scales to its column
   rather than to a fixed pixel size. */
.sm-pair {
  display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
  width: 100%; margin-bottom: 12px;
}
.sm-side { min-width: 0; }
.sm-label {
  text-align: center; font-size: 11px; font-weight: 800; letter-spacing: .4px;
  text-transform: uppercase; color: var(--c5); margin-bottom: 5px;
}
.sm-label-b { color: var(--ink-soft); }
.sm-grid { display: grid; width: 100%; aspect-ratio: 1; background: var(--surface); border-radius: var(--r-sm); overflow: hidden; }
.sm-grid-b { opacity: .93; }
.sm-cell { position: relative; background: transparent; }
/* walls are drawn as insets so neighbouring cells share one crisp line */
.sm-cell.wn { box-shadow: inset 0 2px 0 var(--ink); }
.sm-cell.we { box-shadow: inset -2px 0 0 var(--ink); }
.sm-cell.ws { box-shadow: inset 0 -2px 0 var(--ink); }
.sm-cell.ww { box-shadow: inset 2px 0 0 var(--ink); }
.sm-cell.wn.we { box-shadow: inset 0 2px 0 var(--ink), inset -2px 0 0 var(--ink); }
.sm-cell.wn.ws { box-shadow: inset 0 2px 0 var(--ink), inset 0 -2px 0 var(--ink); }
.sm-cell.wn.ww { box-shadow: inset 0 2px 0 var(--ink), inset 2px 0 0 var(--ink); }
.sm-cell.we.ws { box-shadow: inset -2px 0 0 var(--ink), inset 0 -2px 0 var(--ink); }
.sm-cell.we.ww { box-shadow: inset -2px 0 0 var(--ink), inset 2px 0 0 var(--ink); }
.sm-cell.ws.ww { box-shadow: inset 0 -2px 0 var(--ink), inset 2px 0 0 var(--ink); }
.sm-cell.wn.we.ws { box-shadow: inset 0 2px 0 var(--ink), inset -2px 0 0 var(--ink), inset 0 -2px 0 var(--ink); }
.sm-cell.wn.we.ww { box-shadow: inset 0 2px 0 var(--ink), inset -2px 0 0 var(--ink), inset 2px 0 0 var(--ink); }
.sm-cell.wn.ws.ww { box-shadow: inset 0 2px 0 var(--ink), inset 0 -2px 0 var(--ink), inset 2px 0 0 var(--ink); }
.sm-cell.we.ws.ww { box-shadow: inset -2px 0 0 var(--ink), inset 0 -2px 0 var(--ink), inset 2px 0 0 var(--ink); }
.sm-cell.wn.we.ws.ww { box-shadow: inset 0 2px 0 var(--ink), inset -2px 0 0 var(--ink), inset 0 -2px 0 var(--ink), inset 2px 0 0 var(--ink); }
.sm-cell.goal::after {
  content: '⚑'; position: absolute; inset: 0;
  display: grid; place-items: center; font-size: 13px; color: var(--warn);
}
.sm-cell.hero::before {
  content: ''; position: absolute; inset: 22%;
  border-radius: 50%; background: var(--c5); box-shadow: var(--shadow);
}
.sm-grid-b .sm-cell.hero::before { background: var(--ink-soft); }
.sm-cell.hero.home::before { background: var(--good); }
.sm-cell.flash { animation: smflash .5s ease 3; }
@keyframes smflash { 50% { background: var(--star); } }
.sm-hud { text-align: center; font-size: 12px; font-weight: 800; color: var(--ink-soft); margin-bottom: 10px; }
.sm-hud b, .sm-hud span { color: var(--ink); }
.sm-pad { display: flex; flex-direction: column; align-items: center; gap: 7px; }
.sm-arrow-row { display: flex; gap: 7px; }
.sm-arrow {
  width: 54px; height: 46px; border: none; border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow);
  font-size: 19px; color: var(--ink); cursor: pointer;
}
.sm-arrow:active { transform: scale(.92); }

/* ---------- tirukan nada ---------- */
.si-hud { display: flex; justify-content: space-between; align-items: center; font-size: 12px; font-weight: 800; color: var(--ink-soft); }
.si-lives { letter-spacing: 2px; }
.si-status { text-align: center; font-weight: 800; font-size: 15px; color: var(--ink); margin: 14px 0 20px; min-height: 1.4em; }
.si-pads { display: grid; gap: 12px; margin: 0 auto; width: min(100%, 300px); }
.si-four { grid-template-columns: 1fr 1fr; }
.si-six  { grid-template-columns: repeat(3, 1fr); }
.si-pad {
  aspect-ratio: 1; border: none; border-radius: var(--r-md);
  box-shadow: var(--shadow); cursor: pointer; opacity: .55;
  transition: opacity .12s ease, transform .12s ease;
}
.si-pad.lit { opacity: 1; transform: scale(1.06); box-shadow: var(--shadow-lg); }
.si-pad:active { opacity: 1; transform: scale(.95); }
.si-pads.shake { animation: shake .38s ease; }

/* ---------- ingat tempatnya ---------- */
.rc-status { text-align: center; font-weight: 800; font-size: 15px; color: var(--ink); margin-bottom: 16px; min-height: 1.4em; }
.rc-grid { display: grid; gap: 8px; margin: 0 auto; width: min(100%, 320px); aspect-ratio: 1; }
.rc-cell {
  border: none; border-radius: var(--r-sm);
  background: var(--surface); box-shadow: var(--shadow);
  font-size: 26px; line-height: 1; cursor: pointer; padding: 0;
  display: grid; place-items: center;
}
/* the icon stays in the DOM and is only made invisible, so the recall phase
   cannot be beaten by reading the markup. `color: transparent` on the cell
   does nothing here — emoji are colour glyphs baked into the font, not text
   painted with the CSS `color` you set, so the icon stayed visible through
   the whole memorize phase no matter what color was applied to it. opacity
   on the icon's own inner span actually hides it, and only the icon: the
   cell's own background/border (and its clickability) are untouched. */
.rc-cell.hidden-item .rc-icon { opacity: 0; }
.rc-cell.got { background: #d9ebe5; }
.rc-cell.got .rc-icon { opacity: 1; }
.rc-cell.miss { background: #f7e3d1; opacity: .6; }
.rc-cell.flash { animation: smflash .4s ease 3; }
.rc-cell:active { transform: scale(.94); }


/* ==========================================================================
   Paket E/F — Sortir Warna, Detektif Mini, Teka-teki Zebra
   ========================================================================== */

/* ---------- sortir warna ---------- */
.ws-tubes {
  display: flex; flex-wrap: wrap; gap: 14px;
  justify-content: center; align-items: flex-end; padding-top: 10px;
}
.ws-tube {
  display: flex; flex-direction: column; gap: 3px;
  padding: 5px; border: none;
  background: var(--surface); box-shadow: var(--shadow);
  border-radius: 8px 8px 16px 16px; cursor: pointer;
}
.ws-tube.sel { outline: 3px solid var(--c8); transform: translateY(-8px); }
.ws-tube.flash { animation: smflash .5s ease 3; }
.ws-tube.shake { animation: shake .38s ease; }
.ws-slot {
  width: 28px; height: 28px; border-radius: 5px;
  background: rgba(230, 217, 198, .35);
}
.ws-slot.full { box-shadow: inset 0 -2px 0 rgba(0,0,0,.08); }
.ws-tubes.ok .ws-tube { outline: 2px solid var(--good); }

/* ---------- detektif & zebra: shared clue list and chips ---------- */
.dt-clues {
  background: var(--surface); border-radius: var(--r-md);
  box-shadow: var(--shadow); padding: 12px 14px; margin-bottom: 18px;
}
.dt-clue {
  font-size: 13px; line-height: 1.55; color: var(--ink);
  padding-left: 16px; position: relative; margin-bottom: 6px;
}
.dt-clue:last-child { margin-bottom: 0; }
.dt-clue::before {
  content: '•'; position: absolute; left: 3px; color: var(--c3); font-weight: 800;
}
.dt-chip {
  width: 2.2em; height: 2.2em; font-size: 22px; line-height: 1;
  border: none; border-radius: var(--r-sm);
  background: var(--surface); box-shadow: var(--shadow); cursor: pointer;
}
.dt-chip.sel { outline: 3px solid var(--c3); transform: scale(1.08); }
.dt-chip:active { transform: scale(.92); }

/* ---------- detektif mini ---------- */
.dt-houses { display: grid; gap: 8px; margin-bottom: 16px; }
.dt-house {
  aspect-ratio: 3 / 4; border: 2px dashed var(--line); border-radius: var(--r-sm);
  background: rgba(255,253,249,.6); cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
}
.dt-house.filled { border-style: solid; background: var(--surface); box-shadow: var(--shadow); }
.dt-house.hinted { border-color: var(--star); }
.dt-slot { font-size: 26px; line-height: 1; min-height: 1.1em; }
.dt-num { font-size: 11px; font-weight: 800; color: var(--ink-soft); }
.dt-houses.ok .dt-house { border-color: var(--good); }
.dt-houses.shake { animation: shake .38s ease; }
.dt-tray { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-bottom: 18px; }

/* ---------- teka-teki zebra ---------- */
.zb-grid { display: grid; gap: 6px; margin-bottom: 16px; align-items: center; }
.zb-hdr {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase;
  letter-spacing: .3px; color: var(--ink-soft); text-align: center;
}
.zb-who { font-size: 24px; text-align: center; }
.zb-cell {
  aspect-ratio: 1; border: 2px dashed var(--line); border-radius: var(--r-sm);
  background: rgba(255,253,249,.6); font-size: 24px; line-height: 1;
  cursor: pointer; display: grid; place-items: center; padding: 0;
}
.zb-cell.filled { border-style: solid; background: var(--surface); box-shadow: var(--shadow); }
.zb-cell.hinted { border-color: var(--star); }
.zb-grid.ok .zb-cell { border-color: var(--good); }
.zb-grid.shake { animation: shake .38s ease; }
.zb-trays { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.zb-tray { display: flex; gap: 8px; justify-content: center; }


/* ==========================================================================
   Paket G — Cari Kata Tersulit, Labirin Monster
   ========================================================================== */

/* ---------- cari kata tersulit ---------- */
.hs-bar {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; flex-wrap: wrap; margin-bottom: 6px;
}
.hs-target { flex: 1 1 auto; min-width: 0; }
.hs-target { font-size: 14px; font-weight: 800; color: var(--ink-soft); }
.hs-target b { color: var(--ink); font-size: 17px; letter-spacing: 2px; }
.hs-time {
  font-size: 17px; font-weight: 800; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.hs-time.low { color: var(--warn); }
.hs-note { font-size: 11.5px; color: var(--ink-soft); line-height: 1.5; margin-bottom: 12px; }
.hs-grid {
  display: grid; gap: 1px; width: 100%; aspect-ratio: 1;
  background: var(--surface); border-radius: var(--r-sm);
  padding: 4px; box-shadow: var(--shadow);
  user-select: none;
}
.hs-cell {
  display: grid; place-items: center;
  font-weight: 800; color: var(--ink); border-radius: 2px;
  line-height: 1; cursor: pointer;
}
.hs-cell.sel   { background: var(--c2); color: #fff; }
.hs-cell.found { background: var(--good); color: #fff; }
.hs-cell.flash { animation: smflash .5s ease 3; }
.hs-grid.shake { animation: shake .3s ease; }

/* ---------- labirin monster (chip's-challenge style) ---------- */
.mo-hud {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  font-size: 12px; font-weight: 800; color: var(--ink-soft); margin-bottom: 10px;
}
.mo-hud span#mo-moves { color: var(--ink); }
.mo-keys { display: flex; gap: 5px; }
.mo-key { width: 20px; height: 20px; display: block; }
/* a key you have not found yet is drawn faint, so the HUD doubles as a checklist */
.mo-key { opacity: .2; filter: grayscale(1); }
.mo-key.have { opacity: 1; filter: none; }

/* minmax(0,1fr) is load-bearing: a bare 1fr is minmax(AUTO,1fr), so any content
   taller than its track stretches the row. That is why the emoji version came
   out 27px wide by 25px tall instead of square. */
.mo-grid {
  display: grid; gap: 1px; width: min(100%, 380px); aspect-ratio: 1;
  margin: 0 auto 14px; background: var(--line);
  border-radius: var(--r-sm); overflow: hidden; padding: 1px;
}
.mo-cell {
  background: var(--surface); position: relative;
  display: block; min-width: 0; min-height: 0; overflow: hidden;
}
/* Absolute, not height:100%. A percentage height needs a parent with a
   DEFINITE height, and a stretched grid item does not reliably provide one —
   the sprites collapsed to 22x2px on the 13x13 board while looking fine on the
   9x9. Pinning to the cell's edges removes the dependency entirely. */
.mo-svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.mo-cell.wall { background: var(--ink); }
.mo-cell.goal { background: #fbf3dd; }
.mo-cell.hero { background: #e2efe9; }
.mo-cell.hero.caught { background: var(--warn); }
.mo-cell.dark { background: #ece3d4; }
.mo-cell.flash { animation: smflash .5s ease 3; }
.mo-cell.shake { animation: shake .3s ease; }

/* ---------- tali melingkar (slitherlink) ----------
   The board is one grid of 2n+1 tracks: even tracks hold the dots, odd tracks
   are the gaps between them, so dots / edges / clue numbers all land in place
   without absolute positioning. */
/* width:100% is required, not cosmetic: the play stage centres its children,
   so without it this wrapper shrinks to its content and the board rendered
   159px wide on a 375px phone — the "kecil banget" Bena reported. */
.sl-wrap { display: flex; justify-content: center; width: 100%; margin-bottom: 18px; }
.sl-grid {
  display: grid; width: 100%; aspect-ratio: 1;
  background: var(--surface); border-radius: var(--r-md);
  padding: 12px; box-shadow: var(--shadow);
}
/* the wrapper carries the cap, not the grid, so the board fills the phone
   width instead of sitting as a small stamp in the middle of the screen */
.sl-wrap > .sl-grid { max-width: 460px; }
.sl-dot { border-radius: 50%; background: var(--ink-soft); opacity: .6; }
.sl-cell {
  display: grid; place-items: center;
  font-weight: 800; font-size: 17px; color: var(--ink);
}
/* generous tap targets: the visible line is thin but the hit area is the
   whole track, which matters a lot on a phone */
.sl-edge { position: relative; cursor: pointer; }
.sl-edge::after {
  content: ''; position: absolute; background: var(--line); border-radius: 3px;
}
.sl-h::after { left: 0; right: 0; top: 50%; height: 3px; transform: translateY(-50%); }
.sl-v::after { top: 0; bottom: 0; left: 50%; width: 3px; transform: translateX(-50%); }
.sl-edge.on::after { background: var(--c6); }
.sl-h.on::after { height: 6px; }
.sl-v.on::after { width: 6px; }
.sl-edge.hinted::after { background: var(--star); }
.sl-grid.ok .sl-edge.on::after { background: var(--good); }
.sl-grid.shake { animation: shake .38s ease; }


/* ---------- tantangan (challenge result) ---------- */


/* ---------- challenge intro ---------- */
.ci-face { font-size: 54px; line-height: 1; margin-bottom: 6px; }


/* ---------- cut & fit: drag to place, tap to rotate ---------- */
.tg-hint-line {
  text-align: center; font-size: 11.5px; font-weight: 700;
  color: var(--ink-soft); margin-bottom: 12px;
}
.tg-piece { touch-action: none; }        /* let pointermove drag instead of scroll */
/* the piece follows the finger, offset up-left so it is not under the thumb */
.tg-ghost {
  position: fixed; pointer-events: none; z-index: 60;
  transform: translate(-50%, -130%); opacity: .9;
  filter: drop-shadow(0 6px 10px rgba(120,100,80,.3));
}
.tg-cell.preview     { background: rgba(111,174,155,.35); border-style: solid; }
.tg-cell.preview-bad { background: rgba(221,154,106,.3); border-style: solid; }


/* ---------- tebak rebus ----------
   The struck-out letter is the whole point of the puzzle, so the line through
   it has to be unmistakable — a thin text-decoration reads as a rendering
   glitch at this size. */
.rb-clue {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 8px; margin: 14px 0 26px;
}
.rb-pic { font-size: 44px; line-height: 1; }
.rb-op { font-size: 26px; font-weight: 800; color: var(--ink-soft); }
.rb-letter {
  position: relative;
  display: inline-grid; place-items: center;
  min-width: 1.5em; padding: 2px 6px;
  background: var(--surface); border-radius: var(--r-sm); box-shadow: var(--shadow);
  font-size: 26px; font-weight: 800; color: var(--ink); letter-spacing: 1px;
}
.rb-cut { color: var(--warn); }
.rb-cut::after {
  content: ''; position: absolute; left: 8%; right: 8%; top: 50%;
  height: 3px; background: var(--warn); border-radius: 2px;
  transform: rotate(-14deg);
}
.rb-q {
  font-size: 30px; font-weight: 800; color: var(--c2);
  min-width: 1.2em; text-align: center;
}


/* ---------- sirkuit lampu ----------
   Restored after being destroyed by a marker-based slice edit: this block sat
   between the monster and slitherlink markers, so replacing "everything from
   monster to slitherlink" silently took it with it and the game rendered as a
   blank screen. Never slice CSS by neighbouring section markers again. */
.li-hud { text-align: center; font-size: 12px; font-weight: 800; color: var(--ink-soft); margin-bottom: 14px; }
.li-hud span { color: var(--ink); }
.li-grid {
  display: grid; gap: 8px; width: min(100%, 300px); aspect-ratio: 1; margin: 0 auto;
  grid-auto-rows: minmax(0, 1fr);
}
.li-cell {
  border: none; border-radius: var(--r-md); padding: 0; min-width: 0; min-height: 0;
  background: #ded3c2; box-shadow: var(--shadow); cursor: pointer;
  transition: background .12s ease, transform .1s ease;
}
.li-cell.on { background: var(--star); box-shadow: 0 0 0 2px rgba(237,195,92,.35), var(--shadow); }
.li-cell:active { transform: scale(.93); }
.li-cell.flash { animation: smflash .5s ease 3; }
.li-grid.ok .li-cell { background: var(--good); }


/* a rule reminder that stays on the board — the how-to modal is seen once and
   Slitherlink is the one game whose goal is not guessable from looking at it */
.sl-tip {
  font-size: 11.5px; line-height: 1.5; color: var(--ink-soft);
  text-align: center; margin-bottom: 12px;
}

/* hazard tiles fill their cell edge to edge, so a pool reads as terrain
   rather than as an object sitting on the floor */
.mo-cell.hazard { background: transparent; }


/* ---------- Bridges (Hashiwokakero) ---------- */
.hs-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 22px;
  width: 100%; height: 100%; padding: 4px 18px 0;
}
.hs-tip {
  font-size: 12px; font-weight: 700; color: var(--ink-soft);
  text-align: center; line-height: 1.4;
}
.hs-board { position: relative; width: 100%; max-width: min(100%, 390px); }
.hs-board::before {
  content: ''; position: absolute; inset: -14px; z-index: 0;
  background: var(--surface); border: 3px solid var(--ink-line);
  border-radius: var(--r-md); box-shadow: var(--shadow);
}
.hs-lines {
  position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1;
  pointer-events: none; overflow: visible;
}
.hs-lines line {
  stroke: var(--ink-line); stroke-width: .07; stroke-linecap: round;
}
.hs-board.ok .hs-lines line { stroke: var(--good); }
.hs-node {
  position: absolute; border-radius: 50%; padding: 0;
  background: var(--surface); border: 2.5px solid var(--ink-line);
  color: var(--ink); font: inherit; font-weight: 800; line-height: 1;
  display: grid; place-items: center; cursor: pointer; z-index: 2;
}
.hs-node.full { background: var(--bg-soft); color: var(--ink-soft); }
.hs-node.over { background: var(--warn); color: #fff; }
.hs-node.sel  { background: var(--c1); color: #fff; transform: scale(1.12); }
.hs-node.bump { animation: hsbump .3s ease; }
@keyframes hsbump { 0%,100%{transform:scale(1)} 50%{transform:scale(1.18)} }
.hs-board.ok .hs-node { border-color: var(--good); }

/* ---------- Battleships (Bimaru) ---------- */
.bm-wrap { display: flex; flex-direction: column; align-items: center; gap: 10px;
  width: 100%; height: 100%; }
.bm-fleet {
  font-size: 12.5px; font-weight: 800; color: var(--ink-soft);
  background: var(--bg-soft); border-radius: 999px; padding: 5px 14px;
}
.bm-grid {
  display: grid; gap: 3px; width: 100%; max-width: min(100%, 420px);
}
.bm-corner { aspect-ratio: 1; }
.bm-count {
  aspect-ratio: 1; display: grid; place-items: center;
  font-size: 13px; font-weight: 800; color: var(--ink-soft);
}
.bm-count.met  { color: var(--good); }
.bm-count.bust { color: var(--warn); }
.bm-cell {
  aspect-ratio: 1; padding: 0; border-radius: 6px;
  background: var(--surface); border: 2px solid var(--line);
  cursor: pointer; position: relative;
}
.bm-cell.ship  { background: var(--ink-line); border-color: var(--ink-line); }
.bm-cell.water { background: var(--bg-soft); border-color: var(--line); }
/* water reads as a dot, so an untouched square is never mistaken for it */
.bm-cell.water::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 22%; height: 22%; margin: -11% 0 0 -11%;
  border-radius: 50%; background: var(--ink-soft); opacity: .55;
}
.bm-cell.given { box-shadow: inset 0 0 0 2px var(--c4); }
.bm-grid.ok .bm-cell.ship { background: var(--good); border-color: var(--good); }

/* ---------- Killer Sudoku ---------- */
.ks-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: 100%; height: 100%; }
.ks-grid {
  display: grid; width: 100%; max-width: min(100%, 420px);
  background: var(--surface); border: 3px solid var(--ink-line); border-radius: 3px;
}
.ks-cell {
  position: relative; aspect-ratio: 1; padding: 0; border: none; background: transparent;
  cursor: pointer; display: grid; place-items: center;
  font: inherit; font-weight: 700; color: var(--c1);
  /* the pale sudoku lattice; heavier box lines and cage dashes sit on top */
  box-shadow: inset -1px -1px 0 var(--line);
}
.ks-grid.n9 .ks-cell { font-size: 15px; }
.ks-grid.n6 .ks-cell { font-size: 18px; }
.ks-cell.given { color: var(--ink); font-weight: 900; }
.ks-cell.sel { background: rgba(63,125,116,.16); }
.ks-cell.bad { color: var(--warn); }
.ks-cell.cagedone .ks-v { color: var(--good); }
.ks-grid.ok .ks-cell { color: var(--good); }

/* sudoku box rules — drawn as real borders so they read heavier than the cage dashes */
.ks-cell.bt { border-top: 2px solid var(--ink-line); }
.ks-cell.bl { border-left: 2px solid var(--ink-line); }
.ks-cell.bb { border-bottom: 2px solid var(--ink-line); }
.ks-cell.br { border-right: 2px solid var(--ink-line); }

/* cage outline: an inset dashed rectangle, so it never fights the box rules */
.ks-cell::before {
  content: ''; position: absolute; inset: 2px; pointer-events: none;
  border: 0 dashed var(--c3);
}
.ks-cell.ct::before { border-top-width: 1.5px; }
.ks-cell.cl::before { border-left-width: 1.5px; }
.ks-cell.cb::before { border-bottom-width: 1.5px; }
.ks-cell.cr::before { border-right-width: 1.5px; }

.ks-sum {
  position: absolute; top: 2px; left: 4px; font-style: normal;
  font-size: 9px; font-weight: 900; color: var(--c3); line-height: 1;
  letter-spacing: -.02em;
}
.ks-grid.n6 .ks-sum { font-size: 12px; }
.ks-v { line-height: 1; padding-top: 5px; }

.ks-pad { display: grid; gap: 6px; width: 100%; max-width: min(100%, 360px); }
.ks-key {
  padding: 12px 0; border: none; border-radius: var(--r-sm);
  background: var(--surface); box-shadow: 0 3px 0 var(--line);
  font: inherit; font-size: 18px; font-weight: 800; color: var(--ink); cursor: pointer;
}
.ks-key:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--line); }
.ks-key.del { background: var(--bg-soft); }

/* ---------- Sudoku variants (Thermo / Arrow / Sandwich) ----------
   One layout for all three. The wrapper takes the full stage width on
   purpose: .play-stage centres its children, so a wrapper without a width
   collapses to its content and every percentage inside it measures against
   nothing — the sizing bug that shipped three times before this. */
.sv-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  width: 100%; height: 100%; padding: 4px 14px 0;
}
.sv-tip {
  font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  text-align: center; line-height: 1.4; max-width: 46ch;
}
/* the plain panel that keeps the board off the stage's paper texture */
.sv-panel {
  position: relative; width: 100%; max-width: min(100%, 400px);
  background: var(--surface); border: 3px solid var(--ink-line);
  border-radius: var(--r-md); box-shadow: var(--shadow); padding: 6px;
}
.sv-grid { display: grid; width: 100%; position: relative; z-index: 1; }
.sv-cell {
  position: relative; aspect-ratio: 1; padding: 0; border: none;
  background: transparent; cursor: pointer; display: grid; place-items: center;
  font: inherit; font-weight: 700; color: var(--c1);
  box-shadow: inset -1px -1px 0 var(--line);
}
.sv-grid.n9 .sv-cell { font-size: 16px; }
.sv-grid.n6 .sv-cell { font-size: 19px; }
.sv-cell.given { color: var(--ink); font-weight: 900; }
.sv-cell.sel { background: rgba(63,125,116,.16); }
.sv-cell.bad { color: var(--warn); }
.sv-grid.ok .sv-cell { color: var(--good); }
.sv-cell.bt { border-top: 2px solid var(--ink-line); }
.sv-cell.bl { border-left: 2px solid var(--ink-line); }
.sv-cell.bb { border-bottom: 2px solid var(--ink-line); }
.sv-cell.br { border-right: 2px solid var(--ink-line); }
.sv-v { line-height: 1; position: relative; z-index: 2; }

/* Clue art normally sits UNDER the digits — thermo tubes and arrow lines are
   large translucent shapes and must not wash the numbers out. Kropki is the
   exception: its dots straddle the wall between two squares, and the cells'
   own heavy box borders were painting straight over them, so that variant
   lifts the art above the grid instead. */
.sv-panel.art-top .sv-grid { z-index: 0; }
.sv-panel.art-top .sv-art  { z-index: 1; }
.sv-art {
  position: absolute; left: 6px; top: 6px;
  width: calc(100% - 12px); height: calc(100% - 12px);
  pointer-events: none; z-index: 0; overflow: visible;
}

.sv-pad { display: grid; gap: 6px; width: 100%; max-width: min(100%, 360px); }
.sv-key {
  padding: 12px 0; border: none; border-radius: var(--r-sm);
  background: var(--surface); box-shadow: 0 3px 0 var(--line);
  font: inherit; font-size: 18px; font-weight: 800; color: var(--ink); cursor: pointer;
}
.sv-key:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--line); }
.sv-key.del { background: var(--bg-soft); }

/* thermo: a bulb and a fat track the digits must climb */
.sv-thermo { stroke: rgba(47,42,38,.22); stroke-width: .34; fill: none;
  stroke-linecap: round; stroke-linejoin: round; }
.sv-bulb { fill: rgba(47,42,38,.22); }

/* arrow: circle holds the total, the tail carries the digits that make it */
.sv-arrowline { stroke: rgba(74,111,165,.5); stroke-width: .07; fill: none;
  stroke-linecap: round; stroke-linejoin: round; }
.sv-arrowhead { fill: rgba(74,111,165,.5); }
.sv-circle { fill: none; stroke: rgba(74,111,165,.55); stroke-width: .07; }

/* sandwich: clues live outside the grid, so the panel gets room for them */
.sv-panel.has-edge { padding: 6px 6px 6px 30px; }
.sv-panel.has-edge .sv-art { left: 30px; width: calc(100% - 36px); }
.sv-edge { position: absolute; font-weight: 900; color: var(--c3); font-size: 12px; }
.sv-edge.left { left: 6px; width: 22px; text-align: center; }
.sv-edge.top  { top: 6px; height: 22px; line-height: 22px; text-align: center; }
.sv-panel.has-edge { padding-top: 30px; }
.sv-panel.has-edge .sv-art { top: 30px; height: calc(100% - 36px); }

/* ---------- Knights & Knaves ---------- */
.kn-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: 100%; height: 100%; padding: 4px 14px 0;
}
.kn-tip {
  font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  text-align: center; line-height: 1.4; max-width: 46ch;
}
/* a plain card, so the statements are not read against the stage texture */
.kn-panel {
  width: 100%; max-width: min(100%, 420px);
  background: var(--surface); border: 3px solid var(--ink-line);
  border-radius: var(--r-md); box-shadow: var(--shadow);
  padding: 6px; display: flex; flex-direction: column; gap: 6px;
}
.kn-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 11px; border-radius: var(--r-sm); background: var(--bg-soft);
}
.kn-who { flex: 1; min-width: 0; }
.kn-name { display: block; font-weight: 900; font-size: 14.5px; color: var(--ink); }
.kn-said { display: block; font-size: 13px; color: var(--ink-soft); line-height: 1.45; }
.kn-pick { display: flex; gap: 6px; flex: none; }
.kn-btn {
  border: 2px solid var(--line); border-radius: 999px;
  padding: 7px 13px; font: inherit; font-size: 12.5px; font-weight: 800;
  background: var(--surface); color: var(--ink-soft); cursor: pointer;
}
.kn-btn.kn-t.on { background: var(--good); border-color: var(--good); color: #fff; }
.kn-btn.kn-f.on { background: var(--c3);  border-color: var(--c3);  color: #fff; }
.kn-panel.ok .kn-row { background: rgba(79,143,120,.16); }

/* kropki dots sit on the wall between two squares */
.kr-dot { stroke: var(--ink-line); stroke-width: .035; }
.kr-w { fill: var(--surface); }
.kr-b { fill: var(--ink-line); }

/* ---------- Tents & Trees ---------- */
.tn-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: 100%; height: 100%; padding: 4px 14px 0;
}
.tn-tip {
  font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  text-align: center; line-height: 1.4; max-width: 46ch;
}
.tn-panel {
  width: 100%; max-width: min(100%, 400px);
  background: var(--surface); border: 3px solid var(--ink-line);
  border-radius: var(--r-md); box-shadow: var(--shadow); padding: 6px;
}
.tn-grid { display: grid; gap: 2px; width: 100%; }
.tn-cell {
  aspect-ratio: 1; display: grid; place-items: center;
  font-size: 15px; line-height: 1; padding: 0; border: none;
}
.tn-corner { background: transparent; }
.tn-count {
  aspect-ratio: 1; display: grid; place-items: center;
  font-size: 13px; font-weight: 800; color: var(--ink-soft);
}
.tn-count.met  { color: var(--good); }
.tn-count.bust { color: var(--warn); }
.tn-tree { background: var(--bg-soft); border-radius: 5px; }
.tn-plot {
  background: var(--surface); border: 2px solid var(--line);
  border-radius: 5px; cursor: pointer;
}
/* grass is a dot, so an untouched square never reads as "ruled out" */
.tn-plot.grass { background: var(--bg-soft); position: relative; }
.tn-plot.grass::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 20%; height: 20%; margin: -10% 0 0 -10%;
  border-radius: 50%; background: var(--ink-soft); opacity: .5;
}
.tn-grid.ok .tn-plot { border-color: var(--good); }

/* Skyscrapers: clues ring the grid on all four sides, so the panel is inset
   to make room and the numbers are positioned against it. */
.sv-panel.has-ring { padding: 28px; }
.sv-panel.has-ring .sv-art { left: 28px; top: 28px;
  width: calc(100% - 56px); height: calc(100% - 56px); }
.sv-ring {
  position: absolute; font-weight: 900; font-size: 13px; color: var(--c2);
  width: 22px; height: 20px; display: grid; place-items: center;
}

/* ---------- Dominosa ---------- */
.dm-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: 100%; height: 100%; padding: 4px 14px 0;
}
.dm-tip {
  font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  text-align: center; line-height: 1.4; max-width: 46ch;
}
.dm-panel {
  width: 100%; max-width: min(100%, 400px);
  background: var(--surface); border: 3px solid var(--ink-line);
  border-radius: var(--r-md); box-shadow: var(--shadow); padding: 7px;
}
.dm-grid { display: grid; gap: 3px; width: 100%; }
.dm-cell {
  aspect-ratio: 1; padding: 0; border: 2px solid var(--line);
  border-radius: 7px; background: var(--surface); cursor: pointer;
  font: inherit; font-weight: 800; font-size: 16px; color: var(--ink);
}
.dm-cell.sel { background: rgba(63,125,116,.18); border-color: var(--c1); }
/* a joined pair loses the wall between its two halves, so it reads as one tile */
.dm-cell.joined { background: var(--bg-soft); border-color: var(--ink-line); }
.dm-cell.jr { border-right-color: transparent; border-top-right-radius: 0; border-bottom-right-radius: 0; }
.dm-cell.jl { border-left-color: transparent;  border-top-left-radius: 0;  border-bottom-left-radius: 0; }
.dm-cell.jd { border-bottom-color: transparent; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.dm-cell.ju { border-top-color: transparent;    border-top-left-radius: 0;    border-top-right-radius: 0; }
.dm-grid.ok .dm-cell.joined { background: rgba(79,143,120,.2); border-color: var(--good); }

/* ---- Suguru ------------------------------------------------------------
   Cage walls are drawn as thick borders on the cells that sit just inside a
   boundary, so a cage reads as one enclosed pen. The panel is a plain
   surface, so the board never blends into the page's checked background. */
.sg-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: 100%; height: 100%; padding: 4px 14px 0;
}
.sg-tip {
  font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  text-align: center; line-height: 1.4; max-width: 46ch;
}
.sg-panel {
  width: 100%; max-width: min(100%, 380px);
  background: var(--surface); border: 3px solid var(--ink-line);
  border-radius: var(--r-md); box-shadow: var(--shadow); padding: 8px;
}
.sg-grid { display: grid; gap: 0; width: 100%; }
.sg-cell {
  aspect-ratio: 1; padding: 0; cursor: pointer; font: inherit;
  font-weight: 800; font-size: 16px; color: var(--c1);
  background: var(--surface); border: 1px solid var(--line);
}
/* the four cage walls */
.sg-cell.wt { border-top-width: 3px;    border-top-color: var(--ink-line); }
.sg-cell.wl { border-left-width: 3px;   border-left-color: var(--ink-line); }
.sg-cell.wb { border-bottom-width: 3px; border-bottom-color: var(--ink-line); }
.sg-cell.wr { border-right-width: 3px;  border-right-color: var(--ink-line); }
.sg-cell.given { color: var(--ink); background: var(--bg-soft); cursor: default; }
.sg-cell.sel { background: rgba(63,125,116,.18); }
.sg-cell.bad { color: #c4796a; background: #f7dcd6; }
.sg-grid.ok .sg-cell { background: rgba(79,143,120,.18); }
.sg-pad { display: grid; gap: 6px; width: 100%; max-width: min(100%, 380px); }
.sg-key {
  padding: 11px 0; border: 2px solid var(--ink-line); border-radius: var(--r-sm);
  background: var(--surface); font: inherit; font-weight: 800; font-size: 17px;
  color: var(--ink); cursor: pointer; box-shadow: 0 2px 0 var(--ink-line);
}
.sg-key:active { transform: translateY(2px); box-shadow: none; }
.sg-key.del { color: var(--ink-soft); }

/* ---- Binairo ------------------------------------------------------------
   Two tokens rather than digits: the puzzle has no reading or arithmetic in
   it, and the art should not add any. Givens are the filled discs with a ring;
   the player's own marks are the same discs without one, so a glance separates
   what was handed to you from what you worked out. */
.bn-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: 100%; height: 100%; padding: 4px 14px 0;
}
.bn-tip {
  font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
  text-align: center; line-height: 1.4; max-width: 46ch;
}
.bn-panel {
  width: 100%; max-width: min(100%, 400px);
  background: var(--surface); border: 3px solid var(--ink-line);
  border-radius: var(--r-md); box-shadow: var(--shadow); padding: 7px;
}
.bn-grid { display: grid; gap: 2px; width: 100%; }
.bn-cell {
  aspect-ratio: 1; padding: 0; cursor: pointer; font: inherit;
  border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface);
  display: flex; align-items: center; justify-content: center;
}
.bn-cell > i {
  display: block; width: 68%; height: 68%; border-radius: 50%;
  background: transparent; transition: background .12s;
}
.bn-cell.a > i { background: var(--star); }
.bn-cell.b > i { background: var(--c2); }
/* a given is locked in — the heavier ring says "this one was not your doing" */
.bn-cell.given { background: var(--bg-soft); cursor: default; }
.bn-cell.given > i { box-shadow: 0 0 0 2px var(--ink-line) inset; }
.bn-cell.bad > i { background: #c4796a; }
.bn-cell.bad { background: #f7dcd6; }
.bn-grid.ok .bn-cell { border-color: var(--good); }

/* ---- Monthly Leaderboard ------------------------------------------------ */
.lead-face { position: relative; }
/* Green up, red down — compared against the standing the player last saw. */
.lead-arrow { font-size: 11px; margin-right: 4px; vertical-align: 1px; }
.lead-arrow.up { color: var(--good); }
.lead-arrow.down { color: #c4796a; }
.lg-title { font-size: 15px; font-weight: 900; }
