/* 30 Wordles — shared styles.
   Theme tokens switch via [data-theme] on <html>; theme.js sets it
   (persisted in localStorage, defaulting to the OS preference). */

:root {
  --bg: #ffffff;
  --surface: #f6f6f4;
  --text: #1c1c1e;
  --muted: #6e7076;
  --border: #e4e5e7;
  --hover: #ededeb;

  /* Wordle tile palette (light) */
  --correct: #6aaa64;
  --present: #c9b458;
  --absent: #787c7e;
  --on-color: #ffffff;
  --key-bg: #d3d6da;
  --key-text: #1c1c1e;
  --tile-border: #d3d6da;
  --tile-border-filled: #8a8d91;

  /* Bundle accent — carried letters */
  --accent: #1273b5;
  --accent-soft: rgba(18, 115, 181, 0.09);

  /* Colorblind (high-contrast) mark palette, Wordle's orange/blue */
  --cb-correct: #f5793a;
  --cb-present: #85c0f9;
}

[data-theme="dark"] {
  --bg: #121213;
  --surface: #1b1c1e;
  --text: #f1f2f4;
  --muted: #939599;
  --border: #2b2c2f;
  --hover: #26272a;

  /* Wordle tile palette (dark) */
  --correct: #538d4e;
  --present: #b59f3b;
  --absent: #3a3a3c;
  --on-color: #ffffff;
  --key-bg: #818384;
  --key-text: #121213;
  --tile-border: #3a3a3c;
  --tile-border-filled: #636467;

  --accent: #79c0e8;
  --accent-soft: rgba(121, 192, 232, 0.12);
}

* { box-sizing: border-box; }

html { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  transition: background-color 0.25s ease, color 0.25s ease;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

::selection { background: var(--accent-soft); }

/* ---------- Header / footer ---------- */

.site-header {
  max-width: 960px;
  margin: 0 auto;
  padding: 22px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.wordmark {
  font-weight: 700;
  font-size: 17px;
  letter-spacing: -0.01em;
  color: var(--text);
}
.wordmark span { color: var(--muted); font-weight: 500; }
.wordmark:hover { text-decoration: none; color: var(--accent); }

/* Round header buttons. NOTE: `.theme-toggle` is theme.js's binding hook
   (it queries that class) — style round header buttons with .circle-btn
   and never add .theme-toggle to any other button. */
.circle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.circle-btn:hover { color: var(--text); border-color: var(--muted); }
.circle-btn svg { width: 17px; height: 17px; }
.icon-moon { display: none; }
[data-theme="dark"] .icon-sun { display: none; }
[data-theme="dark"] .icon-moon { display: block; }

.header-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}
.circle-btn[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
.circle-btn[aria-pressed="true"]:hover { opacity: 0.85; }

/* Colorblind mode: marks swap green/yellow for orange/blue everywhere they
   carry meaning (tiles, keyboard, strips). */
[data-colorblind="on"] .tile.correct { background: var(--cb-correct); border-color: var(--cb-correct); }
[data-colorblind="on"] .tile.present { background: var(--cb-present); border-color: var(--cb-present); }
[data-colorblind="on"] .key.correct { background: var(--cb-correct); }
[data-colorblind="on"] .key.present { background: var(--cb-present); }
[data-colorblind="on"] .strip .slot.known { background: var(--cb-correct); border-color: var(--cb-correct); }

.site-footer {
  max-width: 960px;
  margin: 0 auto;
  padding: 28px 24px 40px;
  color: var(--muted);
  font-size: 13.5px;
}

/* ---------- Page scaffolding ---------- */

.page {
  max-width: 960px;
  margin: 0 auto;
  padding: 12px 24px 48px;
}
.page.narrow { max-width: 620px; }

.eyebrow {
  margin: 18px 0 6px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

h1 {
  margin: 0 0 10px;
  font-size: clamp(28px, 5vw, 40px);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.lede {
  margin: 0 0 28px;
  font-size: 18px;
  color: var(--muted);
  max-width: 56ch;
}

/* ---------- Home: day grid ---------- */

.hero { padding: 34px 0 8px; }

.day-grid {
  margin-top: 34px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
  gap: 12px;
}

.day-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 16px 15px;
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--text);
  transition: border-color 0.15s ease, transform 0.15s ease,
    background-color 0.15s ease;
}
.day-card:hover {
  text-decoration: none;
  transform: translateY(-2px);
  border-color: var(--muted);
}

.day-card .num {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.day-card .name { font-weight: 650; font-size: 15.5px; }
.day-card .blurb { font-size: 13.5px; color: var(--muted); }

.day-card.empty { color: var(--muted); background: transparent; cursor: default; }
.day-card.empty:hover { transform: none; border-color: var(--border); }

.day-card.active {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.day-card.active .num { color: var(--accent); }

/* ---------- Variant pages ---------- */

.rules {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 18px 20px;
  margin: 0 0 30px;
  background: var(--surface);
}
.rules h2 {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 650;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}
.rules ul { margin: 0; padding-left: 18px; }
.rules li { margin: 4px 0; font-size: 14.5px; }
.rules code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  background: var(--hover);
  border-radius: 5px;
  padding: 1px 6px;
}

/* ---------- Day nav & coming soon ---------- */

/* Every day page ends with a symmetric prev/next pair; the edge days keep
   an invisible ghost so the row stays balanced. Unreleased days are real
   pages too — the nav chains through them. */
.day-nav {
  margin-top: 44px;
  display: flex;
  gap: 10px;
}
.day-nav a,
.day-nav .ghost {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--muted);
  text-decoration: none;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.day-nav a:hover {
  border-color: var(--muted);
  color: var(--text);
}
.day-nav a:last-child { justify-content: flex-end; }
.day-nav .arrow { font-weight: 700; }
.day-nav .ghost { visibility: hidden; }

/* Coming-soon pages: five empty tiles waiting, breathing softly in turn.
   (prefers-reduced-motion flattens this along with everything else.) */
.soon-tiles {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  max-width: 350px;
  margin: 34px auto 0;
}
.soon-tiles .tile { animation: soon-pulse 2.8s ease-in-out infinite; }
.soon-tiles .tile:nth-child(2) { animation-delay: 0.35s; }
.soon-tiles .tile:nth-child(3) { animation-delay: 0.7s; }
.soon-tiles .tile:nth-child(4) { animation-delay: 1.05s; }
.soon-tiles .tile:nth-child(5) { animation-delay: 1.4s; }
@keyframes soon-pulse {
  0%, 100% { border-color: var(--tile-border); }
  50% { border-color: var(--tile-border-filled); }
}

/* ---------- Bundle board ---------- */

/* The title bundles its own pair: "un" rides inside a bundle rectangle
   (dashed like a chain preview, accent like the in-game pairs). */
.bundle-title .pairbox {
  display: inline-block;
  border: 2px dashed var(--accent);
  border-radius: 8px;
  padding: 0 0.1em;
  margin: 0 0.06em;
}

.game {
  max-width: 350px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Guess rows are separated by 5px seams; each chain pair spans its two
   rows plus the seam and renders as a single bundled rectangle. */
.board {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-template-rows: auto 5px auto 5px auto 5px auto 5px auto 5px auto;
  column-gap: 5px;
  width: 100%;
}

.tile {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(22px, 7vw, 30px);
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1;
  border: 2px solid var(--tile-border);
  border-radius: 3px;
  color: var(--text);
  user-select: none;
  transition: border-color 0.1s ease, background-color 0.15s ease;
}
.tile.filled { border-color: var(--tile-border-filled); }

.tile.reveal { animation: flip 0.55s ease forwards; }
@keyframes flip {
  0% { transform: rotateX(0); }
  50% { transform: rotateX(90deg); }
  100% { transform: rotateX(0); }
}

.tile.shake { animation: shake 0.55s ease; }
@keyframes shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}

.tile.correct { background: var(--correct); border-color: var(--correct); color: var(--on-color); }
.tile.present { background: var(--present); border-color: var(--present); color: var(--on-color); }
.tile.absent  { background: var(--absent);  border-color: var(--absent);  color: var(--on-color); }

/* Two connected tiles shown as one 1-wide, 2-tall rectangle.
   chain: letter not delivered yet · armed: carried letter in place ·
   spent: both halves scored, colors stand alone */
.pair {
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--bg);
  border: 2px solid var(--tile-border);
  border-radius: 8px;
  overflow: hidden;
  transition: border-color 0.15s ease;
}
.pair.chain { border-style: dashed; border-color: var(--accent); }
.pair.armed { border-color: var(--accent); }
.pair.spent { border-color: transparent; }

.pair .tile {
  flex: 1;
  min-height: 0;
  width: 100%;
  aspect-ratio: auto;
  border: none;
  border-radius: 0;
}
.pair .tile.filled { background: var(--hover); }
.pair .tile.carried { background: var(--accent-soft); }

/* ---------- Subtle board (Day 2) ---------- */

/* The title practices what the game preaches: it fades out left to right —
   solid until "b", then dimming through "tle" until the final e sits at
   about half strength. */
.fade-title {
  /* background-clip: text clips the paint to the glyphs, but the gradient's
     % stops still map to the element box — a full-width h1 would park the
     whole word inside the solid 0–40% zone and the fade would happen in
     empty space. Shrink the box to the word. */
  width: fit-content;
  /* Fallback first: a plain fade for engines without color-mix. */
  background-image: linear-gradient(90deg, var(--text) 40%, transparent 100%);
  background-image: linear-gradient(90deg, var(--text) 40%,
    color-mix(in srgb, var(--text) 45%, transparent) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

/* Page-scoped accent: a violet that actually reads as violet. Root
   --accent stays Bundle's water blue. The score colorizes from quiet gray
   to saturated violet as it climbs — the number itself is the feedback, so
   nothing ever paints over it. */
body.subtle {
  --accent: #6c5ce7;
  --accent-soft: rgba(108, 92, 231, 0.16);
  --score-hi: #5b46e8;
  --score-on: #ffffff;
  --score-grad-a: #e7e1fd;
  --score-grad-b: #c7b9f9;
  background-color: var(--bg);
  background-image: radial-gradient(900px 420px at 50% -6%,
    var(--accent-soft), transparent 70%);
}
[data-theme="dark"] body.subtle {
  --accent: #9d8dfd;
  --accent-soft: rgba(157, 141, 253, 0.2);
  --score-hi: #b4a3ff;
  --score-on: #121213;
  --score-grad-a: #3a3178;
  --score-grad-b: #5346ab;
}

/* No tile outlines here — letters and the score carry the board alone. */
body.subtle .tile { border: none; }

/* Keys tint with the share of all earned score their letter accounts for
   (inline --m, 0-1, set by subtle.js): unused keys stay plain, any use
   tints 30% at once so used vs unused reads instantly, then scales with
   the letter's share up to full saturation. Quiet, and very much on
   purpose. */
body.subtle .key {
  background: var(--key-bg);
  background: color-mix(in oklab, var(--score-hi) calc(var(--m, 0) * 100%),
    var(--key-bg));
  color: var(--key-text);
  color: color-mix(in oklab, var(--score-on) calc(var(--m, 0) * 100%),
    var(--key-text));
  transition: background-color 0.5s ease, color 0.5s ease;
}

.subtle-game { max-width: 380px; }

/* The board is a fixed window holding ~6 rows; guesses fill it from the top
   and it scrolls once they outgrow it, old rows fading under the top edge. */
.rows {
  width: 100%;
  height: min(372px, 62vh);
  display: flex;
  flex-direction: column;
  gap: 5px;
  overflow-y: auto;
  scrollbar-width: none;
  scroll-behavior: smooth;
}
.rows::-webkit-scrollbar { display: none; }
.rows.overflowing {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 26px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 26px);
}

.srow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr) 56px;
  gap: 5px;
  animation: row-rise 0.3s ease both;
}
@keyframes row-rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
/* Restored rows carry `quiet` permanently: no enter animation, no color
   sweep — the board paints finished, not animated. */
.srow.quiet { animation: none; }
.srow.quiet .score { transition: none; }

/* The score: just a bold number, colorized from quiet gray to saturated
   violet via --w (0-1). Text only — no surface, so nothing can cover it. */
.score {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--muted);
  color: color-mix(in oklab, var(--score-hi) calc(var(--w, 0) * 100%),
    var(--muted));
  transition: color 0.6s ease;
  user-select: none;
}
.score.settle { animation: settle 0.45s ease; }
@keyframes settle {
  from { opacity: 0.3; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

.srow.win .tile {
  background: linear-gradient(120deg, var(--score-grad-a), var(--score-grad-b));
  border-color: transparent;
}

/* ---------- Wobble board (Day 3) ---------- */

/* Page-scoped accent: warm amber. Root --accent stays Bundle's water blue. */
body.wobble {
  --accent: #a35a00;
  --accent-soft: rgba(163, 90, 0, 0.12);
}
[data-theme="dark"] body.wobble {
  --accent: #f2a33c;
  --accent-soft: rgba(242, 163, 60, 0.16);
}

.wobble-game { max-width: 350px; }

/* The title wobbles too: every other letter dips a little. */
.wobble-title span {
  display: inline-block;
  transform: translateY(0.12em);
}

/* Staggered brick rows: probes are 4 tiles riding columns 1-4 or, shifted
   one full column right, columns 2-5. The shift is a transform (not a
   margin — margins would shrink the row and with it every tile's
   percentage width), so tiles are identical in both layouts and the
   wobble still animates. */
.wobble-board {
  --wgap: 5px;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--wgap);
}

.wrow {
  display: flex;
  gap: var(--wgap);
  transition: transform 0.25s ease;
  /* Entrance animates opacity only — a transform keyframe with fill:both
     would override .wrow.even's translateX forever. */
  animation: wrow-in 0.3s ease both;
}
@keyframes wrow-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
.wrow.even { transform: translateX(calc((100% + var(--wgap)) / 5)); }
.wrow.quiet { animation: none; }

.wrow .tile {
  width: calc((100% - 4 * var(--wgap)) / 5);
  border-color: transparent;
}
.wrow .tile.inplay { border-color: var(--tile-border); }
.wrow .tile.filled { border-color: var(--tile-border-filled); }
.wrow.scored .tile { border-color: transparent; }

/* The clock: which guess it is, and when the commit is due. */
.turnline {
  margin-top: 12px;
  width: 100%;
  text-align: center;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.turnline.final { color: var(--correct); }

/* ---------- Toggle board (Day 4) ---------- */

/* Page-scoped accent: teal. Root --accent stays Bundle's water blue. */
body.toggle {
  --accent: #0f766e;
  --accent-soft: rgba(15, 118, 110, 0.12);
  --score-hi: #0d9488;
}
[data-theme="dark"] body.toggle {
  --accent: #2dd4bf;
  --accent-soft: rgba(45, 212, 191, 0.14);
  --score-hi: #5eead4;
}

.toggle-game { max-width: 350px; }

/* The title: one toggle stands in for both g's — the pill reads as two
   circles side by side, and two little tails echo the double descender.
   Click it: the knob slides. */
.toggle-title .tg {
  display: inline-block;
  position: relative;
  width: 1.24em;
  height: 0.74em;
  margin: 0 0.05em;
  cursor: pointer;
}
.toggle-title .tg .pill {
  position: absolute;
  top: 0.1em;
  left: 0.1em;
  width: 1.1em;
  height: 0.4em;
  border-radius: 999px;
  background: var(--accent);
}
.toggle-title .tg .pill::after {
  content: "";
  position: absolute;
  top: 0.05em;
  left: 0.05em;
  width: 0.3em;
  height: 0.3em;
  border-radius: 50%;
  background: var(--bg);
  transition: left 0.2s ease;
}
.toggle-title .tg .pill.on::after { left: calc(100% - 0.35em); }
.toggle-title .tg .tail {
  position: absolute;
  top: 0.44em;
  width: 0.38em;
  height: 0.32em;
  border: 0.055em solid var(--text);
  border-top: none;
  border-left-color: transparent;
  border-right-color: transparent;
  border-radius: 0 0 80% 80% / 0 0 100% 100%;
}
.toggle-title .tg .tail.a { left: 0.12em; }
.toggle-title .tg .tail.b { left: 0.72em; }
.toggle-title .sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.toggle-board {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.trow {
  display: flex;
  gap: 8px;
  align-items: center;
  animation: row-rise 0.3s ease both;
}
.trow.quiet { animation: none; }

/* The toggle: a minimal pill with a circular middle. State lives in
   aria-pressed (and .on); the knob travels via `left` so it works at any
   tile width. */
.trow .tog {
  flex: 1;
  height: 32px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  position: relative;
  cursor: pointer;
  font: inherit;
  padding: 0;
  transition: background-color 0.2s ease, border-color 0.2s ease;
}
.trow .tog::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--muted);
  transition: left 0.2s ease, background-color 0.2s ease;
}
.trow .tog[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
}
.trow .tog[aria-pressed="true"]::after {
  left: calc(100% - 27px);
  background: var(--bg);
}
.trow .tog:disabled { cursor: default; }
.trow.scored .tog:disabled { opacity: 0.85; }
.trow .score { flex: none; width: 56px; height: 32px; }

/* The loss reveal: "Out of guesses" with the answer as small view-only
   toggles, right under the text inside the banner. */
.banner-pattern {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 8px;
}
.banner-pattern .tog {
  width: 40px;
  height: 20px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  position: relative;
  display: inline-block;
}
.banner-pattern .tog::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--muted);
  transition: left 0.2s ease, background-color 0.2s ease;
}
.banner-pattern .tog[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
}
.banner-pattern .tog[aria-pressed="true"]::after {
  left: calc(100% - 16px);
  background: var(--bg);
}

/* ---------- Meddle board (Day 5) ---------- */

/* Page-scoped accent: rose. Root --accent stays Bundle's water blue. */
body.meddle {
  --accent: #be185d;
  --accent-soft: rgba(190, 24, 93, 0.1);
}
[data-theme="dark"] body.meddle {
  --accent: #f472b6;
  --accent-soft: rgba(244, 114, 182, 0.16);
}

.meddle-game { max-width: 350px; }

/* The title double-exposes itself: two ghost copies of the word sit behind
   it, misregistered left and right — two words meddling in one board, one
   title. */
.meddle-title {
  position: relative;
  z-index: 0;
}
.meddle-title::before,
.meddle-title::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  user-select: none;
  pointer-events: none;
}
.meddle-title::before {
  transform: translate(-0.15em, 0.025em);
  color: var(--accent);
  opacity: 0.4;
}
.meddle-title::after {
  transform: translate(0.15em, -0.025em);
  color: var(--text);
  opacity: 0.18;
}

/* Standard bordered Wordle tiles, but rows grow on demand inside the same
   scroll window Subtle uses. Entrance animates opacity only. */
.mrow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.mrow.quiet { animation: none; }

/* ---------- Crumble board (Day 6) ---------- */

/* Page-scoped accent: terracotta dust. Root --accent stays Bundle's water
   blue. */
body.crumble {
  --accent: #a34721;
  --accent-soft: rgba(163, 71, 33, 0.12);
}
[data-theme="dark"] body.crumble {
  --accent: #f0926b;
  --accent-soft: rgba(240, 146, 107, 0.16);
}

.crumble-game { max-width: 350px; }

/* The title is Thanos-snapped: the bottom half of the word disintegrates.
   The h1's own paint carries only the top half (background clipped to the
   glyphs and cut at the midline — same box-vs-glyph gotcha as .fade-title,
   so the box shrinks to the word); ::after is the bottom half as a dust
   copy whose speckle holes grow via an animated @property while it sags
   and drifts, settling half-gone — frozen mid-snap; ::before is a pinch of
   motes lifting off it. Plays once on load, then rests. Without @property
   the holes never grow and the word simply stays whole. */
.crumble-title {
  position: relative;
  width: fit-content;
  background-image: linear-gradient(180deg, var(--text) 55%, transparent 55%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
/* Three registered properties grow at their own rates, so the erosion is
   progressive and uneven — one uniform hole size is what made the old
   version read as halftone dots. */
@property --d1 {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}
@property --d2 {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}
@property --d3 {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}
.crumble-title::before,
.crumble-title::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  color: var(--text);
  -webkit-text-fill-color: var(--text);
  pointer-events: none;
  user-select: none;
}
/* The disintegrating half: three coprime mask tiles (large pocks, medium
   pits, fine grit) with soft-feathered holes and slowly drifting positions
   — irregular enough to read as erosion, not a halftone grid. */
.crumble-title::after {
  clip-path: inset(52% 0 0 0);
  -webkit-mask-image:
    radial-gradient(ellipse 55% 45% at 30% 30%,
      transparent var(--d1), #000 calc(var(--d1) + 2.5px)),
    radial-gradient(ellipse 40% 60% at 68% 62%,
      transparent var(--d2), #000 calc(var(--d2) + 2px)),
    radial-gradient(ellipse 32% 45% at 52% 78%,
      transparent var(--d3), #000 calc(var(--d3) + 1.5px));
  mask-image:
    radial-gradient(ellipse 55% 45% at 30% 30%,
      transparent var(--d1), #000 calc(var(--d1) + 2.5px)),
    radial-gradient(ellipse 40% 60% at 68% 62%,
      transparent var(--d2), #000 calc(var(--d2) + 2px)),
    radial-gradient(ellipse 32% 45% at 52% 78%,
      transparent var(--d3), #000 calc(var(--d3) + 1.5px));
  -webkit-mask-size: 9px 7px, 6px 10px, 4px 5px;
  mask-size: 9px 7px, 6px 10px, 4px 5px;
  -webkit-mask-position: 1px 2px, 3px 5px, 0 0;
  mask-position: 1px 2px, 3px 5px, 0 0;
  animation: title-dust 2.6s ease-in 0.8s both;
}
@keyframes title-dust {
  0% {
    --d1: 0%; --d2: 0%; --d3: 0%; opacity: 1; transform: none;
    -webkit-mask-position: 1px 2px, 3px 5px, 0 0;
    mask-position: 1px 2px, 3px 5px, 0 0;
  }
  /* erosion front-runs the fade: most of the eating is done by 40%, the
     back half just sags and dims the leftover grit */
  40% {
    --d1: 36%; --d2: 42%; --d3: 50%; opacity: 1;
  }
  100% {
    --d1: 46%; --d2: 52%; --d3: 60%; opacity: 0.55;
    transform: translate(0.04em, 0.08em);
    -webkit-mask-position: 3px 5px, 1px 8px, 2px 4px;
    mask-position: 3px 5px, 1px 8px, 2px 4px;
  }
}
.crumble-title::before {
  clip-path: inset(48% 0 0 0);
  -webkit-mask-image:
    radial-gradient(circle, #000 0.7px, transparent 1.2px),
    radial-gradient(circle, #000 0.6px, transparent 1px);
  mask-image:
    radial-gradient(circle, #000 0.7px, transparent 1.2px),
    radial-gradient(circle, #000 0.6px, transparent 1px);
  -webkit-mask-size: 9px 11px, 13px 9px;
  mask-size: 9px 11px, 13px 9px;
  -webkit-mask-position: 0 0, 4px 6px;
  mask-position: 0 0, 4px 6px;
  opacity: 0;
  animation: title-motes 2.4s ease-out 1s both;
}
@keyframes title-motes {
  0% { opacity: 0; transform: none; }
  30% { opacity: 0.5; }
  100% { opacity: 0; transform: translate(0.14em, -0.12em); }
}

/* The well: a Tetris shaft, 5 wide. The queue slot waits at the top;
   the pile grows from the floor, and ghost cells sketch the empty shaft.
   The shaft starts deep enough for 6 visible rows and extends upward as
   the pile grows (JS sets its height in px — rows are square). */
.well {
  width: 100%;
  padding: 5px;
  border: 2px solid var(--tile-border);
  border-top: none;
  border-radius: 0 0 10px 10px;
}
.well.done .queue { visibility: hidden; }

.queue {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  margin-bottom: 5px;
}

/* Pile tiles and ghost cells share one positioning system: bottom-left
   origin, --c columns across, --r rows up. The transform percentages are
   relative to the tile itself, so (100% + gap) is exactly one column/row
   pitch at any well width. */
.shaft { position: relative; width: 100%; }

.ghosts {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.ghosts .cell {
  position: absolute;
  left: 0;
  bottom: 0;
  width: calc((100% - 20px) / 5);
  aspect-ratio: 1;
  border: 2px solid var(--tile-border);
  border-radius: 3px;
  opacity: 0.4;
  transform: translate(calc(var(--c) * (100% + 5px)),
    calc(var(--r) * (-100% - 5px)));
}

.pile-tile {
  position: absolute;
  left: 0;
  bottom: 0;
  width: calc((100% - 20px) / 5);
  z-index: 1;
  transform: translate(calc(var(--c) * (100% + 5px)),
    calc(var(--r) * (-100% - 5px)));
  transition: transform var(--td, 0ms) var(--ease, ease);
}
/* The crumble: the tile ERODES instead of breaking — the same organic
   three-scale speckle mask as the title eats bg, border and letter alike
   while the tile sags and fades (no halves, no slices, no --ck needed:
   the mask dissolves whatever mark the tile carries, colorblind modes
   included). Cancelling yellows share it. */
.pile-tile.crumble,
.pile-tile.cancel {
  -webkit-mask-image:
    radial-gradient(ellipse 55% 45% at 30% 30%,
      transparent var(--d1), #000 calc(var(--d1) + 2px)),
    radial-gradient(ellipse 40% 60% at 68% 62%,
      transparent var(--d2), #000 calc(var(--d2) + 1.5px)),
    radial-gradient(ellipse 32% 45% at 52% 78%,
      transparent var(--d3), #000 calc(var(--d3) + 1px));
  mask-image:
    radial-gradient(ellipse 55% 45% at 30% 30%,
      transparent var(--d1), #000 calc(var(--d1) + 2px)),
    radial-gradient(ellipse 40% 60% at 68% 62%,
      transparent var(--d2), #000 calc(var(--d2) + 1.5px)),
    radial-gradient(ellipse 32% 45% at 52% 78%,
      transparent var(--d3), #000 calc(var(--d3) + 1px));
  -webkit-mask-size: 8px 6px, 5px 9px, 4px 4px;
  mask-size: 8px 6px, 5px 9px, 4px 4px;
  animation: tile-crumble 0.6s ease-in forwards;
}
/* Erosion front-runs the fade: the tile is mostly eaten by halfway, and
   the opacity only winks out at the end — otherwise it reads as a noisy
   fade instead of being eaten. */
@keyframes tile-crumble {
  0% {
    --d1: 0%; --d2: 0%; --d3: 0%; opacity: 1;
    transform: translate(calc(var(--c) * (100% + 5px)),
      calc(var(--r) * (-100% - 5px)));
  }
  50% {
    --d1: 50%; --d2: 56%; --d3: 64%; opacity: 0.85;
    transform: translate(calc(var(--c) * (100% + 5px)),
      calc(var(--r) * (-100% - 5px) + 18%));
  }
  100% {
    --d1: 66%; --d2: 72%; --d3: 80%; opacity: 0;
    transform: translate(calc(var(--c) * (100% + 5px)),
      calc(var(--r) * (-100% - 5px) + 45%));
  }
}

/* The assembled answer: one settled row reading the word left to right. */
.pile-tile.winline { animation: win-glow 1.6s ease 0.4s; }
@keyframes win-glow {
  0%, 100% { box-shadow: none; }
  40% { box-shadow: 0 0 18px var(--accent-soft); }
}

/* ---------- Bubble board (Day 7) ---------- */

/* Page-scoped accent: soap-bubble cyan (celebration only — marks carry no
   color on this page). Root --accent stays Bundle's water blue. --iris is
   the shared thin-film rainbow every bubble rim wears. */
body.bubble {
  --accent: #0891b2;
  --accent-soft: rgba(8, 145, 178, 0.12);
  --iris: conic-gradient(from 220deg,
    rgba(255, 154, 213, 0.6),
    rgba(255, 210, 143, 0.52),
    rgba(154, 240, 192, 0.55),
    rgba(140, 200, 255, 0.62),
    rgba(192, 154, 255, 0.55),
    rgba(255, 154, 213, 0.6));
  /* How far the lamella's flat wall sits from the bubble's bounding edge
     — expressed as the facing corners' horizontal border-radius, so the
     wall ends ROUNDED (a clip made them pointy). Retune live with
     BUBBLE.wall() in the console. */
  --wall-inset: 40%;
  /* Breathing room between two connected lamellae. */
  --wall-gap: 8px;
  /* The soap film every bubble wears, and the flat shared wall drawn
     between connected bubbles (a faint vertical line, brighter than the
     film — real double bubbles show their common wall). The lamella line
     hugs the clip plane so the full stroke survives the flat clip. */
  /* The film is three separate layers — and MUST stay three separate
     custom props (--film-a/b/c) wherever it shares a background-image
     list with other layers: var(--film) in a list expands to three
     layers, and background-size/position lists then cycle misaligned
     (that's what painted middle bubbles solid blue). */
  --film-a: radial-gradient(circle at 32% 27%, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0) 36%);
  --film-b: radial-gradient(circle at 70% 78%, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 45%);
  --film-c: linear-gradient(165deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.02) 55%, rgba(255, 255, 255, 0.09));
  --film: var(--film-a), var(--film-b), var(--film-c);
  --wall: rgba(160, 210, 255, 0.7);
  /* The lamella line: a short vertical stripe at the bubble's facing
     edge, spanning the flat wall between the rounded corners. */
  --wall-line: linear-gradient(var(--wall), var(--wall));
}
[data-theme="dark"] body.bubble {
  --accent: #22d3ee;
  --accent-soft: rgba(34, 211, 238, 0.14);
}

.bubble-game { max-width: 350px; }

/* The title practices the game: every letter of the name floats in its own
   soap bubble — the same iridescent shell the board's bubbles wear. Letters
   blow in one after another, then bob gently at staggered lifts. */
.bubble-title {
  display: flex;
  align-items: center;
  gap: 0.13em;
}
.bubble-title .tb {
  --lift: 0.03em;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2em;
  height: 2em;
  border-radius: 50%;
  background:
    radial-gradient(circle at 31% 26%, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0) 38%),
    radial-gradient(circle at 70% 78%, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0) 46%),
    linear-gradient(165deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.02) 55%, rgba(255, 255, 255, 0.1));
  font-size: 0.72em;
  font-weight: 700;
  line-height: 1;
  transform: translateY(var(--lift));
  animation:
    tb-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both,
    tb-bob 2.4s ease-in-out infinite;
  animation-delay: 0s, 1s;
  /* keep the bob on the compositor — a slow tiny translate on
     rasterized text pixel-snaps and reads as single-frame jumps */
  will-change: transform;
  user-select: none;
}
.bubble-title .tb::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  padding: 2px;
  background: var(--iris);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.bubble-title .tb:nth-child(2) { --lift: -0.06em; animation-delay: 0.08s, 1.12s; }
.bubble-title .tb:nth-child(3) { --lift: 0.07em; animation-delay: 0.16s, 1.24s; }
.bubble-title .tb:nth-child(4) { --lift: -0.03em; animation-delay: 0.24s, 1.36s; }
.bubble-title .tb:nth-child(5) { --lift: 0.05em; animation-delay: 0.32s, 1.48s; }
.bubble-title .tb:nth-child(6) { --lift: -0.07em; animation-delay: 0.4s, 1.6s; }
@keyframes tb-in {
  from { transform: scale(0); opacity: 0; }
  to { transform: translateY(var(--lift)) scale(1); opacity: 1; }
}
@keyframes tb-bob {
  0%, 100% { transform: translateY(var(--lift)); }
  50% { transform: translateY(calc(var(--lift) - 0.09em)); }
}

/* Rows of bubbles. Bubbles are soap film, not tiles: no fills, no borders
   — a transparent film with one glossy highlight and a thin iridescent rim
   (::before, the masked conic ring; border-image can't take border-radius,
   so the rim is a pseudo masked down to a ring). An EMPTY slot carries no
   shell at all: the bubble is born with its letter (suitangi: bubbles
   appear when the letters appear), wears its film while filled, and firms
   when held. The puzzle is color-agnostic: shape is the only voice —
   popped, held, fused. */
.bubble-board {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.brow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
}
.brow .tile {
  position: relative;
  border: none;
  border-radius: 50%;
  text-shadow: 0 0 6px var(--bg);
}
/* The film is a ::after BEHIND the letter (z -1) so it can fade in and out
   with opacity — background gradients can't transition. The rim stays a
   ::before masked ring. An EMPTY slot carries neither: the bubble is born
   with its letter (suitangi: bubbles appear when the letters appear). */
.brow .tile::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background-image: var(--film);
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}
.brow .tile.filled::after,
.brow .tile.held::after,
.brow .tile.popped::after { opacity: 1; }
/* Connected bubbles draw their half of the shared wall on their film: a
   vertical stripe at the facing edge, 60% of the bubble tall — exactly
   the flat stretch between the rounded corners. */
.brow .tile.wr::after {
  background-image: var(--film-a), var(--film-b), var(--film-c), var(--wall-line);
  background-size: auto, auto, auto, 2px 60%;
  background-position: 0 0, 0 0, 0 0, calc(100% - 1px) 50%;
  background-repeat: no-repeat;
}
.brow .tile.wl::after {
  background-image: var(--film-a), var(--film-b), var(--film-c), var(--wall-line);
  background-size: auto, auto, auto, 2px 60%;
  background-position: 0 0, 0 0, 0 0, 1px 50%;
  background-repeat: no-repeat;
}
.brow .tile.wr.wl::after {
  background-image: var(--film-a), var(--film-b), var(--film-c),
    var(--wall-line), var(--wall-line);
  background-size: auto, auto, auto, 2px 60%, 2px 60%;
  background-position: 0 0, 0 0, 0 0, calc(100% - 1px) 50%, 1px 50%;
  background-repeat: no-repeat;
}
.brow .tile::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 2.5px;
  background: var(--iris);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
.brow .tile.filled::before { opacity: 0.85; }
.brow .tile.held::before { opacity: 1; }
/* Popped bubbles keep their whole shell — the pop bursts a full bubble,
   not a ghost of one. */
.brow .tile.popped::before { opacity: 0.85; }
/* A typed letter blows up into its bubble (springy overshoot). */
.brow .tile.bub-in { animation: bub-grow 0.24s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes bub-grow {
  from { transform: scale(0.35); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
/* The pre-reveal swell: the whole bubble blows up before its fate shows. */
.brow .tile.swell { animation: bub-swell 0.3s ease; }
@keyframes bub-swell {
  0%, 100% { transform: scale(1); }
  55% { transform: scale(1.14); }
}
/* The pop, slow and dramatic: the skin inflates, hesitates, over-stretches
   — then bursts. Fill-forwards holds the cell invisible. */
.brow .tile.pop { animation: bub-pop 0.95s ease-in forwards; }
@keyframes bub-pop {
  0% { transform: scale(1); opacity: 1; }
  30% { transform: scale(1.22); opacity: 1; }
  45% { transform: scale(1.1); opacity: 1; }
  58% { transform: scale(1.3); opacity: 1; }
  64% { transform: scale(1.26); opacity: 1; }
  100% { transform: scale(0); opacity: 0; }
}
/* The burst itself, at the pop's over-stretch point: the rim flashes into
   an expanding ring while a spray of droplets (one box-shadow constellation
   — transform: scale flies them all outward together) scatters and fades. */
.brow .tile.pop::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--text) 40%, transparent);
  box-shadow:
    -18px -22px 0 -1px color-mix(in srgb, var(--accent) 55%, transparent),
    16px -24px 0 -1.5px color-mix(in srgb, var(--text) 32%, transparent),
    25px 3px 0 -1px color-mix(in srgb, var(--accent) 45%, transparent),
    -23px 11px 0 -1.5px color-mix(in srgb, var(--text) 32%, transparent),
    -1px 27px 0 -1px color-mix(in srgb, var(--accent) 55%, transparent);
  opacity: 0;
  transform: scale(0.5);
  animation: bub-burst 0.95s ease-out forwards;
}
@keyframes bub-burst {
  0%, 58% { opacity: 0; transform: scale(0.5); }
  63% { opacity: 1; }
  100% { opacity: 0; transform: scale(1.8); }
}

/* A connected run: the bubbles stay THEMSELVES but stop being perfect
   circles — connected neighbours reshape into rounded D's (border-radius
   on the cell; film and rim inherit it), slide to sit --wall-gap apart,
   and each draws its lamella line at its facing edge. (suitangi: keep the
   bubbles individually visible with a flat line between, round the ends
   where the outline meets the wall, and they "connect", not "fuse".) */
.run {
  position: relative;
  display: flex;
  gap: 5px; /* keep in sync with the JS seam math */
}
.run .tile {
  flex: 1;
  min-width: 0;
  transition: border-radius 0.35s ease;
}
.run.on .tile {
  /* Slide until the facing edges sit --wall-gap apart: layout puts
     neighbours 5px apart (the row gap), so each junction closes
     (5px - gap) — negative = the chain eases apart. translateX % is
     relative to the bubble itself, so it stays exact at any size. */
  --tx: calc((var(--ci, 0) - (var(--ck, 1) - 1) / 2) *
    (5px - var(--wall-gap)));
  transform: translateX(var(--tx));
  animation: connect-press 0.5s ease;
}
/* Connected bubbles aren't clipped circles — they're shaped D's: the
   facing side's corners carry the border-radius (film AND rim follow it),
   so the outline rounds smoothly into the lamella instead of stabbing
   into it at a point. The flat wall between the corners spans 60% of the
   height (100% minus the two 20% vertical radii). */
.run.on .tile.wr {
  border-radius: 50% var(--wall-inset) var(--wall-inset) 50% /
    50% 20% 20% 50%;
}
.run.on .tile.wl {
  border-radius: var(--wall-inset) 50% 50% var(--wall-inset) /
    20% 50% 50% 20%;
}
.run.on .tile.wr.wl {
  border-radius: var(--wall-inset) var(--wall-inset) var(--wall-inset)
    var(--wall-inset) / 20% 20% 20% 20%;
}
/* The press: slide in and bulge as the skins meet, then settle on the
   shared wall. */
@keyframes connect-press {
  0% { transform: translateX(0) scale(1); }
  45% { transform: translateX(var(--tx)) scale(1.06, 1.12); }
  100% { transform: translateX(var(--tx)) scale(1); }
}
/* The blown word: the answer as one connected chain. The glow rides the
   container with a stadium radius — soft enough that the silhouette
   mismatch with the flat-walled circles doesn't read. */
.run.win {
  border-radius: 999px;
  animation: run-win 1.6s ease 0.35s;
}
@keyframes run-win {
  0%, 100% { box-shadow: none; }
  40% { box-shadow: 0 0 20px var(--accent-soft); }
}

/* Restored rows are born resolved: no grow, no swell, no pop, no press —
   and popped bubbles, whose pop animation is disabled here, hide outright.
   Connected rows are built already shaped and spaced (quiet kills the
   margin transition, so they don't slide on load). */
.brow.quiet .tile {
  animation: none;
  transition: none;
}
.brow.quiet .run { animation: none; }
.brow.quiet .tile.popped { opacity: 0; }

/* Kept letters light their keys with the film's own pastels — the
   keyboard's whole vocabulary is kept vs absent, like the board's. */
body.bubble .key.kept {
  background: linear-gradient(120deg,
    rgba(255, 154, 213, 0.38),
    rgba(255, 210, 143, 0.34),
    rgba(154, 240, 192, 0.36),
    rgba(140, 200, 255, 0.4),
    rgba(192, 154, 255, 0.38));
  color: var(--text);
}

/* Restated last and at full specificity: this game's own animation classes
   would otherwise out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha, bubble edition). submit() still clears shake
   before revealing. */
.brow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Swivel board (Day 8) ---------- */

/* Page-scoped accent: moss green. Root --accent stays Bundle's water
   blue. */
body.swivel {
  --accent: #4d7c0f;
  --accent-soft: rgba(77, 124, 15, 0.12);
}
[data-theme="dark"] body.swivel {
  --accent: #a3e635;
  --accent-soft: rgba(163, 230, 53, 0.13);
}

.swivel-game { max-width: 420px; }
.swivel-game .game-actions { flex-wrap: wrap; justify-content: center; }
/* The banner sits between the board and the buttons, with breathing
   room on both sides. */
.swivel-game .game-banner { margin: 16px 0 18px; }
/* Clue swaps fade instead of snapping: as the board turns, the shared
   tiles cross-fade to the other board's colors (and the keyboard with
   them) — overriding the shared 0.15s snap. */
.swivel-board .tile {
  transition: background-color 0.45s ease, border-color 0.45s ease;
}
body.swivel .key { transition: background-color 0.45s ease; }

/* The title swivels in: each letter lands with a quarter-turn pivot.
   Delays are per-child EXPLICIT values — no calc-in-delay (the Bubble
   day's gotcha). */
.swivel-title .swv {
  display: inline-block;
  animation: sw-title-in 0.55s cubic-bezier(0.34, 1.45, 0.64, 1) both;
}
.swivel-title .swv:nth-child(2) { animation-delay: 0.07s; }
.swivel-title .swv:nth-child(3) { animation-delay: 0.14s; }
.swivel-title .swv:nth-child(4) { animation-delay: 0.21s; }
.swivel-title .swv:nth-child(5) { animation-delay: 0.28s; }
.swivel-title .swv:nth-child(6) { animation-delay: 0.35s; }
@keyframes sw-title-in {
  from { transform: rotate(90deg) scale(0.4); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* The pivot: a viewport whose width/height swap while the board rotates
   90° inside it — the board is 8 wide x 6 tall, so rotated its footprint
   is 6 x 8. JS sizes both boxes in px (from the tile pitch) so the swap
   stays exact at any screen width. Swiveling is CLOCKWISE: it lands the
   horizontal board upright (M to H); counterclockwise returns to the
   main board. */
.pivot {
  position: relative;
  margin: 0 auto;
  transition: width 0.65s cubic-bezier(0.7, 0, 0.3, 1),
    height 0.65s cubic-bezier(0.7, 0, 0.3, 1);
}
.swivel-board {
  position: absolute;
  left: 50%;
  top: 50%;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(6, 1fr);
  gap: 5px;
  transform: translate(-50%, -50%);
  transition: transform 0.65s cubic-bezier(0.7, 0, 0.3, 1);
}
.pivot.swiveled .swivel-board { transform: translate(-50%, -50%) rotate(90deg); }

.swivel-board .tile {
  aspect-ratio: auto; /* the board's px box makes the cells square */
  font-size: 1em;     /* JS sets the board's font from the tile pitch */
  border-radius: 3px;
}
.swivel-board .tile.ghost { opacity: 0.45; }

/* Every glyph rides a span so it can turn. The swivel counter-rotates
   the letters a quarter-turn against the board's clockwise turn, so they
   all land face-up on screen — including the horizontal board's letters,
   which sit at -90° (tops left) while the board is upright. */
.swivel-board .lt {
  display: inline-block;
  transition: transform 0.65s cubic-bezier(0.7, 0, 0.3, 1);
}
.swivel-board .s-tile .lt { transform: rotate(-90deg); }
.pivot.swiveled .swivel-board .lt { transform: rotate(-90deg); }

/* The crossing tiles (the 5x5 = 25-tile overlap) score BOTH ways but
   show ONE clue at a time, following the view: the main wordle's clue
   while upright, the horizontal wordle's clue once swiveled (those clues
   only exist once the sixth main word completes the columns). The moment
   the columns score, the whole crossing block pulses. */
.swivel-board .duo.scored { animation: duo-scored 1.1s ease; }
@keyframes duo-scored {
  0%, 100% { box-shadow: none; }
  40% { box-shadow: 0 0 16px var(--accent-soft); }
}

/* Board dividers: a dotted line splitting the two boards, one per view,
   both in the game's lime accent. Upright, divider-m stands vertical in
   the gap between main columns 5 and 6 (main board left, horizontal
   board's open columns right). Swiveled, divider-h lies horizontal in
   the gap between main rows 1 and 2 — on screen it stands between the
   horizontal board's letter columns and the main-only heel column. The
   view class shows one and hides the other. */
.swivel-board .divider {
  position: absolute;
  pointer-events: none;
  opacity: 0.8;
}
.swivel-board .divider-m {
  top: 0;
  bottom: 0;
  left: calc((100% + 5px) * 5 / 8 - 2.5px);
  width: 2px;
  background-image: repeating-linear-gradient(to bottom,
    var(--accent) 0 4px, transparent 4px 9px);
}
.swivel-board .divider-h {
  left: 0;
  right: 0;
  top: calc((100% + 5px) * 1 / 6 - 2.5px);
  height: 2px;
  background-image: repeating-linear-gradient(to right,
    var(--accent) 0 4px, transparent 4px 9px);
}
.pivot.swiveled .swivel-board .divider-m,
.pivot:not(.swiveled) .swivel-board .divider-h { display: none; }

/* Found words glow down their line (keyframes shared with Crumble). */
.swivel-board .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* Restored boards are born resolved: tiles rebuilt from the save carry
   `quiet` — no reveal animations. repaintMain() strips it from cells it
   repaints, so their clue swaps still fade on later view toggles. */
.swivel-board .tile.quiet { animation: none; }

/* Restated last and at full specificity: this game's scoped classes would
   otherwise out-rank the shared .tile.shake and mute the reject shake
   (the Day 1 gotcha). submit() still clears shake before revealing. */
.swivel-board .tile.shake { animation: shake 0.55s ease; }

/* ---------- Stifle board (Day 9) ---------- */

/* Page-scoped accent: deep crimson — held breath, spent letters. Root
   --accent stays Bundle's water blue. */
body.stifle {
  --accent: #b91c1c;
  --accent-soft: rgba(185, 28, 28, 0.1);
}
[data-theme="dark"] body.stifle {
  --accent: #f87171;
  --accent-soft: rgba(248, 113, 113, 0.13);
}

.stifle-game { max-width: 350px; }

/* Seven standard rows of bordered tiles. */
.stifle-board {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.frow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.frow.quiet { animation: none; }

/* The title practices the stifling: t and i are already gone — outline
   only, no fill. The word rises in letter by letter (per-child EXPLICIT
   delays — no calc-in-delay, the Bubble day's gotcha). */
.stifle-title span {
  display: inline-block;
  animation: stifle-title-in 0.45s ease both;
}
.stifle-title span:nth-child(2) { animation-delay: 0.06s; }
.stifle-title span:nth-child(3) { animation-delay: 0.12s; }
.stifle-title span:nth-child(4) { animation-delay: 0.18s; }
.stifle-title span:nth-child(5) { animation-delay: 0.24s; }
.stifle-title span:nth-child(6) { animation-delay: 0.3s; }
@keyframes stifle-title-in {
  from { opacity: 0; transform: translateY(0.3em); }
  to { opacity: 1; transform: none; }
}
.stifle-title .out { color: var(--text); }
@supports (-webkit-text-stroke: 1px #000) {
  .stifle-title .out {
    color: transparent;
    -webkit-text-stroke: 0.05em var(--text);
  }
}

/* The stifled key: letter and cap vanish, the shape stays as a bare
   outline. An inset shadow draws it — a real border would grow every key
   and shift the rows. Earned marks live underneath (the dataset state
   survives the fade) and show again the moment the letter returns. The
   fade runs slower than the shared snap so the keys exhale, not blink;
   (0,3,1) also out-ranks the colorblind key overrides, so a stifled key
   is an outline in both mark palettes. */
body.stifle .key {
  transition: background-color 0.4s ease, color 0.4s ease,
    box-shadow 0.4s ease;
}
body.stifle .key.cool {
  background: transparent;
  color: transparent;
  box-shadow: inset 0 0 0 2px var(--tile-border);
}

/* ---------- Hustle board (Day 10) ---------- */

/* Page-scoped accent: safety orange — hurry up. Root --accent stays
   Bundle's water blue. */
body.hustle {
  --accent: #ea580c;
  --accent-soft: rgba(234, 88, 12, 0.11);
}
[data-theme="dark"] body.hustle {
  --accent: #f97316;
  --accent-soft: rgba(249, 115, 22, 0.14);
}

.hustle-game { max-width: 350px; }

/* The clock sits above the board and is the page's whole scoreboard:
   quiet before the run, full-strength while it ticks, the accent once
   it's frozen on a win. tabular-nums keeps the digits from jittering. */
.clock {
  width: 100%;
  text-align: center;
  font-size: 28px;
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  line-height: 1.2;
  color: var(--muted);
  user-select: none;
}
.clock.running { color: var(--text); }
.clock.done { color: var(--accent); }
.clock-hint {
  margin: 2px 0 12px;
  text-align: center;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.clock-hint.hidden { display: none; }
.hustle-game .game-banner { margin-top: 14px; }

/* Rows grow on demand inside the shared scroll window. */
.hrow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.hrow.quiet { animation: none; }

/* The compressed reveal: same flip, 0.3s instead of 0.55s — the JS
   timeline (FLIP_MID = 150) colors at this flip's half-turn. In a
   speedrun, reveal time is clock time. */
body.hustle .tile.reveal { animation-duration: 0.3s; }

/* The title dashes in from the left, letter by letter, and two accent
   speed lines streak in behind the word (the h1 shrinks to the word so
   they trail the glyphs, not the page). */
.hustle-title {
  position: relative;
  width: fit-content;
}
.hustle-title span {
  display: inline-block;
  animation: hst-in 0.35s cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
.hustle-title span:nth-child(2) { animation-delay: 0.04s; }
.hustle-title span:nth-child(3) { animation-delay: 0.08s; }
.hustle-title span:nth-child(4) { animation-delay: 0.12s; }
.hustle-title span:nth-child(5) { animation-delay: 0.16s; }
.hustle-title span:nth-child(6) { animation-delay: 0.2s; }
@keyframes hst-in {
  from { opacity: 0; transform: translateX(-0.45em) skewX(-14deg); }
  to { opacity: 1; transform: none; }
}
.hustle-title::before,
.hustle-title::after {
  content: "";
  position: absolute;
  left: calc(100% + 0.14em);
  height: 0.07em;
  border-radius: 999px;
  background: var(--accent);
  opacity: 0;
  animation: hst-streak 0.45s ease-out 0.3s forwards;
}
.hustle-title::before { top: 38%; width: 0.5em; }
.hustle-title::after { top: 60%; width: 0.82em; animation-delay: 0.38s; }
@keyframes hst-streak {
  from { transform: translateX(-0.5em); opacity: 0; }
  to { transform: none; opacity: 0.55; }
}

/* ---------- Castle board (Day 11) ---------- */

/* Page-scoped accent: cobalt — heraldic royal blue. Root --accent stays
   Bundle's water blue. */
body.castle {
  --accent: #1d4ed8;
  --accent-soft: rgba(29, 78, 216, 0.1);
}
[data-theme="dark"] body.castle {
  --accent: #8ab0f5;
  --accent-soft: rgba(138, 176, 245, 0.14);
}

.castle-game { max-width: 350px; }

/* Rows grow on demand inside the shared scroll window. */
.crow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.crow.quiet { animation: none; }

/* The walled tiles: colorless — they carry the letter but no mark color,
   and their border turns to dashed stonework (the pending row wears it
   too, as the player's map of what will clue this guess). */
.crow .tile.locked { border-style: dashed; }

/* A breach just opened this slot on the waiting row: the wall flashes
   cobalt and the dashes stand down. */
.crow .tile.gate-open { animation: gate-open 0.9s ease; }
@keyframes gate-open {
  0%, 100% { box-shadow: none; }
  40% { box-shadow: 0 0 16px var(--accent-soft); border-color: var(--accent); }
}

/* The castle falls: the winning row glows down its line (keyframes
   shared with Crumble/Swivel). */
.crow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title sits inside its own keep — and the whole keep is ONE
   continuous line: an inline SVG path runs up the left side, rises and
   dips across five crenels along the top, down the right side and home
   along the bottom (suitangi: the outline of crenels, connected to the
   box — not solid blocks). preserveAspectRatio="none" lets it hug the
   word at any width while vector-effect keeps the stroke a constant 2px;
   the walls draw themselves on load (pathLength + dashoffset). */
.castle-title {
  position: relative;
  width: fit-content;
  padding: 0.42em 0.32em 0.14em;
}
.castle-title .keep {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible; /* the stroke straddles the box edge */
  pointer-events: none;
}
.castle-title .keep path {
  fill: none;
  stroke: var(--muted);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: draw-walls 1.1s ease-out 0.2s forwards;
}
@keyframes draw-walls {
  to { stroke-dashoffset: 0; }
}
.castle-title span {
  display: inline-block;
  animation: castle-rise 0.35s ease both;
}
.castle-title span:nth-child(2) { animation-delay: 0.05s; }
.castle-title span:nth-child(3) { animation-delay: 0.1s; }
.castle-title span:nth-child(4) { animation-delay: 0.15s; }
.castle-title span:nth-child(5) { animation-delay: 0.2s; }
.castle-title span:nth-child(6) { animation-delay: 0.25s; }
.castle-title span:nth-child(7) { animation-delay: 0.3s; }
@keyframes castle-rise {
  from { opacity: 0; transform: translateY(0.22em); }
  to { opacity: 1; transform: none; }
}

/* Restated last and at full specificity: this game's tile animation
   classes would otherwise out-rank the shared .tile.shake and mute the
   reject shake (the Day 1 gotcha). submit() still clears shake first. */
.crow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Encircle board (Day 12) ---------- */

/* Page-scoped accent: hot magenta — the ring around the board. Root
   --accent stays Bundle's water blue. (First ship wore a teal that
   collided with Toggle's — re-accented.) */
body.encircle {
  --accent: #c026d3;
  --accent-soft: rgba(192, 38, 211, 0.1);
}
[data-theme="dark"] body.encircle {
  --accent: #e879f9;
  --accent-soft: rgba(232, 121, 249, 0.14);
}

.encircle-game { max-width: 350px; }

/* Rows grow on demand inside the shared scroll window; a pair of ring
   controls (‹ ›) flanks the tiles — visible only on the live row, so
   every board column stays aligned. */
.erow {
  display: grid;
  grid-template-columns: 28px repeat(5, 1fr) 28px;
  gap: 5px;
  align-items: stretch;
  animation: wrow-in 0.3s ease both;
}
.erow.quiet { animation: none; }

.rot {
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  border-radius: 6px;
  visibility: hidden;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.erow.on .rot { visibility: visible; }
.rot:hover { color: var(--accent); background: var(--accent-soft); }

/* The start of a word wears a small dot — scored rows keep theirs as a
   record of where that guess began. On the live row the start tile is the
   highlighted handle the arrows turn: accent ring and glow. */
.encircle-game .tile { position: relative; }
.encircle-game .tile.s0::after {
  content: "";
  position: absolute;
  top: 4px;
  left: 4px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--tile-border-filled);
}
.erow.on .tile.s0 {
  border-color: var(--accent);
  box-shadow: 0 0 9px var(--accent-soft);
}
.erow.on .tile.s0::after { background: var(--accent); }

/* The circle closes: the winning row glows down its line (keyframes
   shared with Crumble/Swivel/Castle). */
.erow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title practices the game: the letters ride ONE shared track. Each
   span is lifted out of the flow (absolute, same origin) and placed
   purely by its transform, and the word is set MONOSPACE so every glyph
   has the same advance — at a uniform 0.75em pitch the visual gaps are
   all equal (a proportional font left holes after the narrow i, r, l).
   A letter only ever fades in at the west edge of the band — never
   mid-word (an earlier per-span version wrapped each letter around its
   OWN slot, spawning ghosts in the middle of the word). The tail letter
   leads, so the ring reads "Encircle" west to east and the letter that
   wraps is the last one — like the game's shift. Per-child explicit
   delays, no calc — the Bubble gotcha. */
.encircle-title {
  position: relative;
  width: 5.9em;
  height: 1.3em;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.encircle-title span {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform, opacity;
  animation: enc-flow 7.2s linear infinite both;
}
.encircle-title span:nth-child(1) { --k: 0; animation-delay: 0s; }
.encircle-title span:nth-child(2) { --k: 1; animation-delay: -0.9s; }
.encircle-title span:nth-child(3) { --k: 2; animation-delay: -1.8s; }
.encircle-title span:nth-child(4) { --k: 3; animation-delay: -2.7s; }
.encircle-title span:nth-child(5) { --k: 4; animation-delay: -3.6s; }
.encircle-title span:nth-child(6) { --k: 5; animation-delay: -4.5s; }
.encircle-title span:nth-child(7) { --k: 6; animation-delay: -5.4s; }
.encircle-title span:nth-child(8) { --k: 7; animation-delay: -6.3s; }
@keyframes enc-flow {
  0%   { transform: translateX(-0.3em); opacity: 0; }
  8%   { opacity: 1; }
  92%  { opacity: 1; }
  100% { transform: translateX(5.7em); opacity: 0; }
}

/* An infinite animation must not survive the global reduce rule (it would
   machine-gun 0.01ms cycles): the ring stops and the letters park in
   reading position (--k places them one slot apart, west to east). */
@media (prefers-reduced-motion: reduce) {
  .encircle-title span {
    animation: none;
    transform: translateX(calc(var(--k, 0) * 0.75em));
  }
}

/* Restated last and at full specificity: this game's animation classes
   would otherwise out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.erow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Dawdle board (Day 13) ---------- */

/* Page-scoped accent: dusk indigo — the clue that arrives after dark.
   Root --accent stays Bundle's water blue. */
body.dawdle {
  --accent: #4f46e5;
  --accent-soft: rgba(79, 70, 229, 0.1);
}
[data-theme="dark"] body.dawdle {
  --accent: #818cf8;
  --accent-soft: rgba(129, 140, 248, 0.14);
}

.dawdle-game { max-width: 350px; }

/* Rows grow on demand inside the shared scroll window. */
.dwrow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.dwrow.quiet { animation: none; }

/* The word comes out at last: the winning row glows down its line
   (keyframes shared with Crumble/Swivel/Castle/Encircle). */
.dwrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title dawdles in: each letter takes a full 1.6s to surface and
   the next one doesn't even start for another 0.8s — the word takes six
   seconds to fully arrive (per-child explicit delays, no calc — the
   Bubble gotcha). A finite animation, so the global reduced-motion rule
   lands it on the final frame: the word simply shows up. */
.dawdle-title span {
  display: inline-block;
  animation: dawdle-in 1.6s ease-in both;
}
.dawdle-title span:nth-child(1) { animation-delay: 0.4s; }
.dawdle-title span:nth-child(2) { animation-delay: 1.2s; }
.dawdle-title span:nth-child(3) { animation-delay: 2s; }
.dawdle-title span:nth-child(4) { animation-delay: 2.8s; }
.dawdle-title span:nth-child(5) { animation-delay: 3.6s; }
.dawdle-title span:nth-child(6) { animation-delay: 4.4s; }
@keyframes dawdle-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Restated last and at full specificity: this game's animation classes
   would otherwise out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.dwrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Couple board (Day 14) ---------- */

/* Page-scoped accent: plum — two things tied together. Root --accent
   stays Bundle's water blue. */
body.couple {
  --accent: #7e22ce;
  --accent-soft: rgba(126, 34, 206, 0.1);
}
[data-theme="dark"] body.couple {
  --accent: #c084fc;
  --accent-soft: rgba(192, 132, 252, 0.14);
}

.couple-game { max-width: 350px; }

/* Rows grow on demand inside the shared scroll window. */
.lrow {
  position: relative;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.lrow.quiet { animation: none; }

/* The carried letter: chosen for you, wearing the couple's color until
   its tile scores. It never erases. */
.lrow .tile.carried {
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* While you type, the row's LAST tile is the letter that will carry —
   dashed accent says so (Bundle's chain-preview language). */
.lrow.on .tile:nth-child(5) {
  border-style: dashed;
  border-color: var(--accent);
}

/* The coupler: one accent line that exits the previous row's last tile,
   runs around the board's edge through the seam, and enters this row's
   first tile — the lock moves from row-end to row-start. Two halves:
   the source row draws the outgoing stub (::after), the receiving row
   draws the seam run + incoming stub (::before). */
.lrow.has-next::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -9px;
  width: 9px;
  height: calc(50% + 2.5px);
  border-top: 2px solid var(--accent);
  border-right: 2px solid var(--accent);
}
.lrow.has-prev::before {
  content: "";
  position: absolute;
  top: -2.5px;
  left: 0;
  right: -9px;
  height: calc(50% + 2.5px);
  border-top: 2px solid var(--accent);
  border-left: 2px solid var(--accent);
}

/* The word comes out at last: the winning row glows down its line
   (keyframes shared with Crumble/Swivel/Castle/Encircle/Dawdle). */
.lrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title wears the same couple: the line exits the last letter,
   wraps under the word and comes up into the first — C and e are the
   tied pair and wear the accent. */
.couple-title {
  position: relative;
  width: fit-content;
  padding: 0 0.14em 0.5em;
}
.couple-title::after {
  content: "";
  position: absolute;
  top: 54%;
  bottom: 0.14em;
  left: 0.08em;
  right: 0.08em;
  border-left: 2px solid var(--accent);
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
}
.couple-title span:first-of-type,
.couple-title span:last-of-type {
  color: var(--accent);
}

/* Restated last and at full specificity: this game's animation classes
   would otherwise out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.lrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Keyboard ---------- */

.keyboard {
  margin-top: 22px;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 6px;
  user-select: none;
}
.key-row { display: flex; gap: 5px; justify-content: center; }
.key {
  flex: 1;
  max-width: 42px;
  height: 52px;
  border: none;
  border-radius: 4px;
  background: var(--key-bg);
  color: var(--key-text);
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 700;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color 0.12s ease;
}
.key:hover { opacity: 0.85; }
.key.wide { max-width: 62px; font-size: 10.5px; }
.key.correct { background: var(--correct); color: var(--on-color); }
.key.present { background: var(--present); color: var(--on-color); }
.key.absent  { background: var(--absent);  color: var(--on-color); }

/* ---------- Game state UI ---------- */

.game-banner {
  margin-top: 18px;
  padding: 10px 18px;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  font-weight: 650;
  letter-spacing: 0.04em;
  text-align: center;
  display: none;
}
.game-banner.show { display: block; }

.game-actions { margin-top: 16px; display: flex; gap: 10px; }

.btn {
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  padding: 9px 16px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.btn:hover { border-color: var(--muted); background: var(--hover); }
.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
[data-theme="dark"] .btn.primary { color: #121213; }
.btn.primary:hover { opacity: 0.9; background: var(--accent); }
.btn.hidden { display: none; }

/* ---------- Toast ---------- */

.toast {
  position: fixed;
  left: 50%;
  bottom: 84px;
  transform: translateX(-50%);
  background: var(--text);
  color: var(--bg);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.03em;
  padding: 12px 20px;
  border-radius: 8px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
  max-width: calc(100vw - 48px);
  text-align: center;
}
.toast.show { opacity: 1; }

/* ---------- Muddle board (Day 15) ---------- */

/* Page-scoped accent: honey gold — the muddled yellow's warmer cousin.
   Root --accent stays Bundle's water blue. */
body.muddle {
  --accent: #a16207;
  --accent-soft: rgba(161, 98, 7, 0.12);
}
[data-theme="dark"] body.muddle {
  --accent: #facc15;
  --accent-soft: rgba(250, 204, 21, 0.14);
}

.muddle-game { max-width: 380px; }
.muddle-game .game-banner { margin: 16px 0 18px; }

/* The honeycomb: seven staggered rows of pointy-top hexes (5, 4, 5, 4,
   5, 4, 5), absolutely positioned by JS inside the board box. A hex tile
   is two stacked clip-path hexagons — the outer one paints the border
   color, the inner one (inset 2px) the fill — because clip-path would
   clip a real border off. The inner carries the letter and the mark. */
.muddle-board { position: relative; margin: 0 auto; }
/* Scoped to the honeycomb board: Meddle (Day 5) also names its rows
   .mrow — unscoped, this rule dragged every Meddle guess row to the top
   of the page, absolute and full-size (suitangi, post-Day-16 report). */
.muddle-board .mrow { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
.muddle-board .mrow.quiet { animation: none; }
.hex {
  position: absolute;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  background: var(--tile-border);
}
.hex-in {
  position: absolute;
  inset: 2px;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  background: var(--surface);
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  text-transform: uppercase;
  transition: background-color 0.45s ease;
}

.hex-in.correct { background: var(--correct); color: var(--on-color); }
.hex-in.present { background: var(--present); color: var(--on-color); }
.hex-in.absent { background: var(--absent); color: var(--on-color); }
/* Colorblind marks: same mechanism as the shared .tile rules (the
   .hex-in classes don't match .tile, so they need their own lines). */
[data-colorblind="on"] .hex-in.correct { background: var(--cb-correct); }
[data-colorblind="on"] .hex-in.present { background: var(--cb-present); }

/* The flip: same two-phase language as Bundle — the mark color lands at
   the half-turn (JS swaps it at FLIP_MID). */
.hex.reveal { animation: hex-flip 0.65s ease; }
@keyframes hex-flip {
  0% { transform: rotateX(0); }
  50% { transform: rotateX(90deg); }
  100% { transform: rotateX(0); }
}

/* The winning row glows across its hexes (keyframes shared with
   Crumble/Swivel/Castle/Encircle/Dawdle/Couple). */
.hex.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* Restored boards are born resolved: hexes rebuilt from the save carry
   `quiet` — no reveal, no glow replay. paintTile strips it from hexes it
   repaints, so later rows still flip. */
.hex.quiet { animation: none; }

/* Restated last and at full specificity: this game's own animation
   classes would otherwise out-rank the shared .tile.shake and mute the
   reject shake (the Day 1 gotcha). submit() still clears shake before
   revealing. */
.hex.shake { animation: shake 0.55s ease; }

/* The title: the two d's of Muddle are one d sitting in a hexagon —
   wearing the muddled yellow, the board's clue in miniature. */
.muddle-title span { display: inline-block; }
.muddle-title .mhex {
  width: 0.88em;
  height: 1em;
  margin: 0 0.03em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  background: var(--present);
  color: var(--on-color);
  animation: mhex-in 0.5s cubic-bezier(0.34, 1.45, 0.64, 1) 0.35s both;
}
@keyframes mhex-in {
  from { opacity: 0; transform: scale(0.3); }
  to { opacity: 1; transform: scale(1); }
}

/* ---------- Peddle board (Day 16) ---------- */

/* Page-scoped accent: money green. Root --accent stays Bundle's water
   blue. */
body.peddle {
  --accent: #15803d;
  --accent-soft: rgba(21, 128, 61, 0.12);
}
[data-theme="dark"] body.peddle {
  --accent: #4ade80;
  --accent-soft: rgba(74, 222, 128, 0.13);
}

.peddle-game { max-width: 380px; }
.peddle-game .game-banner { margin-top: 14px; }

/* The bankroll: the balance is the whole scoreboard. It ticks down as
   letters are typed (the running price rides the waiting row's chip,
   next to the word being typed), bumps on a refund, and turns the
   accent on a win. */
.money-bar {
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: 10px;
  user-select: none;
}
.money-label {
  display: block;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}
.money-amount {
  display: block;
  font-size: 28px;
  font-weight: 750;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.money-amount.done { color: var(--accent); }
.money-amount.bump { animation: money-bump 0.5s ease; }
@keyframes money-bump {
  35% { transform: scale(1.08); color: var(--accent); }
}

/* The price chart: four tier bands, one chip per letter. Chips light up
   as the game clues them in — yellow wash on a first yellow, green and
   "free" on a green discovery, dimmed with a struck price once the
   letter is proven absent. Chips flex per band, so each tier reads as
   one row of equal prices. */
.price-chart {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 12px;
  user-select: none;
}
.chart-row { display: flex; gap: 4px; }
.chart-tier {
  flex: none;
  width: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
}
.chip {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 6px;
  padding: 4px 0 5px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  transition: border-color 0.3s ease, background-color 0.3s ease,
    opacity 0.3s ease;
}
.chip b { font-size: 14px; font-weight: 700; line-height: 1; }
.chip i {
  font-style: normal;
  font-size: 10px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.chip.dead { opacity: 0.4; }
.chip.dead i { text-decoration: line-through; }
.chip.hit-y { border-color: var(--present); }
.chip.hit-g { border-color: var(--correct); background: var(--accent-soft); }
.chip.hit-g i { color: var(--accent); }
[data-colorblind="on"] .chip.hit-y { border-color: var(--cb-present); }
[data-colorblind="on"] .chip.hit-g { border-color: var(--cb-correct); }

/* Rows grow on demand inside the shared scroll window; the extra
   right-hand column is the price of that guess — the live tab of the
   word being typed, gross while it flips, net after the refunds land,
   accent when the guess turned a profit. flex: none, like Subtle's
   .srow — without it the scroll window flex-shrinks the rows once the
   game outgrows it and the boards squish. */
.prow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr) 50px;
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.prow.quiet { animation: none; }

.price {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  user-select: none;
}
.price.gain { color: var(--accent); }
.price.settle { animation: settle 0.45s ease; }

/* The winning row glows down its line (keyframes shared with
   Crumble/Swivel/Castle/Encircle/Dawdle/Couple/Muddle). */
.prow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title: letters rise in, and a price tag swings in off the last e —
   which really does cost $10. Direct children only: the tag is a nested
   span and must not inherit the rise (or have its swing out-ranked by a
   :nth-child delay rule). */
.peddle-title > span { display: inline-block; animation: peddle-rise 0.35s ease both; }
.peddle-title > span:nth-child(2) { animation-delay: 0.05s; }
.peddle-title > span:nth-child(3) { animation-delay: 0.1s; }
.peddle-title > span:nth-child(4) { animation-delay: 0.15s; }
.peddle-title > span:nth-child(5) { animation-delay: 0.2s; }
.peddle-title > span:nth-child(6) { animation-delay: 0.25s; }
@keyframes peddle-rise {
  from { opacity: 0; transform: translateY(0.22em); }
  to { opacity: 1; transform: none; }
}
.peddle-title .ptag-host { position: relative; }
.peddle-title .ptag {
  position: absolute;
  right: -0.5em;
  bottom: -0.6em;
  font-size: 0.28em;
  line-height: 1;
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 0.3em 0.55em 0.34em 0.85em;
  border: 1.5px solid var(--accent);
  border-radius: 0.4em;
  color: var(--accent);
  background: var(--accent-soft);
  transform-origin: 15% -30%;
  animation: ptag-swing 1s cubic-bezier(0.34, 1.4, 0.64, 1) 0.55s both;
}
/* the tag hole */
.peddle-title .ptag::before {
  content: "";
  position: absolute;
  left: 0.3em;
  top: 50%;
  width: 0.34em;
  height: 0.34em;
  margin-top: -0.17em;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid var(--accent);
}
@keyframes ptag-swing {
  0% { opacity: 0; transform: rotate(-24deg); }
  55% { opacity: 1; transform: rotate(15deg); }
  100% { transform: rotate(9deg); }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.prow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Fiddle board (Day 17) ---------- */

/* Page-scoped accent: steel — a fidgety pencil-metal gray-blue, the one
   hue family no earlier day had claimed. Root --accent stays Bundle's
   water blue. */
body.fiddle {
  --accent: #475569;
  --accent-soft: rgba(71, 85, 105, 0.1);
}
[data-theme="dark"] body.fiddle {
  --accent: #94a3b8;
  --accent-soft: rgba(148, 163, 184, 0.16);
}

.fiddle-game { max-width: 380px; }
.fiddle-game .game-banner { margin-top: 14px; }

/* Rows grow on demand inside the shared scroll window; the extra
   right-hand column is the fiddle counter — letters still owed to the
   previous guess, ticking down as they're typed, the check (accent)
   once the fiddle is satisfied. flex: none, like Subtle's .srow —
   without it the scroll window flex-shrinks the rows once the game
   outgrows it and the boards squish. */
.fdrow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr) 44px;
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.fdrow.quiet { animation: none; }

.fcount {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  user-select: none;
}
.fcount.ok { color: var(--accent); }
.fcount.settle { animation: settle 0.45s ease; }

/* While you type, tiles whose letter counts toward the previous guess
   wear the steel outline — the counter's math made visible, so the
   shared letters can be seen and counted at a glance. Pending-row voice
   only: it would fight the mark colors on scored rows, so submit()
   strips it before the reveal (it would out-rank .tile.correct). */
.fdrow .tile.shared {
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* The winning row glows down its line (keyframes shared with
   Crumble/Swivel/Castle/Encircle/Dawdle/Couple/Muddle/Peddle). */
.fdrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title fidgets: F-i-d rise in, and the last three letters — d, l,
   e — continuously type themselves out and backspace themselves away,
   like a word being fidgeted with on a keyboard. One timeline, three
   explicit keyframe sets: each letter pops in a keystroke after the
   last (d at 0s, l at 0.22s, e at 0.44s of the 3.6s cycle) and ALL
   vanish backspace-order (e first, then l, then d) because their
   disappearances are staggered the other way (66%/72%/78%). One-shot
   reduced-motion rule HERE, section-scoped: the global reduce rule only
   shortens durations, and a 0.01ms INFINITE animation machine-guns
   (the Encircle lesson) — these letters must simply sit still, whole. */
.fiddle-title > span:not(.fd) { display: inline-block; animation: fiddle-rise 0.35s ease both; }
.fiddle-title > span:not(.fd):nth-child(2) { animation-delay: 0.05s; }
.fiddle-title > span:not(.fd):nth-child(3) { animation-delay: 0.1s; }
.fiddle-title > span:not(.fd):nth-child(4) { animation-delay: 0.15s; }
.fiddle-title .fd {
  display: inline-block;
  opacity: 0;
  animation-duration: 3.6s;
  animation-timing-function: linear;
  animation-delay: 1s;
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}
.fiddle-title .fd-d { animation-name: fd-cycle-d; }
.fiddle-title .fd-l { animation-name: fd-cycle-l; }
.fiddle-title .fd-e { animation-name: fd-cycle-e; }
@keyframes fd-cycle-d {
  0% { opacity: 0; transform: translateY(0.14em); }
  3% { opacity: 1; transform: none; }
  75% { opacity: 1; transform: none; }
  78% { opacity: 0; transform: none; }
  100% { opacity: 0; }
}
@keyframes fd-cycle-l {
  0%, 6% { opacity: 0; transform: translateY(0.14em); }
  9% { opacity: 1; transform: none; }
  69% { opacity: 1; transform: none; }
  72% { opacity: 0; transform: none; }
  100% { opacity: 0; }
}
@keyframes fd-cycle-e {
  0%, 12% { opacity: 0; transform: translateY(0.14em); }
  15% { opacity: 1; transform: none; }
  63% { opacity: 1; transform: none; }
  66% { opacity: 0; transform: none; }
  100% { opacity: 0; }
}
@keyframes fiddle-rise {
  from { opacity: 0; transform: translateY(0.22em); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .fiddle-title .fd { animation: none; opacity: 1; transform: none; }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.fdrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Cycle board (Day 18) ---------- */

/* Page-scoped accent: the tile green itself — the page wears the board's
   color, since the game has no hue of its own (every family is claimed).
   Root --accent stays Bundle's water blue. */
body.cycle {
  --accent: #6aaa64;
  --accent-soft: rgba(106, 170, 100, 0.15);
}
[data-theme="dark"] body.cycle {
  --accent: #538d4e;
  --accent-soft: rgba(83, 141, 78, 0.2);
}

.cycle-game { max-width: 380px; }
.cycle-game .game-banner { margin-top: 14px; }

/* Rows grow on demand inside the shared scroll window; the extra
   right-hand column is the mode chip — a mini tile saying which mode
   that guess scored in, or for the waiting row, which mode the guess
   you're typing will score in. flex: none, like Subtle's .srow —
   without it the scroll window flex-shrinks the rows once the game
   outgrows it and the boards squish. */
.cycrow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr) 44px;
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.cycrow.quiet { animation: none; }

/* The mode chip: a mini tile in the mode's color, letter G or Y.
   Scored rows recede; the waiting row's chip stands at full strength
   with a soft ring — that's the mode you're typing into. */
.cmode {
  justify-self: center;
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 27px;
  height: 27px;
  border-radius: 6px;
  font-size: 13.5px;
  font-weight: 750;
  line-height: 1;
  color: var(--on-color);
  user-select: none;
  transition: opacity 0.3s ease, box-shadow 0.3s ease;
}
.cmode.g { background: var(--correct); }
.cmode.y { background: var(--present); }
[data-colorblind="on"] .cmode.g { background: var(--cb-correct); }
[data-colorblind="on"] .cmode.y { background: var(--cb-present); }
.cycrow:not(.on-row) .cmode { opacity: 0.55; }
.cycrow.on-row .cmode { box-shadow: 0 0 0 3px var(--accent-soft); }

/* The winning row glows down its line (keyframes shared with
   Crumble/Swivel/Castle/Encircle/Dawdle/Couple/Muddle/Peddle/Fiddle). */
.cycrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title cycles: the letters wash back and forth between the two clue
   colors, green and yellow — a phase-offset wave rolling through the
   word (negative delays; no calc-in-delay, the Bubble gotcha). The
   animation reads the mark palette vars, so it follows dark/light for
   free; colorblind mode gets its own keyframes so the title matches the
   board's orange/blue. Infinite, so reduced-motion is handled HERE:
   the global reduce rule only shortens durations and a 0.01ms INFINITE
   animation machine-guns (the Encircle lesson) — the title settles
   plain. */
.cycle-title span { display: inline-block; animation: cycle-hue 2.8s ease-in-out infinite; }
.cycle-title span:nth-child(2) { animation-delay: -0.28s; }
.cycle-title span:nth-child(3) { animation-delay: -0.56s; }
.cycle-title span:nth-child(4) { animation-delay: -0.84s; }
.cycle-title span:nth-child(5) { animation-delay: -1.12s; }
@keyframes cycle-hue {
  0%, 100% { color: var(--correct); }
  50% { color: var(--present); }
}
[data-colorblind="on"] .cycle-title span { animation-name: cycle-hue-cb; }
@keyframes cycle-hue-cb {
  0%, 100% { color: var(--cb-correct); }
  50% { color: var(--cb-present); }
}
@media (prefers-reduced-motion: reduce) {
  .cycle-title span { animation: none; color: var(--text); }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.cycrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Findle board (Day 19) ---------- */

/* Page-scoped accent: sky blue — the search-party color for a game about
   hunting through a grid. Root --accent stays Bundle's water blue. */
body.findle {
  --accent: #0284c7;
  --accent-soft: rgba(2, 132, 199, 0.12);
}
[data-theme="dark"] body.findle {
  --accent: #38bdf8;
  --accent-soft: rgba(56, 189, 248, 0.16);
}

.findle-game { max-width: 560px; }

/* Grid left, guess log right; stacks under 600px. */
.findle-cols {
  display: flex;
  gap: 16px;
  width: 100%;
  align-items: flex-start;
  justify-content: center;
}
.findle-board { flex: none; width: min(100%, 342px); }

.fgrid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 3px;
  width: 100%;
}
/* The cells are small tiles: the shared .tile palette, marks and the
   colorblind overrides carry over untouched; only the type tightens.
   Letters sit from birth — the grid is display-only; typing does the
   pointing. */
.fgrid .tile {
  font-size: clamp(11px, 3.2vw, 15px);
  font-weight: 650;
  border-width: 1px;
  border-radius: 3px;
}
/* Where the typed word reads on the board, live from the fifth letter. */
.fgrid .tile.sel {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}
/* The win glows the target's own cells (keyframes shared with every day). */
.fgrid .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

.findle-side {
  flex: 1;
  min-width: 0;
  max-width: 190px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.sideline {
  margin: 0;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The pending word: the click-selection or the letters being typed. */
.fslots {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 3px;
}
.fslots .tile {
  font-size: 15px;
  font-weight: 700;
  border-width: 1.5px;
}
.fslots.shake { animation: shake 0.55s ease; }

/* The guess log: mini tile rows, one per guess — Wordle's history, but
   each letter is graded where it sits in the grid. Scrolls once the
   guesses outgrow it. */
.findle-log {
  display: flex;
  flex-direction: column;
  gap: 3px;
  max-height: 300px;
  overflow-y: auto;
  scrollbar-width: none;
}
.findle-log::-webkit-scrollbar { display: none; }

.flogrow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 3px;
  animation: wrow-in 0.3s ease both;
}
.flogrow.quiet { animation: none; }
.flogrow .tile {
  font-size: 12px;
  font-weight: 650;
  border-width: 1px;
}

@media (max-width: 600px) {
  .findle-cols { flex-direction: column; align-items: center; }
  .findle-side { width: 100%; max-width: 342px; }
}

/* The title gets found over and over: letters rise in, then a green
   "spotted" pulse sweeps across the word (per-child explicit delays, both
   animations at once — no calc-in-delay, the Bubble gotcha). Infinite, so
   reduced-motion is handled HERE section-scoped: the global reduce rule
   only shortens durations and a 0.01ms INFINITE animation machine-guns
   (the Encircle lesson) — the title must simply sit still, plain. */
.findle-title span {
  display: inline-block;
  animation: title-rise 0.35s ease both, find-sweep 4.5s linear infinite;
}
.findle-title span:nth-child(2) { animation-delay: 0.05s, 4.32s; }
.findle-title span:nth-child(3) { animation-delay: 0.1s, 4.14s; }
.findle-title span:nth-child(4) { animation-delay: 0.15s, 3.96s; }
.findle-title span:nth-child(5) { animation-delay: 0.2s, 3.78s; }
.findle-title span:nth-child(6) { animation-delay: 0.25s, 3.6s; }
@keyframes title-rise {
  from { opacity: 0; transform: translateY(0.22em); }
  to { opacity: 1; transform: none; }
}
@keyframes find-sweep {
  0% { color: var(--text); }
  6% { color: var(--correct); }
  16%, 100% { color: var(--text); }
}
[data-colorblind="on"] .findle-title span {
  animation-name: title-rise, find-sweep-cb;
}
@keyframes find-sweep-cb {
  0% { color: var(--text); }
  6% { color: var(--cb-correct); }
  16%, 100% { color: var(--text); }
}
@media (prefers-reduced-motion: reduce) {
  .findle-title span { animation: none; color: var(--text); }
}

/* ---------- Scramble board (Day 20) ---------- */

/* Page-scoped accent: emerald — the mid-spring slot was the last free
   green (Peddle's money-green sits ~15° toward yellow, Toggle's teal ~15°
   toward blue; audited before shipping). Root --accent stays Bundle's
   water blue. */
body.scramble {
  --accent: #047857;
  --accent-soft: rgba(4, 120, 87, 0.12);
}
[data-theme="dark"] body.scramble {
  --accent: #34d399;
  --accent-soft: rgba(52, 211, 153, 0.16);
}

.scramble-game { max-width: 380px; }

/* The entry strip sits above the board: plain slots in typed order —
   dealing the letters to the numbered tiles is the board's job. The
   wider seam below separates the typing from the dealt rows. Both need
   width: 100% like every board container — inside the centered .game
   flex they shrink-wrap, and empty 1fr tiles collapse to their borders
   (the first ship's 3.2px-tiles bug). */
.sentry {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  margin-bottom: 12px;
}

.scboard {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* The seam between typing and the dealt rows. */
.deal-line {
  width: 100%;
  height: 1px;
  margin: 0 0 12px;
  background: var(--border);
}
.scrow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
}
.scrow .tile { position: relative; }

/* The deal, printed on the board: the little number in each tile's top
   corner is the typed slot that tile takes. Waiting row's numbers stand
   at full strength in the accent; scored rows' numbers recede onto the
   mark color. */
.slot-num {
  position: absolute;
  top: 2px;
  right: 3px;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  color: var(--muted);
  opacity: 0.55;
  transition: opacity 0.3s ease;
  user-select: none;
}
.scrow.on-row .slot-num { color: var(--accent); opacity: 1; }
.scrow .tile.correct .slot-num,
.scrow .tile.present .slot-num,
.scrow .tile.absent .slot-num { color: var(--on-color); opacity: 0.75; }

/* The lift: a typed answer lands in order, so its row's numbers would
   lie — fade them out with the win. */
.scrow.unscrambled .tile .slot-num { opacity: 0; }

/* The winning row glows down its line (keyframes shared with every day). */
.scrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title can't sit still: all eight letters slide to a fresh
   arrangement every 1.6s, forever. Monospace so one letter = 1ch and the
   slot offsets are exact; per-letter keyframes carry each letter through
   the shuffle — all letters move together, which is what a scramble is.
   Infinite, so reduced-motion is handled HERE section-scoped: the global
   reduce rule only shortens durations and a 0.01ms INFINITE animation
   machine-guns (the Encircle lesson) — the title must sit still. */
.scramble-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0;
}
.scramble-title span {
  display: inline-block;
  animation-duration: 6.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.scramble-title span:nth-child(1) { animation-name: scr-0; }
.scramble-title span:nth-child(2) { animation-name: scr-1; }
.scramble-title span:nth-child(3) { animation-name: scr-2; }
.scramble-title span:nth-child(4) { animation-name: scr-3; }
.scramble-title span:nth-child(5) { animation-name: scr-4; }
.scramble-title span:nth-child(6) { animation-name: scr-5; }
.scramble-title span:nth-child(7) { animation-name: scr-6; }
.scramble-title span:nth-child(8) { animation-name: scr-7; }
@keyframes scr-0 {
  0% { transform: translateX(0); }
  25% { transform: translateX(2ch); }
  50% { transform: translateX(4ch); }
  75% { transform: translateX(1ch); }
  100% { transform: translateX(0); }
}
@keyframes scr-1 {
  0% { transform: translateX(0); }
  25% { transform: translateX(-1ch); }
  50% { transform: translateX(4ch); }
  75% { transform: translateX(-1ch); }
  100% { transform: translateX(0); }
}
@keyframes scr-2 {
  0% { transform: translateX(0); }
  25% { transform: translateX(1ch); }
  50% { transform: translateX(-2ch); }
  75% { transform: translateX(3ch); }
  100% { transform: translateX(0); }
}
@keyframes scr-3 {
  0% { transform: translateX(0); }
  25% { transform: translateX(-2ch); }
  50% { transform: translateX(-2ch); }
  75% { transform: translateX(1ch); }
  100% { transform: translateX(0); }
}
@keyframes scr-4 {
  0% { transform: translateX(0); }
  25% { transform: translateX(2ch); }
  50% { transform: translateX(-2ch); }
  75% { transform: translateX(-1ch); }
  100% { transform: translateX(0); }
}
@keyframes scr-5 {
  0% { transform: translateX(0); }
  25% { transform: translateX(-1ch); }
  50% { transform: translateX(-2ch); }
  75% { transform: translateX(-3ch); }
  100% { transform: translateX(0); }
}
@keyframes scr-6 {
  0% { transform: translateX(0); }
  25% { transform: translateX(1ch); }
  50% { transform: translateX(0); }
  75% { transform: translateX(1ch); }
  100% { transform: translateX(0); }
}
@keyframes scr-7 {
  0% { transform: translateX(0); }
  25% { transform: translateX(-2ch); }
  50% { transform: translateX(0); }
  75% { transform: translateX(-1ch); }
  100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .scramble-title span { animation: none; }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.scrow .tile.shake { animation: shake 0.55s ease; }
.sentry.shake { animation: shake 0.55s ease; }

/* ---------- Tuple board (Day 21) ---------- */

/* Page-scoped accent: graphite — the colorless game wears gray. Warm
   stone against Fiddle's cool blue-slate steel (the only other neutral;
   every hue family was long claimed). Root --accent stays Bundle's
   water blue. */
body.tuple {
  --accent: #57534e;
  --accent-soft: rgba(87, 83, 78, 0.12);
}
[data-theme="dark"] body.tuple {
  --accent: #d6d3d1;
  --accent-soft: rgba(214, 211, 209, 0.14);
}

.tuple-game { max-width: 380px; }

/* Rows grow on demand inside the shared scroll window — nine deep; the
   extra right-hand column is the tuple, the game's whole voice.
   flex: none like every row in the scroll window (it flex-shrinks
   otherwise, the Peddle lesson). */
.tuprow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr) 80px;
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.tuprow.quiet { animation: none; }

/* The tuple: [yellows, greens] — two number tiles wearing the mark
   colors, framed in the tuple's literal brackets. This is the only
   color on the board. Scored rows wear it; the waiting row shows
   NOTHING — a live tuple would be a free probe (type, read, backspace,
   repeat), and the guess is the unit of measurement here. */
.ttup {
  grid-column: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  user-select: none;
}
.tsep {
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
  color: var(--muted);
}
.tnum {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 5px;
  font-size: 14px;
  font-weight: 750;
  line-height: 1;
  color: var(--on-color);
}
.tnum.y { background: var(--present); }
.tnum.g { background: var(--correct); }
[data-colorblind="on"] .tnum.y { background: var(--cb-present); }
[data-colorblind="on"] .tnum.g { background: var(--cb-correct); }

/* The numbers land as the flips settle — the row's would-be colors
   collapsing into two numbers, yellows first, greens half a beat later.
   Restored rows never wear .pop: boards are born finished. */
.ttup.pop .tnum { animation: tup-pop 0.34s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
.ttup.pop .tnum.g { animation-delay: 0.14s; }
.ttup.pop .tsep { animation: tup-sep 0.24s ease both; }
@keyframes tup-pop {
  from { transform: scale(0.3); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
@keyframes tup-sep {
  from { opacity: 0; }
}

/* The ghost answer: the loss reveal — the answer walks on wearing its
   own [0, 5], tinted in the page's graphite so it reads as reveal, not
   guess. */
.tuprow.answer .tile {
  background: var(--accent-soft);
  border-color: var(--accent);
}

/* The winning row glows down its line (keyframes shared with every
   day); the ghost answer glows too — both say "this row is the word". */
.tuprow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title wears the game's own notation: [Tup,le] — brackets and
   comma as muted punctuation, Tup on yellow tiles, le on green, the
   same number tiles the board answers with. Rise-in is one-shot, so
   the global reduced-motion rule lands it finished (no section-scoped
   rule needed — that's for INFINITE animations, the Encircle lesson). */
.tuple-title { white-space: nowrap; }
.tuple-title > span {
  display: inline-block;
  animation: tup-rise 0.45s ease both;
}
.tuple-title > span:nth-child(1) { animation-delay: 0s; }
.tuple-title > span:nth-child(2) { animation-delay: 0.06s; }
.tuple-title > span:nth-child(3) { animation-delay: 0.12s; }
.tuple-title > span:nth-child(4) { animation-delay: 0.18s; }
.tuple-title > span:nth-child(5) { animation-delay: 0.24s; }
.tuple-title > span:nth-child(6) { animation-delay: 0.3s; }
.tuple-title > span:nth-child(7) { animation-delay: 0.36s; }
.tuple-title > span:nth-child(8) { animation-delay: 0.42s; }
@keyframes tup-rise {
  from { opacity: 0; transform: translateY(0.3em) scale(0.7); }
}
.tuple-title .tup-y,
.tuple-title .tup-g {
  /* Letter-in-a-shape, per the Muddle hex / Bubble bubble mechanics:
     inline-flex so the chip's baseline IS the glyph's baseline (the
     brackets and comma stay on it; no vertical-align nudge to fight).
     The box must be taller than the inherited line-height AND hold a
     descender — the first ship's 0.94em box let the p sink out the
     bottom (Muddle's d has no descender; this word does) — so 1.1em
     with line-height: 1 to keep the line box inside. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  width: 1.05em;
  height: 1.1em;
  border-radius: 0.15em;
  margin: 0 0.015em;
  color: var(--on-color);
}
.tuple-title .tup-y { background: var(--present); }
.tuple-title .tup-g { background: var(--correct); }
[data-colorblind="on"] .tuple-title .tup-y { background: var(--cb-present); }
[data-colorblind="on"] .tuple-title .tup-g { background: var(--cb-correct); }
.tuple-title .tbr,
.tuple-title .tcm { color: var(--muted); font-weight: 600; }

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.tuprow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Middle board (Day 22) ---------- */

/* Page-scoped accent: the tile green itself — the game's soul is the
   green midpoint between gray and yellow (Cycle's precedent: every hue
   family is claimed, so the page wears the board's color). Root
   --accent stays Bundle's water blue. */
body.middle {
  --accent: #6aaa64;
  --accent-soft: rgba(106, 170, 100, 0.15);
}
[data-theme="dark"] body.middle {
  --accent: #538d4e;
  --accent-soft: rgba(83, 141, 78, 0.2);
}

.middle-game { max-width: 380px; }

/* Rows grow on demand inside the shared scroll window — the clues are
   the tiles' own marks, so this is the plain five-tile grid. Scoped to
   the game: Meddle AND Muddle own an .mrow each, the Day-15 CSS-leak
   lesson. */
.midrow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.midrow.quiet { animation: none; }

/* The winning row glows down its line (keyframes shared with every
   day); the loss's all-green ghost row wears it too. */
.midrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title sits on its own thermometer: a strip under the word running
   gray → green → yellow across the alphabet, green parked in the
   hard-stopped middle — the whole game in one bar. The h1 must be
   fit-content: gradient % stops map to the ELEMENT box, so a full-width
   h1 would stretch the bar across the page (the fade-title gotcha).
   Colorblind mode swaps orange/blue in; gray stays gray, like the
   board's marks. Static, so the global reduced-motion rule is enough. */
.middle-title {
  position: relative;
  width: fit-content;
  padding-bottom: 10px;
}
.middle-title > span {
  display: inline-block;
  animation: mid-rise 0.45s ease both;
}
.middle-title > span:nth-child(1) { animation-delay: 0s; }
.middle-title > span:nth-child(2) { animation-delay: 0.06s; }
.middle-title > span:nth-child(3) { animation-delay: 0.12s; }
.middle-title > span:nth-child(4) { animation-delay: 0.18s; }
.middle-title > span:nth-child(5) { animation-delay: 0.24s; }
.middle-title > span:nth-child(6) { animation-delay: 0.3s; }
@keyframes mid-rise {
  from { opacity: 0; transform: translateY(0.3em); }
}
.middle-title::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(90deg,
    var(--absent) 0%, var(--correct) 44%, var(--correct) 56%,
    var(--present) 100%);
}
[data-colorblind="on"] .middle-title::after {
  background: linear-gradient(90deg,
    var(--absent) 0%, var(--cb-correct) 44%, var(--cb-correct) 56%,
    var(--cb-present) 100%);
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.midrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Mingle board (Day 23) ---------- */

/* Page-scoped accent: the tile green, third wear (Cycle, Middle) — the
   minesweeper palette itself collides with every claimed accent family
   (blue→Castle, red→Stifle, indigo→Dawdle), so the page wears the
   board's green, the palette's centerpiece score. Root --accent stays
   Bundle's water blue. */
body.mingle {
  --accent: #6aaa64;
  --accent-soft: rgba(106, 170, 100, 0.15);
}
[data-theme="dark"] body.mingle {
  --accent: #538d4e;
  --accent-soft: rgba(83, 141, 78, 0.2);
}

/* The score palette in heat order (suitangi, v3): 0 gray, 1 red,
   2 yellow, 3 blue, 4 green — green means the best neighbors. 2 and 4
   wear the Wordle mark values themselves (theme-proven); 0 needs no
   var — the tile's own filled border already says "scored, nothing
   here". */
body.mingle {
  --ms1: #dc2626;
  --ms2: var(--present);
  --ms3: #2563eb;
  --ms4: var(--correct);
}
[data-theme="dark"] body.mingle {
  --ms1: #f87171;
  --ms3: #60a5fa;
}

.mingle-game { max-width: 380px; }

/* The score chart, above the board (the Peddle price-chart slot): five
   outline swatches with their numbers in, so the palette needs no
   guessing — and the numbers double as the colorblind reading. */
.mschart {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 6px;
  width: 100%;
}
.mschart .msc {
  display: inline-grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 2px solid var(--tile-border);
  border-radius: 4px;
  font-size: 14px;
  font-weight: 750;
  line-height: 1;
  color: var(--muted);
  user-select: none;
}
.mschart .msc.sc1 { border-color: var(--ms1); color: var(--ms1); }
.mschart .msc.sc2 { border-color: var(--ms2); color: var(--ms2); }
.mschart .msc.sc3 { border-color: var(--ms3); color: var(--ms3); }
.mschart .msc.sc4 { border-color: var(--ms4); color: var(--ms4); }
.mscaption {
  margin: 0 0 14px;
  font-size: 12px;
  color: var(--muted);
  text-align: center;
  letter-spacing: 0.02em;
}

/* Rows grow on demand inside the shared scroll window — plain
   five-tile rows; the outline carries everything. */
.mingrow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.mingrow.quiet { animation: none; }

/* The mingle scores ride the tile's own border, in heat order: 1 red,
   2 yellow, 3 blue, 4 green; 0 lets the filled border stand. The
   letters stay neutral — the outline is the whole message. */
.mingrow .tile.sc1 { border-color: var(--ms1); }
.mingrow .tile.sc2 { border-color: var(--ms2); }
.mingrow .tile.sc3 { border-color: var(--ms3); }
.mingrow .tile.sc4 { border-color: var(--ms4); }

/* Colorblind mode: a five-color scale can't orange/blue-swap, so the
   score shows as its number — minesweeper prints its numbers too. The
   digit rides ::after, keyed off the score class, corner-seated like
   Scramble's slot numbers; waiting tiles (no sc class) print nothing. */
.mingrow .tile { position: relative; }
[data-colorblind="on"] .mingrow .tile::after {
  position: absolute;
  top: 2px;
  right: 3px;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  color: var(--muted);
  user-select: none;
}
[data-colorblind="on"] .mingrow .tile.sc1::after { content: "1"; color: var(--ms1); }
[data-colorblind="on"] .mingrow .tile.sc2::after { content: "2"; color: var(--ms2); }
[data-colorblind="on"] .mingrow .tile.sc3::after { content: "3"; color: var(--ms3); }
[data-colorblind="on"] .mingrow .tile.sc4::after { content: "4"; color: var(--ms4); }

/* The winning row glows down its line (keyframes shared with every
   day); the ghost answer wears it too. */
.mingrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title already knows the game: six letters in outline-only tiles,
   wearing a real score line [0, 3, 2, 1, 4, 0] — all five colors in
   one word, fills never shown. (The hidden marks behind it: K,A,P,K,A,K.)
   The glyphs sit at ~0.62em inside 2em boxes so the tiles read like
   the BOARD's — half glyph, half air, board-tile padding (the first
   ship's 1.18em boxes hugged the letters; suitangi wanted the board
   look). Radius in span-em lands at the board's 3px; rise-in is
   one-shot, so the global reduced-motion rule lands it finished. */
.mingle-title { white-space: nowrap; }
.mingle-title > span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.62em;
  width: 2em;
  height: 2em;
  line-height: 1;
  border: 2px solid var(--tile-border);
  border-radius: 0.12em;
  margin: 0 3px;
  animation: ming-rise 0.45s ease both;
}
.mingle-title > span:nth-child(1) { animation-delay: 0s; }
.mingle-title > span:nth-child(2) { animation-delay: 0.06s; }
.mingle-title > span:nth-child(3) { animation-delay: 0.12s; }
.mingle-title > span:nth-child(4) { animation-delay: 0.18s; }
.mingle-title > span:nth-child(5) { animation-delay: 0.24s; }
.mingle-title > span:nth-child(6) { animation-delay: 0.3s; }
@keyframes ming-rise {
  from { opacity: 0; transform: translateY(0.3em); }
}
.mingle-title > span.sc1 { border-color: var(--ms1); }
.mingle-title > span.sc2 { border-color: var(--ms2); }
.mingle-title > span.sc3 { border-color: var(--ms3); }
.mingle-title > span.sc4 { border-color: var(--ms4); }

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.mingrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Quartile board (Day 24) ---------- */

/* Page-scoped accent: chocolate — the bar that breaks in quarters.
   Every other family was long claimed (blue→Castle/Findle/Bundle,
   orange→Hustle, terracotta→Crumble, stone→Tuple); warm cocoa is the
   free slot. Scoped on body.quartile, do NOT touch :root --accent. */
body.quartile {
  --accent: #7c2d12;
  --accent-soft: rgba(124, 45, 18, 0.12);
}
[data-theme="dark"] body.quartile {
  --accent: #d2a679;
  --accent-soft: rgba(210, 166, 121, 0.18);
}

.quartile-game { max-width: 320px; }

/* Rows grow on demand inside the shared scroll window — four tiles
   each; the stream of guesses fills them in reading order. */
.qrow {
  flex: none;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 5px;
  animation: wrow-in 0.3s ease both;
}
.qrow.quiet { animation: none; }

/* The winning row across glows (keyframes shared with every day); the
   ghost answer wears it too. */
.qrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title wraps by every 4 letters — Quar / tile — the whole game
   in one word. A 4-column grid of mono cells: each glyph is exactly
   1ch, so both lines align like the board's columns and the wrap is
   structural, not a <br>. Letters rise in reading order (one-shot, so
   the global reduced-motion rule lands it finished); the wrapped t —
   the letter that jumped to the next row — wears the accent. */
.quartile-title {
  display: grid;
  grid-template-columns: repeat(4, 1ch);
  grid-auto-rows: 1.2em;
  width: fit-content;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0;
}
.quartile-title span {
  animation: quart-rise 0.45s ease both;
}
.quartile-title span:nth-child(1) { animation-delay: 0s; }
.quartile-title span:nth-child(2) { animation-delay: 0.05s; }
.quartile-title span:nth-child(3) { animation-delay: 0.1s; }
.quartile-title span:nth-child(4) { animation-delay: 0.15s; }
.quartile-title span:nth-child(5) { animation-delay: 0.2s; }
.quartile-title span:nth-child(6) { animation-delay: 0.25s; }
.quartile-title span:nth-child(7) { animation-delay: 0.3s; }
.quartile-title span:nth-child(8) { animation-delay: 0.35s; }
.quartile-title span.wrap { color: var(--accent); }
@keyframes quart-rise {
  from { opacity: 0; transform: translateY(0.3em); }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.qrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Dividle board (Day 25) ---------- */

/* Page-scoped accent: olive — dull, yellow-leaning green, a family the
   audit never touched (Swivel's moss #4d7c0f is grass-bright, Scramble
   emerald, Peddle money-green). The line that cuts every word wears
   it. Scoped on body.dividle, do NOT touch :root --accent. */
body.dividle {
  --accent: #5e6414;
  --accent-soft: rgba(94, 100, 20, 0.14);
}
[data-theme="dark"] body.dividle {
  --accent: #c8d16a;
  --accent-soft: rgba(200, 209, 106, 0.18);
}

.dividle-game { max-width: 380px; }

/* The board is a fixed seven-row column (the Stifle shape) — every
   row's division line stays visible, no scrolling. width: 100% is
   load-bearing: .game is a centered flex column, and without it the
   board shrink-wraps and the empty aspect-ratio tiles collapse their
   1fr tracks to zero (the whole board renders pin-size). */
.dvboard {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* Each row: triangle, word, triangle (the Encircle flank grid). The
   arrows are the live row's alone; visibility keeps every board's
   columns aligned on the scored rows. */
.dvrow {
  display: grid;
  grid-template-columns: 28px 1fr 28px;
  gap: 5px;
  align-items: stretch;
  animation: wrow-in 0.3s ease both;
}
.dvrow.quiet { animation: none; }

.dvarrow {
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  visibility: hidden;
  transition: color 0.15s ease, background-color 0.15s ease, opacity 0.15s ease;
}
.dvarrow svg {
  width: 13px;
  height: 13px;
  display: block;
}
.dvarrow:hover { color: var(--text); background: var(--hover); }
.dvarrow:disabled { opacity: 0.3; cursor: default; }
.dvarrow:disabled:hover { color: var(--muted); background: transparent; }
.dvrow.on .dvarrow { visibility: visible; }

/* The word side: five tiles and the line that rides between them. The
   line's left edge sits centered in the gap after the (--d)th tile —
   tile pitch is (100% + 5px)/5 with the 5px column gap, so
   left = (--d)·pitch − half the gap − half the line. */
.dvword {
  position: relative;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
}
.dvline {
  position: absolute;
  top: -5px;
  bottom: -5px;
  width: 2px;
  border-radius: 2px;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  left: calc((100% + 5px) * var(--d, 2) / 5 - 3.5px);
  transition: left 0.18s ease, background-color 0.3s ease, box-shadow 0.3s ease;
  pointer-events: none;
}
/* A scored row's cut is part of its record — the line dims and the
   halo lifts (the ghost answer's line goes with it). */
.dvrow.scored .dvline {
  background: var(--tile-border-filled);
  box-shadow: none;
}

/* The title wears the game's own cut: Divi | dle, an accent line
   between the halves. Letters rise in, then the line grows down the
   middle (one-shot, so the global reduced-motion rule lands it
   finished). */
.dividle-title {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}
.dividle-title > span {
  display: inline-block;
  animation: div-rise 0.45s ease both;
}
.dividle-title > span:nth-child(1) { animation-delay: 0s; }
.dividle-title > span:nth-child(2) { animation-delay: 0.05s; }
.dividle-title > span:nth-child(3) { animation-delay: 0.1s; }
.dividle-title > span:nth-child(4) { animation-delay: 0.15s; }
.dividle-title > span:nth-child(5) { animation-delay: 0.2s; }
.dividle-title > span:nth-child(6) { animation-delay: 0.25s; }
.dividle-title > span:nth-child(7) { animation-delay: 0.3s; }
.dividle-title > span:nth-child(8) { animation-delay: 0.35s; }
.dividle-title > span.dline {
  width: 3px;
  height: 0.95em;
  margin: 0 0.13em;
  border-radius: 2px;
  background: var(--accent);
  transform-origin: center;
  animation: div-line 0.4s ease 0.45s both;
}
@keyframes div-rise {
  from { opacity: 0; transform: translateY(0.3em); }
}
@keyframes div-line {
  from { opacity: 0; transform: scaleY(0.15); }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.dvrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Triple board (Day 26) ---------- */

/* Page-scoped accent: denim blue — the blue board's color muted for
   buttons. (Bundle owns root water blue, Castle cobalt, Findle sky; this
   is the flat desaturated slot between them.) The three boards wear
   their identity colors from the painted map: blue / orange / purple.
   Scoped on body.triple, do NOT touch :root --accent. */
body.triple {
  --accent: #35548c;
  --accent-soft: rgba(53, 84, 140, 0.14);
  --w1: #3579bd;
  --w2: #d98a00;
  --w3: #7c3aed;
}
[data-theme="dark"] body.triple {
  --accent: #8fa8cc;
  --accent-soft: rgba(143, 168, 204, 0.16);
  --w1: #7fb0e0;
  --w2: #eda645;
  --w3: #a78bfa;
}

.triple-game { max-width: 624px; }
/* The bigger board outgrows the shared narrow page (620px). */
body.triple .page.narrow { max-width: 672px; }
.triple-game .game-banner { margin: 16px 0 18px; }
.triple-game .game-actions { flex-wrap: wrap; justify-content: center; }

/* Clue swaps fade instead of snapping as the board turns (the Swivel
   lesson). Muddle's .hex-in already carries the 0.45s ease; the keys
   need their own line. */
body.triple .key { transition: background-color 0.45s ease; }

/* The pivot is a FIXED box sized (in px, by JS) to the largest single
   view's rotated footprint, so nothing jumps between views. The render
   coords are baked rotated 30deg, so the honeycomb stands pointy-top
   with the blue and purple words reading straight across (guess-rows
   sheared half a hex below each other, the Muddle look). View 2 swivels
   +60deg from the baked frame to stand orange's columns across — and
   rotating also shifts a view's silhouette off-center, so view 2 adds
   the --tp-cx/--tp-cy re-centering translate that JS measures per fit.
   Sizing the pivot to one view instead of the union of the shifted
   boxes is what keeps it inside a phone viewport (the horizontal-scroll
   bug). The glyphs counter-rotate and stay face-up; views 1/3 sit flush
   at 0deg. Hexes reuse Muddle's .hex / .hex-in pieces (flush pointy-top
   clip-path, marks, flip, shake, colorblind). */
.tp-pivot { position: relative; margin: 0 auto; }
.tp-board {
  position: absolute;
  transform: translate(-50%, -50%) translate(0, 0) rotate(0deg);
  transition: transform 0.65s cubic-bezier(0.7, 0, 0.3, 1);
}
.tp-pivot.view-2 .tp-board {
  transform: translate(-50%, -50%)
    translate(var(--tp-cx, 0px), var(--tp-cy, 0px)) rotate(60deg);
}

/* Every glyph rides a .lt span that counter-rotates against the board,
   so letters land face-up in every view — and visibly turn during the
   swivel. */
.tp-board .lt {
  display: inline-block;
  transition: transform 0.65s cubic-bezier(0.7, 0, 0.3, 1);
}
.tp-pivot.view-2 .tp-board .lt { transform: rotate(-60deg); }

/* The board outline: one stroked SVG path per board hugging the whole
   region's silhouette (JS traces the boundary edges through the same
   rotation/fit as the tiles). Only the facing board's outline shows —
   the other two stay hidden, and the outline swivels with the board
   since it lives inside it. */
.toutline {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  display: none;
}
.toutline.w1 path { stroke: var(--w1); }
.toutline.w2 path { stroke: var(--w2); }
.toutline.w3 path { stroke: var(--w3); }
.tp-pivot.view-1 .toutline.w1,
.tp-pivot.view-2 .toutline.w2,
.tp-pivot.view-3 .toutline.w3 { display: block; }

/* The inert honeycomb ring drawn around the playable union reads dimmer
   than the game's own hexes. */
.tp-board .hex.pad { opacity: 0.4; }

/* Found words glow down their line (keyframes global from Muddle). */
.tp-board .hex.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title: letters rise in, then a tricolor bar settles underneath —
   the three boards in one strip (h1 is width: fit-content so the
   gradient maps to the word, not the page — the fade-title gotcha). */
.triple-title {
  width: fit-content;
}
.triple-title span {
  display: inline-block;
  animation: tp-in 0.5s cubic-bezier(0.34, 1.45, 0.64, 1) both;
}
.triple-title span:nth-child(2) { animation-delay: 0.06s; }
.triple-title span:nth-child(3) { animation-delay: 0.12s; }
.triple-title span:nth-child(4) { animation-delay: 0.18s; }
.triple-title span:nth-child(5) { animation-delay: 0.24s; }
.triple-title span:nth-child(6) { animation-delay: 0.3s; }
@keyframes tp-in {
  from { opacity: 0; transform: translateY(-0.35em); }
}
.triple-title::after {
  content: "";
  display: block;
  height: 4px;
  margin-top: 7px;
  border-radius: 2px;
  background: linear-gradient(to right,
    var(--w1) 0 33.4%, var(--w2) 33.4% 66.7%, var(--w3) 66.7% 100%);
}

/* Restated last and at full specificity: this game's animation classes
   must not out-rank the shared .tile.shake and mute the reject shake
   (the Day 1 gotcha). submit() still clears shake first. */
.tp-board .hex.shake { animation: shake 0.55s ease; }

/* ---------- Ordinal board (Day 27) ---------- */

/* Page-scoped accent: mauve — the dusty shelf-end of the rose family
   (Meddle owns the vivid pink; muting a claimed hue is the Triple denim
   precedent). Every free family was gone by Day 25. Root --accent stays
   Bundle's water blue. */
body.ordinal {
  --accent: #a2557f;
  --accent-soft: rgba(162, 85, 127, 0.12);
}
[data-theme="dark"] body.ordinal {
  --accent: #d8a7c5;
  --accent-soft: rgba(216, 167, 197, 0.16);
}

.ordinal-game { max-width: 380px; }

/* The entry strip sits above the board: plain slots in typed order —
   alphabetizing the letters is the board's job. The wider seam below
   separates the typing from the sorted rows. Both need width: 100%
   like every board container — inside the centered .game flex they
   shrink-wrap, and empty 1fr tiles collapse to their borders (the
   first ship's 3.2px-tiles bug). */
.orentry {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  margin-bottom: 12px;
}

.orboard {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* The seam between typing and the sorted rows. */
.sort-line {
  width: 100%;
  height: 1px;
  margin: 0 0 12px;
  background: var(--border);
}
.orow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
}

/* The winning row glows down its line (keyframes shared with every day). */
.orow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title sorts itself: the seven letters slide from the spelling
   into alphabetical order (adilnOr — the capital rides its O, the
   Scramble flying-capital-S precedent), rest there a beat, and slide
   home. Back and forth, forever. Monospace so one letter = 1ch and the
   slot offsets are exact; per-letter keyframes carry each letter's
   sorted delta — O +5, r +5, d −1, i −1, a −5, l −3. The n never moves
   (home 4 is sorted 4), so it wears the accent instead: the one letter
   the sort can't push around. Infinite, so reduced-motion is handled
   HERE section-scoped: the global reduce rule only shortens durations
   and a 0.01ms INFINITE animation machine-guns (the Encircle lesson) —
   the title must sit still. */
.ordinal-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0;
}
.ordinal-title span {
  display: inline-block;
  animation-duration: 6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.ordinal-title span:nth-child(5) { color: var(--accent); }
.ordinal-title span:nth-child(1),
.ordinal-title span:nth-child(2) { animation-name: ord-r5; }
.ordinal-title span:nth-child(3),
.ordinal-title span:nth-child(4) { animation-name: ord-l1; }
.ordinal-title span:nth-child(6) { animation-name: ord-l5; }
.ordinal-title span:nth-child(7) { animation-name: ord-l3; }
@keyframes ord-r5 {
  0% { transform: translateX(0); }
  35% { transform: translateX(5ch); }
  57% { transform: translateX(5ch); }
  100% { transform: translateX(0); }
}
@keyframes ord-l1 {
  0% { transform: translateX(0); }
  35% { transform: translateX(-1ch); }
  57% { transform: translateX(-1ch); }
  100% { transform: translateX(0); }
}
@keyframes ord-l5 {
  0% { transform: translateX(0); }
  35% { transform: translateX(-5ch); }
  57% { transform: translateX(-5ch); }
  100% { transform: translateX(0); }
}
@keyframes ord-l3 {
  0% { transform: translateX(0); }
  35% { transform: translateX(-3ch); }
  57% { transform: translateX(-3ch); }
  100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .ordinal-title span { animation: none; }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.orow .tile.shake { animation: shake 0.55s ease; }
.orentry.shake { animation: shake 0.55s ease; }

/* ---------- Aisle board (Day 28) ---------- */

/* Page-scoped accent: sea glass — the grayed shelf of Toggle's teal,
   the last quiet corner left (every vivid family was long claimed; the
   muted-shade precedent is Triple's denim). Root --accent stays
   Bundle's water blue. */
body.aisle {
  --accent: #4e7d74;
  --accent-soft: rgba(78, 125, 116, 0.12);
}
[data-theme="dark"] body.aisle {
  --accent: #9fd3c4;
  --accent-soft: rgba(159, 211, 196, 0.16);
}

.aisle-game { max-width: 380px; }

.aiboard {
  position: relative;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* The aisle itself: one continuous hairline down the middle of the
   board. The rows are positioned, so they (and their letters) paint
   over it — and since the middle tiles never take a mark color, their
   transparent centers are exactly what let the line show through. The
   aisle is a gap in the clues, not a feature, so it wears the border
   gray, not the accent. */
.aiboard::before {
  content: "";
  position: absolute;
  left: calc(50% - 1px);
  top: 3px;
  bottom: 3px;
  width: 2px;
  background: var(--border);
}
.airow {
  position: relative;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
}

/* The middle tile: born colorless, stays colorless — through the flip,
   on the win, and on the ghost row. Its letter sits muted: typed, part
   of the word, never a clue. */
.airow .tile.aicol { color: var(--muted); }

/* The winning row glows down its line (keyframes shared with every
   day) — the colorless middle glows with it. */
.airow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title plays the game: the four letters around the aisle wash
   green → yellow → green forever — the two clue colors the sides
   speak — while the middle letter sits in muted gray: the aisle never
   talks. Per-letter phase offsets via negative delays (no
   calc-in-delay); the two sides drift out of phase, the way their
   clues answer separately. Infinite, so reduced-motion is handled HERE
   section-scoped: the global reduce rule only shortens durations and a
   0.01ms INFINITE animation machine-guns (the Encircle lesson). */
.aisle-title span { display: inline-block; }
.aisle-title span:nth-child(3) { color: var(--muted); }
.aisle-title span:nth-child(1),
.aisle-title span:nth-child(2),
.aisle-title span:nth-child(4),
.aisle-title span:nth-child(5) { animation: aisle-hue 6s ease-in-out infinite; }
.aisle-title span:nth-child(2) { animation-delay: -1.5s; }
.aisle-title span:nth-child(4) { animation-delay: -0.75s; }
.aisle-title span:nth-child(5) { animation-delay: -2.25s; }
@keyframes aisle-hue {
  0%, 100% { color: var(--correct); }
  50% { color: var(--present); }
}
[data-colorblind="on"] .aisle-title span { animation-name: aisle-hue-cb; }
@keyframes aisle-hue-cb {
  0%, 100% { color: var(--cb-correct); }
  50% { color: var(--cb-present); }
}
@media (prefers-reduced-motion: reduce) {
  .aisle-title span { animation: none; color: var(--text); }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.airow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Idle board (Day 29) ---------- */

/* Page-scoped accent: sage — the grayed shelf of the greens (Swivel owns
   the moss, Aisle the sea glass, and the tile green itself is thrice
   claimed). A waiting game wears a resting color. Root --accent stays
   Bundle's water blue. */
body.idle {
  --accent: #5f7f63;
  --accent-soft: rgba(95, 127, 99, 0.12);
}
[data-theme="dark"] body.idle {
  --accent: #a6c8a1;
  --accent-soft: rgba(166, 200, 161, 0.16);
}

.idle-game { max-width: 380px; }

/* The clock sits above the marquee and is the page's whole scoreboard
   (the Hustle pattern): quiet before the run, full-strength while it
   ticks, the accent once frozen on a win. tabular-nums keeps the digits
   from jittering. */
.idle-clock {
  width: 100%;
  text-align: center;
  font-size: 28px;
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  line-height: 1.2;
  color: var(--muted);
  user-select: none;
}
.idle-clock.running { color: var(--text); }
.idle-clock.done { color: var(--accent); }
.idle-clock-hint {
  margin: 2px 0 6px;
  text-align: center;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.idle-clock-hint.hidden { display: none; }

/* The marquee: a viewport exactly the board's width on a two-copy track
   of the day's shuffled alphabet, scrolling right-to-left. The transform
   is driven by rAF in idle.js — one clock owns the scroll AND the column
   alignment (a letter lights with its mark only while it stands on the
   column where it would earn it), so CSS cannot own the animation. Edge
   masks fade the letters in and out of the window. */
.marquee {
  width: 100%;
  overflow: hidden;
  padding: 6px 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.mq-track {
  display: flex;
  width: max-content;
  will-change: transform;
}
.mq-half { display: flex; gap: 5px; padding-right: 5px; }

/* Marquee letters are the board's own tiles: same size, same pitch, real
   shared .tiles so the mark colors and the colorblind swaps come free
   (the Muddle hex-in lesson, obeyed). --mq-tile is measured from the
   viewport by JS with the same five-tiles-four-gaps math as the grid. */
.mq-tile {
  width: var(--mq-tile, 62px);
  flex: none;
}
/* Reduced motion: idle.js never translates the strip — the five visible
   tiles become the crossing window, stepping one column per tick with
   color/content changes only (no movement). The .mq-static class marks
   that mode; the visuals need no special rules. */
.marquee.mq-static { mask-image: none; -webkit-mask-image: none; }

.idboard {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-top: 8px;
}
.idrow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
}
.idrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The stage: strip + row + the Start gate. Before Start the board is a
   secret — the two rows reserve their space but show nothing, and the
   button sits centered in their place; Start reveals the board and
   starts the clock together, with no layout jump. */
.idle-stage { position: relative; width: 100%; }
.idle-stage:not(.started) .marquee,
.idle-stage:not(.started) .idboard { visibility: hidden; }
.idle-start {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 16px;
  padding: 12px 40px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The compressed reveal (the Hustle law — reveal time is clock time):
   the same flip at 0.3s; the JS colors at FLIP_MID = 150. Keep the CSS
   and the constant in sync. */
body.idle .tile.reveal { animation-duration: 0.3s; }

/* The title idles: the letters rise in, then a couple of z's drift off
   the last letter forever — the word asleep at its post. The z's are
   INFINITE, so reduced-motion is handled HERE section-scoped (the
   Encircle machine-gun lesson) and parks them invisible; the rise is
   finite, so the global reduce rule lands it finished. */
.idle-title {
  position: relative;
  width: fit-content;
}
.idle-title span {
  display: inline-block;
  animation: idle-in 0.4s ease both;
}
.idle-title span:nth-child(2) { animation-delay: 0.06s; }
.idle-title span:nth-child(3) { animation-delay: 0.12s; }
.idle-title span:nth-child(4) { animation-delay: 0.18s; }
@keyframes idle-in {
  from { opacity: 0; transform: translateY(0.35em); }
  to { opacity: 1; transform: none; }
}
.idle-title::before,
.idle-title::after {
  content: "z";
  position: absolute;
  color: var(--accent);
  font-weight: 800;
  opacity: 0;
  animation: idle-z 5.2s ease-in-out infinite;
}
.idle-title::after { right: -0.5em; top: -0.3em; font-size: 0.52em; }
.idle-title::before { right: -0.85em; top: 0.15em; font-size: 0.36em; animation-delay: -2.6s; }
@keyframes idle-z {
  0% { transform: translateY(0.5em); opacity: 0; }
  18% { opacity: 0.85; }
  50%, 100% { transform: translateY(-0.9em); opacity: 0; }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first. */
.idrow .tile.shake { animation: shake 0.55s ease; }

@media (prefers-reduced-motion: reduce) {
  .idle-title::before,
  .idle-title::after { animation: none; opacity: 0; }
}

/* ---------- Gamble board (Day 30) ---------- */

/* Page-scoped accent: wine — the dark shelf of the red-pink band
   (Stifle owns the crimson, Meddle the rose, Ordinal the mauve; the
   muting-a-claimed-hue precedent is Triple's denim). A card-table
   color for a game about the money you leave on the table. Root
   --accent stays Bundle's water blue. */
body.gamble {
  --accent: #6d2137;
  --accent-soft: rgba(109, 33, 55, 0.12);
}
[data-theme="dark"] body.gamble {
  --accent: #cf8fa1;
  --accent-soft: rgba(207, 143, 161, 0.16);
}

.gamble-game { max-width: 480px; }

/* The pot: the whole economy in one line — what's left of the 10.
   The swatch wears the page accent: a clue point has no color until
   it's spent on the board. */
.gb-budget {
  width: 100%;
  display: flex;
  justify-content: center;
  margin-bottom: 14px;
}
.gb-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: var(--muted);
}
.gb-chip b {
  font-size: 17px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.gb-chip .sw {
  width: 24px;
  height: 24px;
  border-radius: 3px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11.5px;
  font-weight: 800;
  color: var(--on-color);
  background: var(--accent);
}

/* Guess rows: five tiles and one bet stepper. The stepper is a custom
   [−] value [+] control — native spinners are tiny and vanish on some
   browsers, and the buttons are the up/down a thumb can actually hit
   (suitangi, "bigger and easier to click, especially mobile").
   flex: none — the rows sit in the shared scroll window, which would
   otherwise shrink them (the Peddle lesson). */
.gbrow {
  display: grid;
  grid-template-columns: repeat(5, 1fr) 100px;
  gap: 5px;
  align-items: center;
  flex: none;
  animation: wrow-in 0.3s ease both;
}
.gbrow.quiet { animation: none; }
.gb-step {
  display: flex;
  align-items: stretch;
  height: 52px;
  border: 2px solid var(--tile-border);
  border-radius: 6px;
  overflow: hidden;
  background: transparent;
}
.gb-step button {
  width: 30px;
  border: none;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 20px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  transition: background-color 0.12s ease;
}
.gb-step button:hover { background: var(--hover); }
.gb-step button:disabled { opacity: 0.35; cursor: default; }
.gb-step input {
  width: 100%;
  min-width: 0;
  border: none;
  background: transparent;
  text-align: center;
  font-family: inherit;
  font-size: 17px;
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  appearance: textfield;
  -moz-appearance: textfield;
}
.gb-step input::-webkit-outer-spin-button,
.gb-step input::-webkit-inner-spin-button {
  appearance: none;
  -moz-appearance: textfield;
  margin: 0;
}
.gb-step input:focus { outline: none; }
.gbrow.scored .gb-step { opacity: 0.55; }

/* The winning row glows down its line (keyframes shared with every day). */
.gbrow .tile.win-glow { animation: win-glow 1.6s ease 0.4s; }

/* The title gambles: every letter slowly turns over muted → yellow →
   green → muted — the three states of a clue you haven't bought yet.
   Per-letter phase offsets via negative delays (no calc-in-delay).
   Infinite, so reduced-motion is handled HERE section-scoped (the
   Encircle machine-gun lesson) and parks the letters in --text. */
/* The title is a slot machine: each letter is a reel of random cells
   (built by an inline script in the page) spinning at the SAME speed —
   later reels just carry more strip — and stopping in sequence, left
   to right. The static transform is the settled letter (--settle), so
   the animation only declares its "from": reduced-motion is covered by
   the global 0.01ms rule landing the finite animation finished, word
   readable. Monospace 1ch slots, the Ordinal geometry. */
.gamble-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0.02em;
}
.gamble-title .slot {
  display: inline-block;
  width: 1ch;
  height: 1em;
  overflow: hidden;
  text-align: center;
}
.gamble-title .reel {
  display: block;
  white-space: pre;
  line-height: 1;
  transform: translateY(var(--settle, 0));
  animation: gamble-reel 1.2s cubic-bezier(0.2, 0.55, 0.25, 1) both;
  will-change: transform;
}
@keyframes gamble-reel {
  from { transform: translateY(0); }
}

/* Restated last and at full specificity: any future tile animation
   classes must not out-rank the shared .tile.shake and mute the reject
   shake (the Day 1 gotcha). submit() still clears shake first — tiles
   only, the steppers are children 5 and 6 and never wear it (the
   Peddle six-children gotcha, one wider). */
.gbrow .tile.shake { animation: shake 0.55s ease; }

/* ---------- Misc ---------- */

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  .rows { scroll-behavior: auto; }
}

@media (max-width: 480px) {
  .site-header, .page, .site-footer { padding-left: 16px; padding-right: 16px; }
  .key { height: 48px; }
}
