/* Right — ad-supported niche hub
   Tokens and components lifted from the design doc (Right Mockups.dc.html). */

:root {
  --paper:   #e8e5dd;
  --card:    #f7f5f0;
  --well:    #faf9f5;
  --ink:     #16150f;
  --ink-2:   #3a382e;
  --ink-3:   #5b584c;
  --mute:    #8a8778;
  --line:    #c9c4b4;
  --green:   #1f8f5f;
  --green-d: #166a46;
  --green-x: #176e49;
  --mint:    #5fd39a;
  --mint-bg: #e9f2ec;
  --red:     #a33a2a;
  --flame:   #d9722c;

  --rule:    rgba(0, 0, 0, .12);
  --rule-2:  rgba(0, 0, 0, .07);
  --on-ink:  rgba(247, 245, 240, .72);
  --on-ink-2:rgba(247, 245, 240, .55);

  --mono: "IBM Plex Mono", ui-monospace, monospace;
  --sans: Archivo, system-ui, sans-serif;

  /* No ad unit may sit within this distance of a control surface. */
  --ad-clearance: 40px;

  /* Fluid rather than a hard 1180px cap — on anything wider than a laptop
     (1440px+, ultrawide, etc.) a fixed cap left most of the viewport as
     unused background on both sides of a centered column. This still
     floors at 1180px (today's laptop-width look is unchanged) and grows
     with the viewport up to 1600px, rather than uncapped, which would make
     line lengths and the games/rail layout absurd on a 32"+ display. */
  --page: clamp(1180px, 78vw, 1600px);
  --rail: 300px;
  /* Fluid instead of a fixed 26px: at in-between widths (roughly
     400-760px, between the two breakpoints below) this scales the side
     margin with the viewport instead of jumping straight from 16px to
     26px at 760px. */
  --gutter: clamp(16px, 4vw, 26px);
}

/* This site is a set of plain .html pages, not an SPA, so a nav click is a
   real navigation — but a real navigation doesn't have to be a hard flash
   to blank-then-repaint. The cross-document View Transitions API crossfades
   the old page out and the new one in, so a click feels like the site
   responded instead of reloading, with none of the routing/state-management
   a client-side router would need to actually stop being static pages.
   Unsupported browsers just ignore this at-rule and keep the plain jump. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

* { box-sizing: border-box; }

/* Present for assistive tech, absent from the visual layout — for a page
   title that already reads clearly from the header/hero copy on screen but
   still needs a real <h1> as the one landmark heading a screen reader's
   page-navigation relies on. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The `hidden` attribute only sets `display: none` through the user-agent
   stylesheet, so ANY class with its own `display` silently outranks it. That
   bug shipped three times here — `.auth__form`, `.settle-banner` and
   `.compare-tray` are all `display: flex`, so the login page showed both
   forms at once, the compare tray never went away and the settlement banner
   sat there empty. Scripts toggling `el.hidden` were correct; the CSS was
   ignoring them. One rule, stated once, for every element on the site.

   It is `!important` because the components it overrules use `!important`
   themselves in the mobile media queries. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--card);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--green); }
a:hover { color: var(--green-d); }

input, button { font: inherit; color: inherit; }

/* ---------- type helpers ---------- */

.eyebrow {
  font: 500 9px/1 var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--mute);
}
.eyebrow--green { color: var(--green); }
.eyebrow--mint  { color: var(--mint); }

.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.pos { color: var(--green); }
.neg { color: var(--red); }

/* ---------- shell ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 16px;
  height: 58px;
  /* Installed as a standalone PWA on iOS (apple-mobile-web-app-capable),
     the page can draw under the status bar — this padding keeps the header
     clear of it either way. env() is 0 in a normal browser tab (and with
     the non-translucent status-bar-style this site uses, iOS itself
     usually reserves the space too), so this is a harmless no-op there. */
  padding: env(safe-area-inset-top) var(--gutter) 0 var(--gutter);
  background: var(--ink);
  color: var(--card);
}
/* justify-content:space-between used to spread its leftover width evenly
   across every top-level child — brand, nav, theme-toggle, sfx-toggle,
   nav-burger — which put a ~70px gap between the avatar and the theme
   toggle (and another between the two icon buttons) while everything
   inside .site-nav itself sat a tight 26px apart. Pinning the nav (and
   everything after it) to the right edge with one auto margin instead
   means the header's own 16px `gap` is the only spacing left-over width
   can land in, so the icon row reads as one evenly spaced group. */
.site-nav { margin-left: auto; }
.site-header__brand { display: flex; align-items: baseline; gap: 14px; flex-shrink: 0; }
.wordmark {
  display: inline-block;
  font: 800 22px/1 var(--sans);
  letter-spacing: -.02em;
  color: var(--card);
  text-decoration: none;
}
.site-header__section {
  font: 500 9px/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(247, 245, 240, .5);
}
/* Eleven links plus up to two stats plus the login pill did not always fit a
   58px-tall fixed header at laptop widths — and nothing here had
   white-space:nowrap, so a squeeze did not overflow (which would at least be
   visible and fixable by scrolling) but silently wrapped a stat's text ("🔥
   streak 6" splitting mid-phrase) inside the fixed height, clipping it.
   Collapsing the eight games behind the "Games" dropdown below cut the
   top-level list to four items, which fit down to very narrow widths
   without a scroll fallback — so overflow-x/y are gone too. They used to
   clip the dropdown panel itself (overflow-y:hidden truncated anything
   that hung below the row), which a scrolling link strip never surfaced. */
.site-nav {
  display: flex;
  align-items: center;
  gap: 26px;
  font: 500 12px/1 var(--sans);
  color: var(--on-ink);
  min-width: 0;
  flex-wrap: nowrap;
}
.site-nav__links {
  display: flex;
  align-items: center;
  gap: 26px;
  min-width: 0;
  flex-wrap: nowrap;
}
.site-nav a, .site-nav__stat, .site-nav__login, .avatar {
  flex-shrink: 0;
  white-space: nowrap;
}
.site-nav a:not(.site-nav__login) { position: relative; color: var(--on-ink); text-decoration: none; }
.site-nav a:hover, .site-nav a[aria-current="page"] { color: var(--card); }
/* Underline grows from the left on hover/current rather than just appearing,
   so the nav reads as responding to the pointer rather than swapping state. */
.site-nav a:not(.site-nav__login)::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -8px;
  height: 2px;
  background: var(--mint);
  transform: scaleX(0);
  transform-origin: left;
}
.site-nav a:not(.site-nav__login):hover::after,
.site-nav a[aria-current="page"]::after { transform: scaleX(1); }
.site-nav__games { position: relative; }
.site-nav__games-trigger {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.site-nav__caret { font-size: 9px; transform: translateY(1px); }
.site-nav__games-trigger[aria-expanded="true"] .site-nav__caret { transform: translateY(1px) rotate(180deg); }
/* A single-column list of badge+label rows, same shape as
   .account-menu__panel (theme-aware card, not a hardcoded dark box) rather
   than the cramped 2-column grid of bare text this used to be. */
.site-nav__games-panel {
  position: absolute;
  top: calc(100% + 16px);
  left: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  width: 200px;
  padding: 6px;
  background: var(--card);
  border: 1px solid var(--rule);
  box-shadow: 0 14px 28px rgba(0, 0, 0, .18);
  border-radius: 10px;
}
.site-nav__games-heading {
  padding: 10px 10px 4px;
  font: 700 10px/1 var(--mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--mute);
}
.site-nav__games-heading:not(:first-child) { margin-top: 2px; border-top: 1px solid var(--rule); }
.site-nav__games-panel a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 6px;
  font: 500 13px/1.2 var(--sans);
  color: var(--ink-2);
}
.site-nav__games-panel a::after { display: none; }
.site-nav__games-panel a:hover, .site-nav__games-panel a[aria-current="page"] {
  background: var(--well);
  color: var(--ink);
}
.site-nav__login {
  padding: 7px 14px;
  border: 1px solid rgba(247, 245, 240, .3);
  color: var(--card) !important;
}
.site-nav__login:hover { background: rgba(247, 245, 240, .12); border-color: rgba(247, 245, 240, .5); }
.site-nav__login:focus-visible { outline: 2px solid var(--mint); outline-offset: 2px; }
.site-nav__stat { font: 500 11px/1 var(--mono); color: var(--mint); }
.account-menu { position: relative; flex-shrink: 0; }
.avatar {
  width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(247, 245, 240, .16);
  font: 600 10px/1 var(--mono);
  color: var(--card);
  border: 0;
  cursor: pointer;
  transition: transform .15s ease, background-color .15s ease;
}
.avatar:hover { background: rgba(247, 245, 240, .3); }

.account-menu__panel {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 50;
  width: 240px;
  max-height: calc(100vh - 80px);
  overflow-y: auto;
  padding: 14px;
  background: var(--card);
  border: 1px solid var(--rule);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
  text-align: left;
  color: var(--ink);
}
.account-menu__divider { margin: 12px 0; border: 0; border-top: 1px solid var(--rule); }
.account-menu__label--section { margin-bottom: 2px; }
.account-menu__label--danger { color: var(--red); }
.account-menu__head {
  margin-bottom: 10px;
  font: 500 11px/1 var(--mono);
  color: var(--mute);
}
.account-menu__form { display: flex; flex-direction: column; gap: 6px; }
/* display:block matters here, not just cosmetic: a <label> is inline by
   default and would otherwise run straight into the input right after it
   on the same line instead of sitting above it. */
.account-menu__label { display: block; font: 500 10px/1 var(--mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: .06em; }
.account-menu__input {
  padding: 8px 10px;
  font: 500 13px/1 var(--sans);
  color: var(--ink);
  background: var(--well);
  border: 1px solid var(--rule);
}
.account-menu__input:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.account-menu__select { cursor: pointer; margin-bottom: 4px; }
.account-menu__note { font: 500 11px/1.4 var(--mono); color: var(--red); }
.account-menu__save {
  margin-top: 4px;
  padding: 8px 0;
  background: var(--ink);
  color: var(--card);
  border: 0;
  font: 600 12px/1 var(--sans);
  cursor: pointer;
  transition: background-color .15s ease;
}
.account-menu__save:hover { background: var(--green); }
.account-menu__save:disabled { opacity: .6; cursor: default; }
.account-menu__delete { background: var(--red); }
.account-menu__delete:hover { background: #a33a2a; }

/* Sits outside <nav data-nav> — see theme-toggle.js for why — so it needs
   its own flex-shrink protection the way the nav items got theirs.

   No margin-left here: .site-header is already a flex row with its own
   16px `gap`, so a further per-button margin used to stack on top of that
   gap instead of replacing it — 16px from .site-header plus 14px of margin
   put 30px between the avatar and this button, and another 30px between
   this one and the sound toggle (which shares this class), while every
   other pair of controls sat 16–26px apart. Letting the header's own gap be
   the only source of spacing is what makes the three right-hand controls
   read as one evenly spaced group instead of a wider, unrelated gap. */
.theme-toggle {
  flex-shrink: 0;
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(247, 245, 240, .1);
  border: 1px solid rgba(247, 245, 240, .25);
  border-radius: 50%;
  color: var(--card);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.sfx-toggle svg { transition: transform .18s ease; }
/* A quick spring-y pop on the icon itself when sound is actually toggled —
   the button already fades/scales on hover via the shared .theme-toggle
   transition below, but the innerHTML swap between the ON/OFF glyphs used
   to be an instant, un-animated cut. This runs once per toggle (see
   sfx.js's paint(animate)), not on every render, so it never fights with
   the hover/active transform. */
@keyframes sfx-pop {
  0% { transform: scale(.55) rotate(-8deg); opacity: .35; }
  55% { transform: scale(1.18) rotate(3deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}
@media (prefers-reduced-motion: no-preference) {
  .sfx-toggle--pop svg { animation: sfx-pop .32s cubic-bezier(.34, 1.56, .64, 1); }
}

/* Mobile menu toggle. Hidden above the 760px breakpoint, where the plain
   nav links are shown inline instead — see the media query further down.
   Its own margin-left:auto (not just .site-nav's) matters once the panel
   opens: .site-nav--open goes position:fixed, leaving the header flex row
   the burger, which would otherwise snap back next to the brand. */
.nav-burger {
  display: none;
  flex-shrink: 0;
  margin-left: auto;
  width: 30px; height: 30px;
  align-items: center; justify-content: center;
  background: rgba(247, 245, 240, .1);
  border: 1px solid rgba(247, 245, 240, .25);
  border-radius: 50%;
  color: var(--card);
  cursor: pointer;
}

.page {
  /* --page floors at 1180px for the laptop-and-up look, but that floor is
     wider than any tablet (iPad tops out at 1024px landscape) — capping
     with 100% here is what stops the page forcing a horizontal scrollbar
     there instead of just narrowing like every other breakpoint does. */
  max-width: min(100%, var(--page));
  margin: 0 auto;
  background: var(--card);
  border-inline: 1px solid var(--rule);
  min-height: 100vh;
}

/* main column + sticky ad rail */
/* Column gap is the ad clearance, not a visual choice: the rail holds ad
   units and the main column holds control surfaces, so the two columns must
   stay 40px apart horizontally. Same for the filter column. */
.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--rail);
  column-gap: var(--ad-clearance);
  row-gap: 30px;
  padding: var(--gutter);
}
.layout--filters {
  grid-template-columns: 210px minmax(0, 1fr) var(--rail);
}
/* Pages with no rail at all. The leaderboard, stats hub and achievements
   carry no ad inventory and no "also open" card, so reserving --rail beside
   them left a 300px empty column. One track instead, which is also what
   every page collapses to below 760px. */
.layout--full {
  grid-template-columns: minmax(0, 1fr);
}
/* The rail.

   Left as the whole column being sticky, and that is a decision rather than
   an oversight — see the README. A sticky element taller than the viewport
   has nowhere to pin, so on pages whose rail stack runs past ~900px this
   scrolls away at 1440x900 instead of holding.

   The obvious fix — make only the 300x600 unit sticky and order it last so it
   cannot overlap the panels above it — was built, measured, and reverted: the
   suite reported `lag=0`, meaning the unit never reached its pin before the
   layout ended. It moved the unit down the page and changed nothing else.
   Making it genuinely stick needs the rail stack to fit the viewport, which
   means dropping a panel or shrinking the unit below the 300x600 the design
   specifies. That is a product decision, not a CSS one. */
.rail {
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-self: start;
  position: sticky;
  top: 16px;
}

/* Two visual groups instead of one flat row of same-weight spans: the
   page's small facts (muted, informational) sit above the actual nav —
   legal links and "How this works" — which gets its own row with a
   divider so it reads as a distinct, more prominent group rather than
   competing with the facts for attention. Pages with only links (the
   legal pages themselves) get .site-footer__links alone; :only-child
   drops the divider since there's nothing above it to separate from. */
.site-footer {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0 var(--gutter);
  padding: 22px 0 28px;
  border-top: 1px solid var(--rule);
  font: 400 12px/1.5 var(--mono);
  color: var(--ink-3);
}
/* ---------- footer sitemap ----------
   Brand + two link columns, above the existing facts/links rows (unchanged
   below — same page-specific footnotes, same JS mount points for "Ad
   settings"/"How this works"). A real sitemap rather than the single thin
   links row this used to be, per the redesign brief. */
.site-footer__sitemap {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr;
  gap: 24px;
  padding-bottom: 22px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--rule-2);
}
.site-footer__brand p { margin: 8px 0 0; max-width: 32ch; color: var(--mute); }
.site-footer__col { display: flex; flex-direction: column; gap: 9px; }
.site-footer__heading {
  font: 500 10px/1 var(--sans);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 3px;
}
.site-footer__col a { color: var(--ink-2); text-decoration: none; }
.site-footer__col a:hover { color: var(--green); text-decoration: underline; }

.site-footer__facts,
.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 20px;
}
.site-footer__facts { color: var(--mute); }
/* index.html's two facts spans ship [hidden] until stats.js gets a server
   answer — without this, an offline visit left an empty row taking up the
   gap above the links row for no visible reason. */
.site-footer__facts:not(:has(span:not([hidden]))) { display: none; }
.site-footer__links {
  padding-top: 12px;
  border-top: 1px solid var(--rule-2);
}
.site-footer__links a {
  color: var(--ink-2);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--rule-2);
  text-underline-offset: 3px;
  transition: color .15s ease, text-decoration-color .15s ease;
}
.site-footer__links a:hover { color: var(--green); text-decoration-color: var(--green); }

/* ---------- ad wells ---------- */

/* Temporarily off — all ad wells hidden site-wide while there's no real ad
   inventory to fill them, so current users aren't looking at rows of blank
   reserved boxes. Delete this rule to bring them back; nothing else here
   changed, so slot geometry/fill logic picks back up unmodified. */
.ad-well { display: none !important; }
.ad-well {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-block: var(--ad-clearance);
}
.ad-well--billboard { width: 970px; max-width: 100%; height: 90px; margin-inline: auto; }
.ad-well--halfpage  { height: 600px; }
.ad-well--mpu       { height: 250px; }

/* Real in-feed inventory (AdSense auto-format) must not sit in a
   fixed-height box — a fixed height can crop or stretch the served
   creative. This well is left to size to its content instead. */
.ad-well--variable  { height: auto; min-height: 90px; padding-block: 8px; }
.ad-well--halfpage.ad-well--variable { min-height: 600px; }
.ad-well--mpu.ad-well--variable { min-height: 250px; }

/* The rail wells are flush inside the rail stack; the stack itself is clear
   of controls because it lives in its own column. */
.rail .ad-well { margin-block: 0; }

.only-mobile { display: none !important; }

/* ---------- generic surfaces ---------- */

.panel {
  background: #fff;
  border: 1px solid rgba(0, 0, 0, .1);
  padding: 20px;
}
/* .panel itself carries no margin — inside a flex row (.streak-row, .rail)
   that would double up with the row's own `gap`. Scoped to direct <main>
   children instead, where two panels can land back-to-back with nothing
   between them (leaderboard's "Private groups" + "How ranking works", the
   equivalent stacked panels elsewhere) and previously touched with only
   their 1px borders separating them. */
/* The notes that close a game page.

   Every game already opens with a rule or two — enough to start playing. This
   is the other half: what the score actually means, why the daily is the same
   board for everyone, and what a round is worth signed out. It sits at the end
   of <main>, so it is above the footer ad unit rather than below it, and it is
   the last thing on the page for a reason — someone who wants to play should
   not have to scroll past an essay to reach the board.

   Deliberately small: one heading and a short paragraph or two. A game page
   whose text outweighs its game has stopped being a game page. */
.game-notes { margin-top: 28px; }
.game-notes h2 {
  font-size: 15px;
  margin: 0 0 10px;
  letter-spacing: -.01em;
}
.game-notes p {
  /* Same face and colour as .auth__note, which is what every game's opening
     rules use — the notes read as the other end of the same voice, a step
     quieter than body copy. A hair larger and looser than .auth__note because
     this is several sentences to be read, not one line to be glanced at. */
  font: 400 13px/1.68 var(--mono);
  color: var(--ink-3);
  margin: 0 0 10px;
  max-width: 62ch;
}
.game-notes p:last-child { margin-bottom: 0; }
.game-notes strong { color: var(--ink); }

main > .panel + .panel {
  margin-top: 20px;
}
.panel--dark { background: var(--ink); color: var(--card); border: 0; }
.panel--dark .eyebrow { color: var(--mint); }

.hero { max-width: 580px; margin-bottom: 28px; }
.hero h1 {
  margin: 0 0 12px;
  font: 700 42px/1.08 var(--sans);
  letter-spacing: -.025em;
  text-wrap: pretty;
}
.hero p { margin: 0; font: 400 15px/1.6 var(--sans); color: var(--ink-3); }

.page-title {
  margin: 0 0 22px;
  font: 700 34px/1.1 var(--sans);
  letter-spacing: -.025em;
}

.grid-2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
/* The homepage's two rows (games, tools) are separate grids, so equal-height
   rows within one grid does not equalise across both — a game card's extra
   header line otherwise makes it taller than a tool card. Pinned explicitly
   instead, with the CTA pushed to the bottom so shorter copy does not leave
   it floating mid-card.

   align-self: start matters here — without it, Grid's default row-stretch
   makes the plain print-cost card fill its cell to match the taller
   entry-card-wrap (switches card + guide link) beside it, so the visible
   border box would be a different size from every other card even though
   the min-height below is identical on all of them. */
.grid-2 .entry-card {
  display: flex;
  flex-direction: column;
  align-self: start;
  min-height: 250px;
}
.grid-2 .entry-card__cta { margin-top: auto; }

/* A link inside a link is invalid HTML — browsers silently split the outer
   anchor at the nested one, which used to be invisible under a 1px border
   but is not something to build on. This wrapper keeps the card and its
   secondary link as one grid cell without nesting the two anchors. */
.entry-card-wrap { display: flex; flex-direction: column; gap: 10px; }
.entry-card-wrap .entry-card { flex: 1; }
.entry-card__guide-link { font: 400 13px/1.4 var(--sans); color: var(--ink-3); }
.entry-card__guide-link:hover { color: var(--green); }

.entry-card__kicker { margin-bottom: 12px; }
.entry-card__title {
  font: 600 21px/1.15 var(--sans);
  letter-spacing: -.015em;
  margin-bottom: 8px;
}
.entry-card__body {
  font: 400 13px/1.55 var(--sans);
  color: var(--ink-3);
  margin-bottom: 14px;
}
.panel--dark .entry-card__body { color: rgba(247, 245, 240, .68); }
.entry-card__cta { font: 500 11px/1 var(--mono); transition: font-size .15s ease, color .15s ease; }
/* Today's puzzle not played yet — the CTA is the thing that should catch
   the eye, so it grows and picks up the accent color instead of just
   sitting there the same size as an already-played one. */
.entry-card__cta--pending {
  font: 700 15px/1.2 var(--sans);
  color: var(--mint);
  letter-spacing: -.01em;
}
a.entry-card { display: block; text-decoration: none; color: var(--ink); }
/* Must out-specify a.entry-card, or the dark cards inherit ink-on-ink. */
a.entry-card.panel--dark { color: var(--card); }
a.entry-card:hover { border-color: var(--green); }
a.entry-card.panel--dark:hover { outline: 1px solid var(--green); }

.entry-card__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 12px;
}
.entry-card__reset { font: 500 10px/1 var(--mono); color: var(--on-ink-2); }

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

.btn {
  display: inline-block;
  padding: 10px 18px;
  font: 600 12px/1 var(--sans);
  background: var(--well);
  color: var(--ink);
  border: 0;
  cursor: pointer;
  text-decoration: none;
}
/* The bare .btn (no --primary/--ghost modifier) never set its own
   background/color above until now — every other use on the site always
   layers one of those two modifiers on top, which masked it, but with
   neither, the button fell back to the browser's native gray widget
   chrome. Admin's plain "Sign out"/"Search" buttons are the one place
   that actually renders bare .btn, and in dark mode that left near-white
   text (correctly following the page's dark-mode color) sitting on that
   unstyled light-gray native background — unreadable. */
.btn--primary { background: var(--green); color: #fff; }
.btn--ghost {
  background: transparent;
  border: 1px solid rgba(247, 245, 240, .3);
  color: var(--card);
}
.btn[disabled] { cursor: default; opacity: .55; }

/* ---------- shared game engine ---------- */

.signin-nudge {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 18px;
  background: var(--ink);
  color: var(--card);
  margin-bottom: 20px;
}
.signin-nudge__text { font: 400 12px/1.5 var(--mono); color: var(--on-ink); }
.signin-nudge__text strong { color: var(--card); font-weight: 600; }
.signin-nudge__actions { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
.signin-nudge__dismiss {
  background: none;
  border: 0;
  color: var(--on-ink-2);
  font: 400 18px/1 var(--sans);
  cursor: pointer;
  padding: 4px;
}
.signin-nudge__dismiss:hover { color: var(--card); }

.settle-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 16px;
  background: var(--mint-bg);
  border: 1px solid rgba(31, 143, 95, .3);
  margin-bottom: 20px;
  font: 500 11px/1.4 var(--mono);
  color: var(--green-x);
}

.call-card {
  border: 1px solid var(--rule);
  background: #fff;
  padding: 26px;
}
.call-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 4px;
}
.call-card__symbol {
  font: 600 34px/1 var(--mono);
  letter-spacing: -.02em;
}
.call-card__symbol--sm { font-size: 30px; }
.call-card__sub { font: 400 13px/1.4 var(--sans); color: var(--ink-3); margin-top: 6px; }
.call-card__price { font: 600 26px/1 var(--mono); }
.call-card__sub--warn { color: var(--red); }
.call-card__delta { font: 500 12px/1 var(--mono); margin-top: 7px; }

.spark {
  margin: 22px 0;
  height: 120px;
  position: relative;
  background: var(--well);
  border: 1px solid var(--rule-2);
}
.spark__mid { position: absolute; left: 0; right: 0; top: 52%; border-top: 1px dashed rgba(0, 0, 0, .2); }
.spark__fill { position: absolute; inset: 8px; opacity: .75; }
.spark__label {
  position: absolute; left: 10px; top: 8px;
  font: 500 9px/1 var(--mono);
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--mute);
}

.calls { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
.calls--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.call-btn {
  padding: 22px;
  text-align: center;
  border: 0;
  cursor: pointer;
  background: var(--ink);
  color: var(--card);
}
.calls--three .call-btn { padding: 20px; }
.call-btn__label { display: block; font: 700 22px/1 var(--sans); letter-spacing: -.02em; }
.calls--three .call-btn__label { font-size: 19px; }
.call-btn__note { display: block; font: 500 10px/1 var(--mono); margin-top: 9px; opacity: .65; }
.call-btn--yes { background: var(--green); color: #fff; }
.call-btn--yes .call-btn__note { opacity: .85; }
.call-btn--neutral {
  background: var(--well);
  color: var(--ink);
  border: 1px solid rgba(0, 0, 0, .18);
}
.call-btn--neutral .call-btn__note { color: var(--mute); opacity: 1; }
.call-btn[aria-pressed="true"] { outline: 3px solid var(--ink); outline-offset: 2px; }
.call-btn[disabled] { cursor: default; filter: grayscale(.55); opacity: .6; }
.call-btn[disabled][aria-pressed="true"] { filter: none; opacity: 1; }

.call-meta {
  margin-top: 14px;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  font: 400 11px/1.4 var(--mono);
  color: var(--mute);
}

/* Confirms or explains the last Buy/Hold/Sell click — separate from
   .call-meta's lock-state line so a "Bought $500 of SOL" (or "Not enough
   cash") message can sit alongside "Locked — settles..." instead of being
   overwritten by it. */
.trade-feedback {
  margin: 10px 0 0;
  padding: 10px 12px;
  font: 500 12px/1.5 var(--sans);
  border-radius: 6px;
}
.trade-feedback--good { background: var(--mint-bg); color: var(--green); }
.trade-feedback--bad { background: #fbe9e6; color: var(--red); }
.trade-feedback--neutral { background: var(--well); color: var(--ink-2); }

/* streak strip */
.streak-row { display: flex; gap: 16px; margin-top: 20px; }
.streak-row > * { flex: 1; }
.streak-cells { display: flex; gap: 4px; flex-wrap: wrap; }
.streak-cell { width: 15px; height: 26px; background: var(--line); }
.streak-cell--hit { background: var(--green); }
.streak-cell--today { background: transparent; border: 1px dashed rgba(0, 0, 0, .3); }
.streak-caption { margin-top: 11px; font: 400 11px/1 var(--mono); color: var(--ink-3); }
.rank { font: 600 26px/1 var(--mono); }
.rank-note { margin-top: 9px; font: 400 11px/1.4 var(--mono); color: var(--ink-3); }

/* ---------- switch comparator ---------- */

.filters__title {
  font: 600 18px/1.1 var(--sans);
  letter-spacing: -.015em;
  margin-bottom: 18px;
}
.filters__stack { display: flex; flex-direction: column; gap: 22px; }
.filters__group .eyebrow { display: block; margin-bottom: 11px; }

.check-list { display: flex; flex-direction: column; gap: 8px; }
.check {
  display: flex; gap: 9px; align-items: center;
  font: 400 13px/1 var(--sans);
  color: var(--ink-2);
  cursor: pointer;
}
.check input { position: absolute; opacity: 0; width: 0; height: 0; }
.check__box {
  width: 12px; height: 12px;
  border: 1px solid rgba(0, 0, 0, .35);
  flex: none;
}
.check input:checked + .check__box { border-color: var(--green); background: var(--green); }
.check input:focus-visible + .check__box { outline: 2px solid var(--ink); outline-offset: 2px; }
.check:hover .check__box { border-color: var(--green); }
.check:hover { color: var(--ink); }

.range { margin: 16px 0 10px; }
.range input {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 3px; margin: 8px 0;
  background: rgba(0, 0, 0, .14);
  cursor: pointer;
}
.range input::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--ink); border: 0; cursor: pointer;
}
.range input::-moz-range-thumb {
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--ink); border: 0; cursor: pointer;
}
.range__value { font: 500 11px/1 var(--mono); color: var(--ink-2); }

.chips { display: flex; gap: 6px; flex-wrap: wrap; }
/* Eight boards, forced into two even rows of four rather than whatever the
   container's width happens to wrap at. */
#board-switch { display: grid; grid-template-columns: repeat(4, max-content); gap: 6px; }
.chip {
  padding: 7px 9px;
  font: 500 11px/1 var(--mono);
  border: 1px solid rgba(0, 0, 0, .2);
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
  /* Every other custom control here (call-btn, the .input inputs, .stick,
     .to-top, .consent__btn) resets the browser's own focus ring and
     replaces it with a :focus-visible one; .chip never did, so clicking one
     — which focuses a <button> in Firefox and Safari, not just tabbing to
     it — could pop the browser's default rectangular ring onto a
     border-radius:999px pill for an instant. Against a pill that's a
     visible, oddly-shaped flash right where the click landed, which is
     what "the chips flicker" was describing. */
  outline: none;
}
.chip:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.chip:hover:not([aria-pressed="true"]) { border-color: var(--green); color: var(--ink); background: var(--mint-bg); }
.chip[aria-pressed="true"] { background: var(--ink); color: var(--card); border-color: var(--ink); }
.chip[aria-pressed="true"]:hover { background: var(--green-d); border-color: var(--green-d); }

.filters__reset {
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  font: 500 11px/1 var(--mono);
  color: var(--green);
  background: none;
  border-inline: 0; border-bottom: 0;
  text-align: left;
  cursor: pointer;
}
.filters__reset:hover { color: var(--green-d); text-decoration: underline; }

/* Only rendered where the filters column stacks above the results (the
   .only-mobile base class hides it above 1100px, same threshold
   .layout--filters collapses to one column at) — a long filter form on a
   phone otherwise pushes the actual table a full screen down with nothing
   marking that it's there. */
.filters__jump {
  margin-top: 14px;
  padding: 13px;
  text-align: center;
  background: var(--ink);
  color: var(--card);
  font: 600 12px/1 var(--sans);
  text-decoration: none;
}
.filters__jump:hover { background: var(--green); color: #fff; }

/* Below 760px (see the media query further down) this collapses the five
   board chips behind a burger-style toggle instead of letting them wrap
   into a ragged second row — .board-switch__toggle stays display:none until
   then, and .chips#board-switch stays visible until then, so desktop is
   entirely unaffected by any of this. */
.board-switch { position: relative; }
.board-switch__toggle {
  display: none;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 12px;
  background: transparent;
  border: 1px solid rgba(0, 0, 0, .2);
  color: var(--ink-2);
  font: 500 12px/1 var(--mono);
  cursor: pointer;
}
.board-switch__toggle svg { transition: transform .15s ease; flex-shrink: 0; }
.board-switch--open .board-switch__toggle svg { transform: rotate(180deg); }

.results-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}

.board-filter {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}
.board-filter__label {
  font: 500 10px/1 var(--mono);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: .06em;
}
/* Wraps just the <select> so the hand-drawn chevron below can be
   positioned against it, matching the sudoku page's own difficulty
   select (.sudoku__difficulty-select) so the same control reads as one
   design wherever it appears. */
.board-filter__select-wrap { position: relative; display: inline-flex; }
.board-filter__select-wrap::after {
  content: "";
  position: absolute;
  right: 10px;
  top: 50%;
  width: 6px;
  height: 6px;
  margin-top: -5px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  color: var(--ink-3);
  pointer-events: none;
}
.board-filter__select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding: 6px 26px 6px 10px;
  font: 500 12px/1 var(--mono);
  color: var(--ink);
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 6px;
  cursor: pointer;
  transition: border-color .15s ease;
}
.board-filter__select:hover { border-color: var(--ink-3); }
.board-filter__select:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.results-count { font: 600 18px/1.1 var(--sans); letter-spacing: -.015em; }
.results-sort { font: 500 11px/1 var(--mono); color: var(--mute); }

.table { border: 1px solid var(--rule); background: #fff; }

/* Groups the board switcher, the scope (everyone/group) and the table
   itself into one card instead of three same-weight elements loose on the
   page — it reads as one unit (the standings) sitting next to the other
   panel below (private groups) rather than blending into the page. */
.leaderboard-panel { margin-bottom: 20px; }
.leaderboard-panel .board-scope { margin-bottom: 14px; }
.leaderboard-panel .table { border-color: var(--rule-2); }
.trow {
  display: grid;
  grid-template-columns: 24px 1.6fr .8fr .6fr .7fr .7fr .6fr;
  gap: 10px;
  padding: 12px 14px;
  align-items: center;
  border-bottom: 1px solid var(--rule-2);
}
.trow:last-child { border-bottom: 0; }
.trow--head {
  padding: 11px 14px;
  background: #f0ede6;
  border-bottom: 1px solid var(--rule);
  font: 500 9px/1 var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #6b6858;
}
.trow--picked { background: var(--mint-bg); }
.trow__name { font: 500 13px/1.2 var(--sans); }
.trow__cell { font: 400 12px/1 var(--mono); color: var(--ink-2); }
.trow .check__box { cursor: pointer; }
.table__empty { padding: 26px 14px; font: 400 13px/1.5 var(--sans); color: var(--ink-3); }

.compare-tray {
  margin: 24px var(--gutter) 0;
  padding: 16px var(--gutter);
  background: var(--ink);
  color: var(--card);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}
.compare-tray__items { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.tray-chip {
  padding: 7px 11px;
  background: rgba(247, 245, 240, .1);
  font: 500 12px/1 var(--sans);
  border: 0;
  color: var(--card);
  cursor: pointer;
}
.read-next { display: flex; flex-direction: column; gap: 9px; font: 400 13px/1.4 var(--sans); }

/* ---------- print cost calculator ---------- */

.calc-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 26px; }
.calc-col { display: flex; flex-direction: column; gap: 18px; }
/* min-width:0 on both: a grid/flex item's default min-width is its
   content's min-content size, so the unbreakable text in .input__suffix
   ("$/kWh" and friends) was quietly setting a floor on .input-pair's track
   width wider than a 320px viewport could actually give it — the classic
   grid-item overflow gotcha, not a fixed width anywhere admitting it. */
.field { min-width: 0; }
.field .eyebrow { display: block; margin-bottom: 8px; }
.input {
  display: flex;
  min-width: 0;
  border: 1px solid rgba(0, 0, 0, .2);
  background: #fff;
}
.input input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: 0;
  padding: 12px 13px;
  font: 500 16px/1 var(--mono);
  background: transparent;
}
.input input:focus-visible { outline: 2px solid var(--green); outline-offset: -2px; }
.input__prefix, .input__suffix {
  padding: 12px 13px;
  font: 500 12px/1.35 var(--mono);
  color: var(--mute);
  display: flex;
  align-items: center;
}
.input__prefix { padding-right: 0; font-size: 16px; }
.input__prefix + input { padding-left: 4px; }
.input__suffix { border-left: 1px solid var(--rule); }
.input-pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }

.result-grid {
  margin-top: 26px;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}
.result { background: var(--ink); color: var(--card); padding: 24px; }
.result .eyebrow { color: var(--mint); margin-bottom: 14px; display: block; }
.result__total {
  font: 600 56px/1 var(--mono);
  letter-spacing: -.03em;
  margin-bottom: 20px;
}
.result__lines {
  display: flex; flex-direction: column; gap: 9px;
  font: 400 12px/1 var(--mono);
  color: var(--on-ink);
}
.result__line { display: flex; justify-content: space-between; gap: 16px; }
.result__actions {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid rgba(247, 245, 240, .16);
  display: flex; gap: 10px; flex-wrap: wrap;
}

.share-card { background: #f0ede6; border: 1px solid rgba(0, 0, 0, .14); padding: 22px; }
.share-card__head {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 18px;
}
.share-card__brand { font: 800 15px/1 var(--sans); letter-spacing: -.02em; }
.share-card__total { font: 600 40px/1 var(--mono); letter-spacing: -.03em; }
.share-card__meta { font: 400 13px/1.4 var(--sans); color: var(--ink-3); margin-top: 8px; }
.share-bar { margin-top: 18px; height: 8px; display: flex; gap: 2px; }
.share-bar span:nth-child(1) { background: var(--green); }
.share-bar span:nth-child(2) { background: var(--ink); }
.share-bar span:nth-child(3) { background: var(--mute); }
.share-bar span:nth-child(4) { background: var(--line); }
.share-legend {
  margin-top: 9px;
  font: 500 9px/1 var(--mono);
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--mute);
}

.toast {
  position: fixed;
  left: 50%; bottom: 24px;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 13px 20px;
  border-radius: 10px;
  background: var(--ink);
  color: var(--card);
  font: 500 12px/1.3 var(--mono);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
  max-width: min(90vw, 420px);
}
.toast[hidden] { display: none; }
/* A copy/share confirmation used to look identical to any other status
   message — easy to miss, especially since the thing it's confirming (a
   clipboard write) has no visible effect of its own to notice instead. */
.toast--ok { border: 1px solid color-mix(in oklch, var(--mint) 55%, transparent); }
.toast__icon {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: var(--mint);
  color: #0c3320;
}
@media (prefers-reduced-motion: no-preference) {
  .toast--ok .toast__icon { animation: toast-pop .3s cubic-bezier(.34, 1.56, .64, 1) both; }
}
@keyframes toast-pop {
  0% { transform: scale(.4); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

/* ---------- crypto desk ---------- */

.desk-top { display: flex; gap: 16px; margin-bottom: 20px; }
.portfolio { flex: 1.3; padding: 22px; background: var(--ink); color: var(--card); }
.portfolio__head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.portfolio__value { display: flex; align-items: baseline; gap: 14px; margin: 16px 0 20px; flex-wrap: wrap; }
.portfolio__value strong { font: 600 40px/1 var(--mono); letter-spacing: -.03em; }
.portfolio__chart { height: 88px; position: relative; background: rgba(247, 245, 240, .05); }
.portfolio__chart > div { position: absolute; inset: 6px; background: var(--green); opacity: .85; transition: clip-path .3s ease; }
.chart-range { display: flex; gap: 6px; margin-top: 8px; }
.chart-range button {
  flex: 1; padding: 5px 0; border: 1px solid rgba(247, 245, 240, .15); border-radius: 6px;
  background: transparent; color: var(--on-ink-2); font: 500 11px/1 var(--mono);
  cursor: pointer; transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.chart-range button:hover { border-color: rgba(247, 245, 240, .35); color: var(--card); }
.chart-range button[aria-pressed="true"] { background: var(--card); color: var(--ink); border-color: var(--card); }
.ledger { flex: 1; }
.ledger__rows {
  display: flex; flex-direction: column; gap: 11px;
  font: 400 12px/1 var(--mono);
  color: var(--ink-2);
}
.ledger__row { display: flex; justify-content: space-between; gap: 12px; }
.ledger__row--total { padding-top: 11px; border-top: 1px solid rgba(0, 0, 0, .1); }
.leaders {
  display: flex; flex-direction: column; gap: 10px;
  font: 400 12px/1 var(--mono);
  color: var(--ink-2);
}
.leaders__row { display: flex; justify-content: space-between; gap: 12px; }
.leaders__row--you { padding-top: 10px; border-top: 1px solid rgba(0, 0, 0, .1); }

/* ---------- sticks ---------- */

.sticks {
  background: var(--card);
  border: 1px solid var(--rule);
  padding: 26px;
  margin-top: 20px;
  margin-bottom: 4px;
}
.sticks__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 22px;
}
.sticks__prompt { font: 700 22px/1.15 var(--sans); letter-spacing: -.02em; }
.sticks__prompt strong { color: var(--green); font-weight: 800; }
.sticks__mode { margin-top: 6px; font: 400 11px/1.4 var(--mono); color: var(--mute); }
.sticks__scores { display: flex; gap: 22px; text-align: right; }
.sticks__score { display: flex; flex-direction: column; gap: 3px; }
.sticks__score-num { display: inline-flex; align-items: center; justify-content: flex-end; gap: 5px; }
.sticks__score strong { font: 600 22px/1 var(--mono); }
.sticks__score span {
  font: 500 9px/1 var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--mute);
}

.sticks__board {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 220px;
  padding: 18px;
  background: var(--well);
  border: 1px solid var(--rule-2);
}
.sticks__empty {
  margin: auto;
  font: 400 13px/1.5 var(--mono);
  color: var(--ink-3);
  text-align: center;
}

/* One stick: a full-width hit area with a bar inside it, so the click target
   is the row rather than the drawn length — picking the shortest stick should
   not be the hardest thing to hit. */
.stick {
  display: block;
  width: 100%;
  padding: 3px 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.stick__bar {
  display: block;
  height: 16px;
  background: var(--ink-2);
  border: 1px solid var(--ink);
  transition: background .12s, transform .12s;
}
.stick:hover:not(:disabled) .stick__bar { background: var(--green); transform: translateX(2px); }
.stick:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.stick:disabled { cursor: default; }

/* Face down: every stick identical, full width, numbered. The player is
   choosing odds, not reading a board, so the covers must give away nothing —
   not length, not order, not a hint from the drawing. */
.stick--down .stick__bar {
  width: 100% !important;
  height: 26px;
  display: flex;
  align-items: center;
  padding-left: 10px;
  background: repeating-linear-gradient(135deg, #d8d4c8 0 6px, #e2dfd4 6px 12px);
  border-color: rgba(0, 0, 0, .28);
  font: 500 11px/1 var(--mono);
  color: rgba(0, 0, 0, .45);
}
.stick--down:hover .stick__bar { background: var(--mint-bg); color: var(--green-d); }

/* Turned over. The winning stick is marked whether or not it was picked, so
   the result can be checked by eye instead of taken on trust. */
.stick__bar { transition: width .35s ease, background .12s; }
.stick--answer .stick__bar { background: var(--green); border-color: var(--green-d); }
.stick--won .stick__bar { background: var(--green); border-color: var(--green-d); }
.stick--wrong .stick__bar { background: var(--red); border-color: var(--red); }
.stick--wrong .stick__bar::after,
.stick--answer .stick__bar::after { content: ""; }

.sticks__status {
  min-height: 20px;
  margin-top: 14px;
  font: 500 12px/1.4 var(--mono);
  color: var(--ink-3);
}
.sticks__status--good { color: var(--green); }
.sticks__status--bad { color: var(--red); }

.sticks__controls {
  display: flex;
  align-items: flex-end;
  gap: 20px;
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--rule-2);
}
.sticks__dial { flex: 1; }
.sticks__deal { white-space: nowrap; }

.sticks__over {
  margin-top: 18px;
  padding: 16px;
  background: var(--mint-bg);
  font: 400 12px/1.6 var(--mono);
  color: var(--ink-2);
}
.sticks__over strong { display: block; font: 700 14px/1.3 var(--sans); margin-bottom: 4px; }
.sticks__over a { display: inline-block; margin-top: 8px; color: var(--green); }

/* ---------- flappy ---------- */

.flappy {
  background: var(--card);
  border: 1px solid var(--rule);
  padding: 26px;
  margin-top: 20px;
  margin-bottom: 4px;
}
.flappy__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 22px;
}
.flappy__prompt { font: 700 22px/1.15 var(--sans); letter-spacing: -.02em; }
.flappy__mode { margin-top: 6px; font: 400 11px/1.4 var(--mono); color: var(--mute); }

.flappy__stage {
  position: relative;
  max-width: 640px;
  margin: 0 auto;
  line-height: 0;
}
.flappy__stage canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 640 / 480;
  background: #dff0e6;
  border: 1px solid var(--rule-2);
  border-radius: 6px;
  cursor: pointer;
  touch-action: manipulation;
}
/* Tower's canvas is portrait (a tower is tall, not wide) — same component,
   different shape, so only the aspect ratio and a width cap (an unbounded
   380x600 canvas stretched to a wide rail column would be a very odd shape)
   need overriding here. */
.tower .flappy__stage canvas { aspect-ratio: 380 / 600; max-width: 380px; margin: 0 auto; }
.tower .flappy__stage { max-width: 380px; margin: 0 auto; }
/* Lives in .flappy__head now, not floating on the canvas — see the comment
   in flappy.html on why. A plain bordered icon button matching the card
   background it actually sits on, rather than the translucent-dark style
   that made sense on top of the game art. */
.flappy__fullscreen {
  flex-shrink: 0;
  align-self: center;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  color: var(--ink-2);
  border: 1px solid var(--rule);
  border-radius: 50%;
  cursor: pointer;
}
.flappy__fullscreen:hover { background: var(--mint-bg); color: var(--ink); border-color: var(--line); }
/* Only rendered (via [hidden]) while the class-fallback fullscreen is on —
   see the comment on this button in flappy.html for why it has to live
   inside the stage rather than next to .flappy__fullscreen. */
.flappy__exit-fullscreen {
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: max(10px, env(safe-area-inset-top));
  right: max(10px, env(safe-area-inset-right));
  z-index: 5;
  width: 36px;
  height: 36px;
  background: rgba(0, 0, 0, .45);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, .35);
  border-radius: 50%;
  cursor: pointer;
}
.flappy__exit-fullscreen:hover { background: rgba(0, 0, 0, .65); border-color: rgba(255, 255, 255, .6); }
.flappy__stage:fullscreen,
.flappy__stage.flappy__stage--fullscreen {
  max-width: none;
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0f1412;
}
/* dvh/dvw as a progressive enhancement over vh/vw: on real mobile browsers
   (as opposed to headless/desktop testing), vh is the LARGEST possible
   viewport — the height before the address bar/toolbar hides itself — so
   sizing off it can overshoot the actually-visible area and read as
   stretched once the browser chrome is accounted for. dvh tracks the
   viewport that's actually there right now. Unsupported browsers simply
   ignore this rule and keep the vh/vw values above. */
@supports (height: 100dvh) {
  .flappy__stage:fullscreen,
  .flappy__stage.flappy__stage--fullscreen {
    width: 100dvw;
    height: 100dvh;
  }
}
/* The real :fullscreen pseudo-class gets browser top-layer treatment for
   free; the class-only fallback (browser Fullscreen API unavailable or its
   request silently rejected) needs position:fixed itself, or 100vw/100vh
   just becomes an oversized block in normal page flow instead of covering
   the viewport. */
.flappy__stage.flappy__stage--fullscreen {
  position: fixed;
  inset: 0;
  z-index: 1000;
  margin: 0;
}
/* width/height as explicit min() formulas rather than width:auto + the
   aspect-ratio property + flex auto-sizing: that combination stretched the
   canvas to fill the full viewport box (ignoring the 640:480 ratio
   entirely) in mobile fullscreen specifically, though it worked fine on
   desktop. This computes the letterboxed fit directly, so it doesn't
   depend on how any given mobile browser resolves aspect-ratio inside a
   fullscreen flex container. */
.flappy__stage:fullscreen canvas,
.flappy__stage--fullscreen canvas {
  width: min(100vw, 100vh * 640 / 480);
  height: min(100vh, 100vw * 480 / 640);
  max-width: none;
  border: 0;
  border-radius: 0;
}
@supports (height: 100dvh) {
  .flappy__stage:fullscreen canvas,
  .flappy__stage--fullscreen canvas {
    width: min(100dvw, 100dvh * 640 / 480);
    height: min(100dvh, 100dvw * 480 / 640);
  }
}
@media (prefers-reduced-motion: no-preference) {
  .flappy__stage canvas { animation: stage-glow 3.5s ease-in-out infinite; }
}
@keyframes stage-glow {
  0%, 100% { box-shadow: 0 0 0 rgba(31, 143, 95, 0); }
  50% { box-shadow: 0 0 22px rgba(31, 143, 95, .25); }
}
.flappy__score-bump { animation: score-bump .3s ease; }
@keyframes score-bump {
  0% { transform: scale(1); }
  40% { transform: scale(1.25); color: var(--green); }
  100% { transform: scale(1); }
}
.flappy__overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 20px;
  text-align: center;
  background: rgba(22, 21, 15, .55);
  color: #fff;
}
.flappy__overlay strong { font: 700 20px/1.2 var(--sans); }
.flappy__overlay span { font: 400 13px/1.5 var(--mono); color: rgba(255, 255, 255, .8); }
.flappy__overlay .btn { margin-top: 12px; }
.flappy__overlay a:not(.btn) { font: 500 13px/1.4 var(--mono); color: var(--mint); margin-top: 12px; }
.flappy__overlay a:not(.btn):hover { color: #fff; }

.flappy__status {
  min-height: 20px;
  margin-top: 14px;
  font: 500 12px/1.4 var(--mono);
  color: var(--ink-3);
  text-align: center;
}

/* ---------- wordle ---------- */

.wordle {
  background: var(--card);
  border: 1px solid var(--rule);
  padding: 26px;
  margin-top: 20px;
  margin-bottom: 4px;
}
.wordle__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 22px;
}
.wordle__prompt { font: 700 22px/1.15 var(--sans); letter-spacing: -.02em; }
.wordle__mode { margin-top: 6px; font: 400 11px/1.4 var(--mono); color: var(--mute); }

.wordle__grid {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 320px;
  margin: 0 auto;
}
.wordle__row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
.wordle__cell {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--well);
  border: 2px solid var(--rule);
  border-radius: 8px;
  font: 700 22px/1 var(--sans);
  color: var(--ink);
  text-transform: uppercase;
  transition: transform .1s ease, border-color .1s ease, background-color .15s ease;
}
/* Only the open row is tappable — clicking it focuses .wordle__input, which
   is what raises the on-screen keyboard on a phone. */
.wordle__row--current .wordle__cell { cursor: pointer; }
/* Present but invisible: 1px so iOS still treats it as a real focus target
   (display:none/visibility:hidden ones get ignored), 16px font so focusing
   it doesn't trigger the page-zoom Safari applies to smaller inputs.
   position:fixed at the top of the viewport (rather than in normal flow,
   after the grid) keeps it always "in view" from the browser's point of
   view — so when the on-screen keyboard opens, there's nothing for it to
   auto-scroll to reveal, and the grid stays put instead of sliding up
   behind the keyboard. */
.wordle__input {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  font-size: 16px;
  pointer-events: none;
}
.wordle__cell--filled { border-color: var(--ink-3); transform: scale(1.04); }
.wordle__cell--hit, .wordle__cell--present, .wordle__cell--miss { transform: none; }
@media (prefers-reduced-motion: no-preference) {
  .wordle__cell--hit, .wordle__cell--present, .wordle__cell--miss {
    animation: wordle-flip .32s ease both;
  }
  .wordle__row .wordle__cell:nth-child(2) { animation-delay: .04s; }
  .wordle__row .wordle__cell:nth-child(3) { animation-delay: .08s; }
  .wordle__row .wordle__cell:nth-child(4) { animation-delay: .12s; }
  .wordle__row .wordle__cell:nth-child(5) { animation-delay: .16s; }
}
@keyframes wordle-flip {
  0% { transform: scale(1) rotateX(0); }
  45% { transform: scale(1.08) rotateX(90deg); }
  55% { transform: scale(1.08) rotateX(90deg); }
  100% { transform: scale(1) rotateX(0); }
}

/* The typing cursor: a blinking bar, the same device a text input uses,
   marking the next empty cell rather than leaving five identical boxes
   with no sense of where a keystroke is about to land. */
.wordle__cell--cursor { border-color: var(--ink-3); }
.wordle__cell--cursor::after {
  content: "";
  width: 2px;
  height: 55%;
  background: var(--ink);
  border-radius: 1px;
}
.wordle__cell--cursor::after { animation: cursor-blink 1.1s step-end infinite; }
@keyframes cursor-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}
.wordle__cell--hit { background: var(--green); border-color: var(--green-d); color: #fff; }
.wordle__cell--present { background: #d8a531; border-color: #b3861f; color: #fff; }
.wordle__cell--miss { background: var(--ink-2); border-color: var(--ink); color: var(--card); }

/* Inline color chips inside explanatory text — same colors as the grid
   itself, so "green" in a sentence is shown, not just described. */
.wordle__swatch {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 4px;
  font-weight: 600;
}
.wordle__swatch--hit { background: var(--green); border: 1px solid var(--green-d); color: #fff; }
.wordle__swatch--present { background: #d8a531; border: 1px solid #b3861f; color: #fff; }
.wordle__swatch--miss { background: var(--ink-2); border: 1px solid var(--ink); color: var(--card); }

.wordle__status {
  min-height: 20px;
  margin: 12px 0;
  font: 500 12px/1.4 var(--mono);
  color: var(--red);
  text-align: center;
}

.wordle__keyboard { display: flex; flex-direction: column; gap: 6px; max-width: 480px; margin: 0 auto; }
.wordle__krow { display: flex; justify-content: center; gap: 5px; }
.wordle__key {
  flex: 1;
  max-width: 40px;
  padding: 12px 0;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 6px;
  font: 600 12px/1 var(--sans);
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  transition: transform .1s ease, background-color .12s ease, box-shadow .12s ease;
}
.wordle__key--wide { flex: 1.6; max-width: 64px; font-size: 10px; }
.wordle__key:hover { background: var(--mint-bg); transform: translateY(-1px); box-shadow: 0 2px 0 rgba(0,0,0,.08); }
.wordle__key:active { transform: translateY(0); box-shadow: none; }
.wordle__key--hit { background: var(--green); border-color: var(--green-d); color: #fff; }
.wordle__key--present { background: #d8a531; border-color: #b3861f; color: #fff; }
.wordle__key--miss { background: var(--ink-2); border-color: var(--ink); color: var(--card); }

.wordle__over {
  margin-top: 18px;
  padding: 16px;
  background: var(--mint-bg);
  font: 400 12px/1.6 var(--mono);
  color: var(--ink-2);
}
.wordle__over strong { display: block; font: 700 14px/1.3 var(--sans); margin-bottom: 4px; }
.wordle__over a { display: inline-block; margin-top: 8px; color: var(--green); }
.wordle__over-actions { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; margin-top: 8px; }
.wordle__over-actions a { margin-top: 0; }
.wordle__over-actions .btn { padding: 7px 14px; font-size: 11px; }

/* ---------- sudoku ---------- */

.sudoku {
  background: var(--card);
  border: 1px solid var(--rule);
  padding: 26px;
  margin-top: 20px;
  margin-bottom: 4px;
}
.sudoku__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 22px;
}
.sudoku__prompt { font: 700 22px/1.15 var(--sans); letter-spacing: -.02em; }
.sudoku__mode { margin-top: 6px; font: 400 11px/1.4 var(--mono); color: var(--mute); }
.sudoku__difficulty {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  font: 500 11px/1 var(--mono);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: .06em;
}
/* Wraps just the <select> so the hand-drawn chevron below can be
   positioned against it without fighting the label's own flex layout
   (which also has to fit the "Difficulty" text before it). */
.sudoku__difficulty-select { position: relative; display: inline-flex; }
.sudoku__difficulty-select::after {
  content: "";
  position: absolute;
  right: 10px;
  top: 50%;
  width: 6px;
  height: 6px;
  margin-top: -5px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  pointer-events: none;
}
.sudoku__difficulty select {
  /* Replaces the browser's own dropdown chrome (native arrow, flat
     platform-styled box) with the chevron above so this reads as part of
     the page's own design instead of an unstyled OS control. */
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding: 6px 26px 6px 10px;
  font: 500 12px/1 var(--sans);
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 8px;
  cursor: pointer;
  transition: border-color .15s ease;
}
.sudoku__difficulty select:hover { border-color: var(--ink-3); }
.sudoku__difficulty select:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.sudoku__difficulty select:disabled { opacity: .5; cursor: default; }
.sudoku__pause {
  flex-shrink: 0;
  align-self: center;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  color: var(--ink-2);
  border: 1px solid var(--rule);
  border-radius: 50%;
  cursor: pointer;
}
.sudoku__pause:hover { background: var(--mint-bg); color: var(--ink); border-color: var(--line); }
.sudoku__paused .btn { min-width: 140px; justify-content: center; }

.sudoku__body {
  display: flex;
  gap: 22px;
  align-items: flex-start;
  justify-content: center;
  flex-wrap: wrap;
}

.sudoku__board { position: relative; width: min(100%, 420px); }
.sudoku__grid {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  width: 100%;
  aspect-ratio: 1;
  border: 2px solid var(--ink);
}
.sudoku__paused {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card);
  border: 2px solid var(--ink);
  border-radius: inherit;
}
.sudoku__cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--well);
  border: 1px solid rgba(0, 0, 0, .13);
  font: 600 17px/1 var(--sans);
  color: var(--ink-2);
  cursor: pointer;
  padding: 0;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.sudoku__cell--top { border-top: 2px solid var(--ink); }
.sudoku__cell--left { border-left: 2px solid var(--ink); }
.sudoku__cell--bottom { border-bottom: 2px solid var(--ink); }
.sudoku__cell--right { border-right: 2px solid var(--ink); }
.sudoku__cell--given { color: var(--ink); font-weight: 700; }
.sudoku__cell--given { cursor: default; }
.sudoku__cell--peer { background: var(--mint-bg); }
.sudoku__cell--match { color: var(--green); }
.sudoku__cell--selected { background: var(--mint); color: var(--ink); }
.sudoku__cell--selected.sudoku__cell--given { color: var(--ink); }
.sudoku__cell--bad { color: var(--red); background: #f6ded9; }
.sudoku__cell--bad.sudoku__cell--selected { background: #e9b6ac; }
/* Not a duplicate itself — an empty cell whose row/column/box peers already
   cover every digit, which only happens because a wrong number sits
   somewhere among them. A ring rather than a fill: it's a symptom, not the
   mistake, so it reads as different from --bad rather than a second red. */
.sudoku__cell--dead { box-shadow: inset 0 0 0 2px #c98a1f; }
.sudoku__cell:disabled { cursor: default; }
.sudoku__cell--placed { animation: cell-pop .22s ease; }
.sudoku__cell--pulse { animation: sudoku-pulse .4s ease; }
@keyframes sudoku-pulse {
  0%, 100% { box-shadow: none; }
  35% { box-shadow: inset 0 0 0 2px var(--red); }
}
.sudoku__grid--solved .sudoku__cell {
  animation: sudoku-solved .5s ease both;
  animation-delay: calc(var(--i, 0) * 6ms);
}
@keyframes sudoku-solved {
  0% { transform: scale(1); }
  40% { transform: scale(1.12); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .sudoku__cell--placed, .sudoku__cell--pulse, .sudoku__grid--solved .sudoku__cell { animation: none; }
}

.sudoku__notes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  width: 100%;
  height: 100%;
  padding: 2px;
}
.sudoku__notes span {
  display: flex;
  align-items: center;
  justify-content: center;
  font: 500 8px/1 var(--mono);
  color: var(--ink-3);
}

.sudoku__side {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: min(100%, 180px);
}
.sudoku__toolbar { display: flex; gap: 6px; }
.sudoku__tool {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 4px;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 6px;
  font: 500 9px/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-2);
  cursor: pointer;
}
.sudoku__tool:hover { background: var(--mint-bg); }
.sudoku__tool--active,
.sudoku__tool[aria-pressed="true"] { background: var(--mint); color: var(--ink); border-color: var(--ink); }

.sudoku__pad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
.sudoku__key {
  padding: 14px 0;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 6px;
  font: 700 16px/1 var(--sans);
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.sudoku__key:hover { background: var(--mint-bg); }
.sudoku__key--done { color: var(--mute); background: transparent; }
.sudoku__key:disabled, .sudoku__tool:disabled { opacity: .4; cursor: default; pointer-events: none; }

/* ---------- 2048 ----------

   A 4x4 CSS grid, the same aspect-ratio/modifier-class idiom
   .wordle__grid/.sudoku__grid use, but its own component: the tile palette
   (a distinct background per value) has no equivalent in either. */
.game2048__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(4, 1fr);
  gap: 8px;
  aspect-ratio: 1;
  max-width: 420px;
  margin: 0 auto;
  padding: 8px;
  background: var(--well);
  border-radius: 8px;
}
.game2048__cell {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  background: rgba(0, 0, 0, .04);
  font: 700 clamp(14px, 5vw, 26px)/1 var(--sans);
  color: var(--ink);
}
/* Tiles used to just be the same 16 cell divs re-stamped with a new value
   every move — no element ever moved, so a slide read as an instant
   teleport. This overlay sits on top of the (now permanently empty)
   background cells above, at the same grid geometry, and holds one
   persistent element per live tile; every tile after the first render
   sits in grid-column/row 1 (CSS grid allows stacked/overlapping items)
   and is pushed to its real cell purely with `transform`, so moving a
   tile is just changing that transform — the transition below animates
   it for free, no manual FLIP bookkeeping needed. */
.game2048__tiles {
  position: absolute;
  inset: 8px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(4, 1fr);
  gap: 8px;
  pointer-events: none;
}
.game2048__tile {
  grid-column: 1;
  grid-row: 1;
  aspect-ratio: 1;
  /* --r/--c (set from JS as plain integers 0-3) place the tile; the "+ 8px"
     has to match .game2048__tiles' own gap above, since translate's
     percentage is relative to the tile's own size (one grid cell), not
     the gap between cells. */
  transform: translate(calc((100% + 8px) * var(--c, 0)), calc((100% + 8px) * var(--r, 0)));
  transition: transform .13s cubic-bezier(.2, .8, .3, 1);
}
.game2048__tile--eaten { transition: transform .13s cubic-bezier(.2, .8, .3, 1), opacity .1s ease .1s; opacity: 0; }
/* Position (outer, transform: translate, set inline per-move) and pop/merge
   scale (inner face, animated) have to be two different elements — an
   animation's keyframes replace an element's whole `transform`, so a
   `scale()` keyframe on the positioned element itself would wipe out its
   translate for the animation's duration and the tile would jump to the
   grid's top-left corner and back. */
.game2048__tile__face {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  font: 700 clamp(14px, 5vw, 26px)/1 var(--sans);
  color: var(--ink);
}
.game2048__tile[data-v] .game2048__tile__face { background: var(--tile-bg, var(--mute)); color: var(--tile-ink, #fff); }
/* A rainbow ramp instead of the classic beige/orange 2048 palette — colors
   are fixed (not theme roles), so tiles read the same, and stay legible,
   in both light and dark mode; each tier pins its own --tile-ink rather
   than relying on a cascaded default, so contrast is never left to chance. */
.game2048__tile[data-v="2"]    { --tile-bg: hsl(45 85% 78%);  --tile-ink: var(--ink); }
.game2048__tile[data-v="4"]    { --tile-bg: hsl(28 85% 70%);  --tile-ink: var(--ink); }
.game2048__tile[data-v="8"]    { --tile-bg: hsl(11 80% 62%);  --tile-ink: #fff; }
.game2048__tile[data-v="16"]   { --tile-bg: hsl(354 75% 58%); --tile-ink: #fff; }
.game2048__tile[data-v="32"]   { --tile-bg: hsl(337 70% 54%); --tile-ink: #fff; }
.game2048__tile[data-v="64"]   { --tile-bg: hsl(310 65% 50%); --tile-ink: #fff; }
.game2048__tile[data-v="128"]  { --tile-bg: hsl(283 62% 48%); --tile-ink: #fff; }
.game2048__tile[data-v="128"] .game2048__tile__face  { font-size: clamp(13px, 4.5vw, 23px); }
.game2048__tile[data-v="256"]  { --tile-bg: hsl(256 60% 46%); --tile-ink: #fff; }
.game2048__tile[data-v="256"] .game2048__tile__face  { font-size: clamp(13px, 4.5vw, 23px); }
.game2048__tile[data-v="512"]  { --tile-bg: hsl(229 58% 44%); --tile-ink: #fff; }
.game2048__tile[data-v="512"] .game2048__tile__face  { font-size: clamp(13px, 4.5vw, 23px); }
.game2048__tile[data-v="1024"] { --tile-bg: hsl(202 60% 42%); --tile-ink: #fff; }
.game2048__tile[data-v="1024"] .game2048__tile__face { font-size: clamp(11px, 4vw, 20px); }
.game2048__tile[data-v="2048"] { --tile-bg: hsl(160 55% 40%); --tile-ink: #fff; }
.game2048__tile[data-v="2048"] .game2048__tile__face { font-size: clamp(11px, 4vw, 20px); }
.game2048__tile[data-v-big]    { --tile-bg: hsl(250 40% 24%); --tile-ink: #fff; }
.game2048__tile[data-v-big] .game2048__tile__face    { font-size: clamp(10px, 3.5vw, 18px); }
.game2048__tile--new .game2048__tile__face { animation: cell-pop .18s ease; }
.game2048__tile--merged .game2048__tile__face { animation: game2048-merge .16s ease; }
@keyframes game2048-merge {
  0% { transform: scale(1); }
  50% { transform: scale(1.22); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .game2048__tile, .game2048__tile--eaten { transition: none; }
  .game2048__tile--new .game2048__tile__face, .game2048__tile--merged .game2048__tile__face { animation: none; }
}
/* Board takes the swipe directly on mobile, same touch-action guard the
   sudoku pad and sticks board use so a drag scrolls tiles, not the page. */
.game2048__grid { touch-action: none; }

/* ---------- game badge (shared: stats hub, achievements, toast) ---------- */

.game-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  font: 700 10px/1 var(--mono);
  letter-spacing: -.02em;
  color: #fff;
}

/* ---------- achievements page ---------- */

.achievements-progress { margin-bottom: 20px; }
.achievements-progress__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 8px;
}
.achievements-progress__count { font: 700 20px/1 var(--mono); color: var(--ink); }
.achievements-progress__bar {
  height: 8px;
  border-radius: 99px;
  background: var(--well);
  overflow: hidden;
}
.achievements-progress__fill {
  height: 100%;
  border-radius: inherit;
  background: var(--green);
  transition: width .3s ease;
}

/* Block, not grid: each game section is its own flex-wrap row sized to
   its own card count, so a game with one achievement renders one
   content-width card instead of stretching across (or leaving empty) the
   page-wide grid's other columns — the old grid forced a fresh row per
   heading, so sparse sections just left dead space beside a lone card. */
.achievements-grid__heading {
  margin: 22px 0 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  font: 700 12px/1 var(--mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.achievements-grid__heading::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 3px;
  background: var(--heading-accent, var(--mute));
}
.achievements-grid__heading:first-child { margin-top: 0; }
.achievements-grid__row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
/* Locked cards used to desaturate the whole card with filter:grayscale,
   which also grayed out the colored game badge — the one thing giving the
   grid any color at all, on a page that's mostly locked cards for most
   readers. The badge now stays at full color always; only the text dims,
   so the grid reads as colorful even before anything is earned. */
.achievement-card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex: 1 1 260px;
  min-width: 0;
  max-width: 320px;
  padding: 14px 16px;
  background: var(--well);
  border: 1px solid var(--rule);
  border-left: 4px solid color-mix(in oklch, var(--card-accent, var(--mute)) 55%, transparent);
  border-radius: 10px;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.achievement-card:hover { transform: translateY(-2px); }
.achievement-card__body { min-width: 0; }
.achievement-card__label,
.achievement-card__desc { opacity: .4; transition: opacity .15s ease; }
.achievement-card--earned {
  background: color-mix(in oklch, var(--card-accent) 12%, var(--card));
  border-color: color-mix(in oklch, var(--card-accent) 45%, var(--rule-2));
  border-left-color: var(--card-accent);
  box-shadow: 0 3px 0 color-mix(in oklch, var(--card-accent) 55%, transparent);
}
.achievement-card--earned .achievement-card__label,
.achievement-card--earned .achievement-card__desc { opacity: 1; }
.achievement-card__icon {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  font-size: 11px;
  box-shadow: 0 2px 0 rgba(0, 0, 0, .12);
}
.achievement-card__label { display: block; font: 700 14px/1.25 var(--sans); color: var(--ink); margin-bottom: 4px; }
.achievement-card__desc { display: block; font: 400 12px/1.4 var(--mono); color: var(--ink-3); }
.achievement-card__check {
  position: absolute;
  top: -7px;
  right: -7px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--card-accent);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 11px/1 var(--mono);
  border: 2px solid var(--card);
}
@media (prefers-reduced-motion: reduce) {
  .achievement-card { transition: none; }
  .achievement-card:hover { transform: none; }
}

/* ---------- achievement toast ----------
   Stacked bottom-right, above everything except the consent card (which
   must always win — see the note on .consent's z-index). Queued rather
   than shown all at once: a run that crosses three thresholds in one
   action pops them one at a time, not as a wall of cards. */
.achievement-toast-stack {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 65;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 300px;
  max-width: calc(100vw - 40px);
}
.achievement-toast {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  background: var(--ink);
  color: var(--card);
  border-left: 3px solid var(--flame);
  box-shadow: 0 6px 0 rgba(0, 0, 0, .14);
  cursor: pointer;
}
.achievement-toast__icon { flex: none; }
.achievement-toast__body { min-width: 0; }
.achievement-toast__eyebrow { display: block; font: 500 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--flame); margin-bottom: 4px; }
.achievement-toast__label { display: block; font: 700 14px/1.2 var(--sans); margin-bottom: 3px; }
.achievement-toast__desc { display: block; font: 400 11px/1.4 var(--mono); color: var(--on-ink-2); }
@media (prefers-reduced-motion: no-preference) {
  .achievement-toast { animation: rise-in .22s ease both; }
}

/* ---------- reaction time ---------- */

.reaction__stage { display: flex; justify-content: center; }
.reaction__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 12px;
  width: 100%;
  /* Bounded by both axes, not just width — a 3x3 grid this size on a short
     viewport (a phone in landscape, a small laptop window) used to push the
     page into a vertical scroll the game never needed. */
  max-width: min(380px, 90vw, 60vh);
  margin: 0 auto;
  /* Rapid repeated taps in a tight grid read to a touchscreen as small pan
     gestures whenever a touch isn't dead-center on a cell — each one
     nudges the page and lets it rubber-band back, which is the "wobble"
     during play. touch-action:manipulation on the cells themselves only
     drops double-tap-zoom, not panning; this blocks panning starting
     anywhere on the grid (including the gaps between cells) so every
     touch here becomes a tap, never a scroll. */
  touch-action: none;
}
.reaction__start {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: min(380px, 90vw, 60vh);
  aspect-ratio: 1;
  margin: 0 auto;
}
.reaction__cell {
  aspect-ratio: 1;
  border-radius: 28%;
  background: var(--well);
  border: 2px solid var(--rule);
  cursor: pointer;
  /* none, not manipulation — a cell's own touch-action wins over the
     grid's for touches starting on it, and manipulation still permits
     panning (it only drops double-tap-zoom), which is exactly what was
     wobbling the page on most real taps. See .reaction__grid's note. */
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  transition: background-color .08s ease, border-color .08s ease, transform .08s ease, box-shadow .08s ease;
}
.reaction__cell:active { transform: scale(.94); }
.reaction__cell--lit {
  background: var(--flame);
  border-color: var(--flame);
  box-shadow: 0 0 0 8px rgba(217, 114, 44, .22);
  transform: scale(1.04);
}
@media (prefers-reduced-motion: reduce) {
  .reaction__cell { transition: none; }
}

/* ---------- streak flame ----------
   Shown wherever a streak count is displayed. Orange and flickering while
   the streak is live (> 0), grey and still once it's out — so the icon
   itself tells you the state at a glance instead of just vanishing, which
   read as a layout gap rather than "no streak". The modifier lives on the
   flame's own .sticks__score-num container rather than the flame alone, so
   the streak *number* next to it (a bare <strong>, no color of its own —
   it inherits) reads the same color as the flame instead of staying
   neutral ink while only the icon flips. Kept to transform/opacity so it
   never nudges layout or trips the ad-clearance geometry check. */
@keyframes flame-flicker {
  0%, 100% { transform: scale(1) rotate(-2deg); opacity: 1; }
  25%      { transform: scale(1.08) rotate(2deg); opacity: .92; }
  50%      { transform: scale(.94) rotate(-3deg); opacity: 1; }
  75%      { transform: scale(1.05) rotate(1deg); opacity: .96; }
}
.sticks__score-num--streak-on { color: var(--flame); }
.sticks__score-num--streak-off { color: var(--line); }
.flame {
  display: inline-block;
  color: inherit;
  animation: flame-flicker 1.6s ease-in-out infinite;
  transform-origin: 50% 85%;
}
.flame svg { display: block; color: inherit; }
.sticks__score-num--streak-off .flame { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .flame { animation: none; }
}

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

.stats-hub__hero { margin-bottom: 20px; }
.stats-hub__hero-row { display: flex; align-items: baseline; gap: 32px; flex-wrap: wrap; }
.stats-hub__hero-num { display: flex; align-items: baseline; gap: 10px; }
/* No color here: the streak number inherits from
   .sticks__score-num--streak-on/off on its wrapper, the same as every other
   streak readout on the site — hard-coding one here would flatten the
   orange-when-live/gray-when-not distinction the flame is there to show. */
.stats-hub__hero-num strong { font: 700 40px/1 var(--mono); letter-spacing: -.03em; }
/* A dedicated class, not a bare `span` selector — the flame's own wrapper
   (.sticks__score-num) is also a <span> and a direct sibling-level child
   here, and a tag selector would have caught it too, silently overwriting
   the orange/gray it's supposed to carry. */
.stats-hub__hero-caption { font: 400 12px/1.4 var(--mono); color: var(--on-ink-2); white-space: nowrap; }
.stats-hub__hero-num .flame svg { width: 20px; height: 20px; }
.stats-hub__hero-num--sub strong { font-size: 26px; color: var(--on-ink); }
.stats-hub__hero-note {
  margin: 16px 0 0;
  padding-top: 14px;
  border-top: 1px solid rgba(255, 255, 255, .12);
  font: 400 12px/1.5 var(--mono);
  color: var(--on-ink-2);
  max-width: 56ch;
}
.trow--stats-hub { grid-template-columns: 26px 1.1fr .9fr .8fr 1fr 1.1fr 1.4fr; align-items: center; }

/* ---------- the footer wall, and the page made of them ---------- */

.ad-well--wall { height: 250px; margin-inline: var(--gutter); }
.ad-well--wall.ad-well--variable { height: auto; min-height: 250px; padding-block: 8px; }
.ad-wall__more {
  display: block;
  margin: -26px var(--gutter) 0;
  font: 500 11px/1 var(--mono);
  color: var(--mute);
  text-decoration: none;
}
.ad-wall__more:hover { color: var(--green); text-decoration: underline; }

.wall-intro { padding: 40px var(--gutter) 8px; max-width: 720px; }
.wall-intro p { font: 400 14px/1.6 var(--sans); color: var(--ink-2); }
.wall-intro__note { font: 400 12px/1.6 var(--mono) !important; color: var(--ink-3) !important; }
.wall-counter {
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  margin: 20px 0 0;
  padding: 14px 16px;
  background: var(--ink);
  color: var(--on-ink);
  font: 400 11px/1.4 var(--mono);
  position: sticky;
  top: 0;
  z-index: 5;
}
.wall-counter strong { color: var(--mint); font-weight: 600; }

.wall {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 var(--gutter);
}
.wall__unit { width: 100%; margin-block: 20px; }
/* Back to top. Fixed, appears once there is something to go back up from. */
.to-top {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 45;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 15px;
  background: var(--ink);
  color: var(--card);
  border: 0;
  font: 500 11px/1 var(--mono);
  letter-spacing: .04em;
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .18s, transform .18s;
  pointer-events: none;
}
.to-top--on { opacity: 1; transform: none; pointer-events: auto; }
.to-top:hover { background: var(--green-d); }
.to-top:focus-visible { outline: 2px solid var(--mint); outline-offset: 2px; }

.wall-end { padding: 30px var(--gutter) 60px; text-align: center; }
.wall-end p { font: 400 12px/1.6 var(--mono); color: var(--ink-3); }
.wall-end a { font: 500 12px/1 var(--mono); color: var(--green); }

/* ---------- buy links ---------- */

.buy {
  font: 500 11px/1 var(--mono);
  color: var(--green);
  text-decoration: none;
  white-space: nowrap;
}
.buy:hover { text-decoration: underline; }
.buy__where {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  margin-top: 10px;
}
.buy-note { margin: 12px 0 0; font: 400 11px/1.5 var(--mono); color: var(--mute); }

/* ---------- long-form ---------- */

.prose { max-width: 68ch; }
.prose h2 {
  margin: 34px 0 10px;
  font: 600 19px/1.25 var(--sans);
  letter-spacing: -.015em;
}
.prose p, .prose li { font: 400 14px/1.7 var(--sans); color: var(--ink-2); }
.prose ul { padding-left: 18px; }
.prose li { margin-bottom: 10px; }
.prose strong { color: var(--ink); font-weight: 600; }
.prose__standfirst {
  font: 400 16px/1.6 var(--sans) !important;
  color: var(--ink-3) !important;
  padding-bottom: 6px;
}
.prose__foot {
  margin-top: 30px;
  padding-top: 16px;
  border-top: 1px solid var(--rule-2);
  font: 400 12px/1.6 var(--mono) !important;
  color: var(--ink-3) !important;
}

/* ---------- rechtliche seiten ---------- */

.legal { max-width: 78ch; padding: 40px var(--gutter) 60px; }
.legal h2 {
  margin: 32px 0 10px;
  font: 600 16px/1.25 var(--sans);
  letter-spacing: -.01em;
}
.legal p, .legal li { font: 400 13px/1.7 var(--sans); color: var(--ink-2); }
.legal ul { padding-left: 18px; }
.legal li { margin-bottom: 8px; }
.legal code {
  padding: 1px 5px;
  background: var(--well);
  border: 1px solid var(--rule-2);
  font: 400 12px/1 var(--mono);
}
/* Unübersehbar, und absichtlich hässlich: eine Rechtsseite mit Platzhaltern
   darf nicht so aussehen, als wäre sie fertig. */
.legal__todo {
  margin-bottom: 30px;
  padding: 16px 18px;
  background: #fdf3d7;
  border: 2px solid #b8860b;
  font: 400 12px/1.65 var(--mono);
  color: #4a3a08;
}
.legal__todo strong { display: block; margin-bottom: 6px; font: 700 13px/1.3 var(--sans); }

/* ---------- consent ---------- */

/* A card in the bottom-left corner rather than a full-bleed bar.

   The bar was the giveaway: edge-to-edge dark strip, text left, two equal
   buttons pushed to the far right by `space-between`. That silhouette is on
   every third site and reads as something bolted on rather than part of the
   page. A narrow card sitting in the corner reads as the site talking, keeps
   the whole layout visible behind it, and — the part that matters commercially
   — a 420px column forces the copy to stay short. */
.consent {
  position: fixed;
  inset: auto auto var(--gutter) var(--gutter);
  /* Above every other overlay, including the sign-in gate, the intro tour,
     and the open mobile nav dropdown: consent must stay reachable no
     matter what else is on screen, or "Accept"/"Reject" can be visually
     blocked with no way to answer. */
  z-index: 95;
  width: 420px;
  max-width: calc(100vw - var(--gutter) * 2);
  padding: 20px 22px 18px;
  background: var(--ink);
  color: var(--card);
  /* One heavy edge instead of a border on all four sides: the same device the
     legal note and the settle banner use, so it belongs to this site. */
  border-left: 3px solid var(--green);
  box-shadow: 0 6px 0 rgba(0, 0, 0, .14);
}
.consent__text strong { display: block; font: 700 15px/1.25 var(--sans); margin-bottom: 7px; }
.consent__text p { margin: 0 0 10px; font: 400 12px/1.55 var(--mono); color: var(--on-ink); }
.consent__text a { font: 400 11px/1 var(--mono); color: var(--mint); }
/* The buttons share the row below the text and split it evenly, which is the
   equal-prominence floor expressed as layout rather than as a magic width. */
.consent__actions { display: flex; gap: 8px; margin-top: 14px; }

/* Only present when the dialog was reopened from the footer, where it means
   "leave my answer alone". Deliberately quiet: it is not a third option on a
   first visit, because on a first visit it does not exist. */
.consent__keep {
  display: block;
  margin: 12px 0 0;
  padding: 0;
  background: none;
  border: 0;
  font: 400 11px/1 var(--mono);
  color: var(--on-ink-2);
  text-decoration: underline;
  cursor: pointer;
}
.consent__keep:hover { color: var(--card); }

/* Both buttons keep the same box: same min-width, same padding, same font,
   same row, one click each. Refusing must not be the *harder* option, or the
   consent is not freely given.

   Accept additionally gets the site's primary green fill, Reject the ordinary
   outline. That is emphasis, and emphasis is what is left once obstruction is
   off the table. Do not "improve" this further by shrinking Ablehnen, dimming
   its label below the 4.5:1 contrast it has here, moving it to a second
   screen, or replacing it with a link — those are the specific patterns EU
   authorities have fined, and they would also make the stored consent
   worthless as a legal basis, which is the whole point of collecting it. */
.consent__btn {
  /* Equal share of the row, whatever the labels say. "Werbung erlauben" is the
     longer word; sizing to content would quietly make Ablehnen the smaller
     target, which is the floor this must not fall through. */
  flex: 1 1 0;
  min-width: 0;
  padding: 12px 14px;
  background: transparent;
  border: 1px solid rgba(247, 245, 240, .45);
  color: var(--card);
}
/* Generic hover first: the primary rule below shares its specificity and must
   win for the element that carries both classes. */
.consent__btn:hover { background: rgba(247, 245, 240, .12); }
.consent__btn--primary {
  background: var(--green);
  border-color: var(--green);
  color: #fff;
}
.consent__btn--primary:hover { background: var(--green-d); border-color: var(--green-d); }
.consent__btn:focus-visible { outline: 2px solid var(--mint); outline-offset: 2px; }

/* ---------- first-visit welcome ---------- */

.intro-tour-backdrop {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: rgba(0, 0, 0, .45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.intro-tour {
  position: relative;
  width: 460px;
  max-width: 100%;
  padding: 26px 26px 22px;
  background: var(--ink);
  color: var(--card);
  border-radius: 4px;
}
.intro-tour__eyebrow { color: var(--mint); }
.intro-tour__title {
  margin: 8px 0 18px;
  font: 700 22px/1.15 var(--sans);
  letter-spacing: -.01em;
  color: var(--card);
}
.intro-tour__list { list-style: none; margin: 0 0 20px; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.intro-tour__step { display: flex; gap: 12px; align-items: flex-start; }
.intro-tour__num {
  flex: 0 0 auto;
  width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(247, 245, 240, .12);
  font: 600 11px/1 var(--mono);
  color: var(--mint);
}
.intro-tour__copy { font: 400 13px/1.55 var(--sans); color: var(--on-ink); }
.intro-tour__copy strong { color: var(--card); font-weight: 600; }
.intro-tour__actions { display: flex; }
.intro-tour__actions .btn { flex: 1 1 0; justify-content: center; }
.intro-tour__close {
  position: absolute;
  top: 14px; right: 14px;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  background: none;
  border: 0;
  border-radius: 50%;
  font: 400 18px/1 var(--sans);
  color: var(--on-ink-2);
  cursor: pointer;
}
.intro-tour__close:hover { color: var(--card); background: rgba(247, 245, 240, .12); }

/* ---------- game gate ---------- */

.game-gate-backdrop {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: rgba(0, 0, 0, .55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.game-gate {
  width: 420px;
  max-width: 100%;
  padding: 28px 26px 24px;
  background: var(--ink);
  color: var(--card);
  border-radius: 4px;
  text-align: center;
}
.game-gate__eyebrow { color: var(--mint); }
.game-gate__title { margin: 10px 0 10px; font: 700 24px/1.15 var(--sans); color: var(--card); }
.game-gate__body { margin: 0 0 22px; font: 400 13px/1.55 var(--sans); color: var(--on-ink); }
.game-gate__actions { display: flex; flex-direction: column; gap: 10px; }
.game-gate__actions .btn { justify-content: center; }
.game-gate__actions .btn--ghost { border-color: rgba(247, 245, 240, .3); color: var(--card); }
.game-gate__actions .btn--ghost:hover { background: rgba(247, 245, 240, .12); }

.footer-help {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-weight: 500;
  color: var(--ink-2);
  text-decoration: underline;
  text-decoration-color: var(--rule-2);
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color .15s ease, text-decoration-color .15s ease;
}
.footer-help:hover { color: var(--green); text-decoration-color: var(--green); }

/* A slot waiting on consent looks the same as one waiting to be scrolled to:
   it holds its space, so answering the banner never reflows the page. */
.ad-well[data-pending] { opacity: .65; }

/* ---------- groups ---------- */

.board-scope {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 16px;
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--rule-2);
}
.board-scope__code { font: 400 11px/1 var(--mono); color: var(--ink-3); }
.group-code {
  padding: 3px 7px;
  background: var(--ink);
  color: var(--mint);
  font: 600 12px/1 var(--mono);
  letter-spacing: .1em;
}

.group-forms { display: flex; flex-direction: column; gap: 14px; margin-top: 16px; }
.group-form { display: flex; flex-direction: column; gap: 7px; }
.group-form__line { display: flex; gap: 8px; }
.group-form__line .input { flex: 1; }
.group-form__line .btn { white-space: nowrap; min-width: 84px; }

/* ---------- leaderboard ---------- */

/* The comparator's .trow grid is a seven-column layout for switch specs; the
   standings need their own column counts, so they override just the tracks. */
.trow--lb-sticks { grid-template-columns: 40px 1.6fr .6fr .7fr .8fr .7fr; }
.trow--lb-crypto { grid-template-columns: 40px 1.8fr .9fr 1fr .7fr; }
.trow--lb-flappy { grid-template-columns: 40px 1.8fr .8fr .7fr .7fr; }
.trow--lb-wordle { grid-template-columns: 40px 1.6fr .6fr .6fr .8fr .7fr; }
.trow--lb-game2048 { grid-template-columns: 40px 1.6fr .6fr .7fr .6fr .6fr; }
.trow--lb-reaction { grid-template-columns: 40px 1.8fr .8fr .7fr .7fr; }

/* ---------- account ---------- */

.auth {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 420px;
  gap: 40px;
  padding: 44px var(--gutter) 60px;
  align-items: start;
}
.auth__intro p { font: 400 14px/1.6 var(--sans); color: var(--ink-2); max-width: 52ch; }
.auth__list {
  margin: 20px 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 56ch;
}
.auth__list li {
  padding-left: 16px;
  border-left: 2px solid var(--mint);
  font: 400 13px/1.55 var(--sans);
  color: var(--ink-2);
}
.auth__list strong { color: var(--ink); font-weight: 600; }
.auth__note { font: 400 12px/1.6 var(--mono); color: var(--ink-3); max-width: 60ch; }

.auth__panel { padding: 0; }
.auth__tabs { display: flex; border-bottom: 1px solid var(--rule); }
.auth__tab {
  flex: 1;
  padding: 15px 12px;
  font: 600 12px/1 var(--sans);
  letter-spacing: .01em;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--mute);
  cursor: pointer;
}
.auth__tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--green); }
.auth__tab:hover:not([aria-selected="true"]) { color: var(--ink-2); border-bottom-color: var(--mute); }

.auth__form { display: flex; flex-direction: column; gap: 18px; padding: 24px; }
.auth__form .field { display: block; }
/* Both forms' submit buttons ("Log in" vs "Create account", never on
   screen together) and every .input in either form get an explicit
   height rather than one sized by its own text/content — the login form
   is two fields short, the register form is five long, and without a
   fixed height here the same-looking control read as a different size
   depending on which tab brought you to it. */
.auth__submit { width: 100%; height: 46px; padding: 0 18px; font-size: 13px; box-sizing: border-box; }
.auth__form .input { min-height: 44px; box-sizing: border-box; }
.auth__form .input input { height: 100%; box-sizing: border-box; }

.field__hint { margin: 7px 0 0; font: 400 11px/1.5 var(--mono); color: var(--mute); }
.field__error {
  margin: 7px 0 0;
  font: 500 11px/1.5 var(--mono);
  color: var(--red);
}
.input:focus-within { border-color: var(--green); }

/* Live compliance feedback on username/password as the reader types
   (auth-ui.js) — the hint text itself flips to state the rule as met or
   not, rather than a separate checkmark element, so there's exactly one
   thing to read instead of a hint plus a status icon saying the same
   thing twice. */
.field__hint--ok { color: var(--green); font-weight: 600; }
.field__hint--bad { color: var(--red); }
.input--valid { border-color: var(--green); }
.input--invalid { border-color: var(--red); }

.auth__offline {
  margin: 0;
  padding: 14px 24px;
  background: var(--mint-bg);
  border-bottom: 1px solid var(--rule-2);
  font: 400 12px/1.6 var(--mono);
  color: var(--ink-2);
}
.auth__offline code { font-weight: 600; }

.auth__done { padding: 26px 24px; }
.auth__done-title { font: 700 20px/1.2 var(--sans); letter-spacing: -.015em; }
.auth__done p { font: 400 13px/1.6 var(--sans); color: var(--ink-2); }
.auth__done-links { display: flex; flex-direction: column; gap: 10px; margin-top: 18px; }
.auth__done-links a {
  font: 500 12px/1 var(--mono);
  color: var(--green);
  text-decoration: none;
}
.auth__done-links a:hover { text-decoration: underline; }

/* ---------- saved work (quotes, favourites) ---------- */

.saved { display: flex; flex-direction: column; gap: 10px; }
.saved__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--rule-2);
  font: 400 12px/1.4 var(--mono);
  color: var(--ink-2);
}
.saved__row:last-child { border-bottom: 0; padding-bottom: 0; }
.saved__name {
  background: none;
  border: 0;
  padding: 0;
  font: 500 12px/1.4 var(--mono);
  color: var(--green);
  cursor: pointer;
  text-align: left;
}
.saved__name:hover { text-decoration: underline; }
.saved__drop {
  background: none;
  border: 0;
  padding: 0 0 0 10px;
  font: 400 14px/1 var(--mono);
  color: var(--mute);
  cursor: pointer;
}
.saved__empty { font: 400 12px/1.6 var(--mono); color: var(--ink-3); margin: 0; }

/* Each group as its own card rather than the plain divider-list look
   .saved__row otherwise has — a name, a member count, and a leave button
   packed onto one thin underlined line read as one run-on string. */
.saved--groups { gap: 8px; }
.saved--groups .saved__row {
  align-items: center;
  padding: 10px 12px;
  border-bottom: 0;
  background: var(--well);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.saved--groups .saved__drop { padding-left: 14px; font-size: 16px; }

/* The favourite toggle sits inside a comparator row. It is a star, not a
   .btn, so it does not enter the ad-clearance control set for a control the
   reader cannot mis-hit. */
.fav {
  background: none;
  border: 0;
  padding: 0 2px;
  font: 400 14px/1 var(--mono);
  color: var(--line);
  cursor: pointer;
}
.fav[aria-pressed="true"] { color: var(--green); }

/* ---------- responsive (1f) ---------- */

@media (max-width: 1100px) {
  :root { --rail: 0px; }
  .layout, .layout--filters { grid-template-columns: minmax(0, 1fr); gap: 22px; }
  .rail { display: none; }
  .only-mobile { display: block !important; }
  .hide-mobile { display: none !important; }
  .page { border-inline: 0; }
  .auth { grid-template-columns: minmax(0, 1fr); gap: 26px; }
}

@media (max-width: 760px) {
  :root { --gutter: 16px; --ad-clearance: 40px; }
  .site-footer__sitemap { grid-template-columns: 1fr 1fr; }
  .site-footer__brand { grid-column: 1 / -1; }
  /* position:fixed instead of sticky here on purpose: iOS Safari's
     collapsing address bar resizes the viewport as you scroll, and a
     `position: sticky` element re-anchors against that resize — on real
     devices this reads as the header floating, jumping, or vanishing for
     a frame while the toolbar animates. Fixed positioning pins it to the
     viewport itself, which the toolbar collapse doesn't disturb. The page
     needs the matching padding-top below so content doesn't start out
     underneath it. */
  .site-header {
    position: fixed;
    top: 0; left: 0; right: 0;
    height: 50px;
    padding: env(safe-area-inset-top) 16px 0;
  }
  .page { padding-top: calc(50px + env(safe-area-inset-top)); }
  .site-header__section { display: none; }
  .site-nav { gap: 14px; font-size: 11px; }
  .nav-burger { display: flex; }
  /* Sign out / login pill and the avatar only make sense as controls, and
     the compact bar is already tight with brand + stat + burger — they move
     into the dropdown with the links instead of crowding the 50px bar. */
  .site-nav:not(.site-nav--open) a:not(.site-nav__stat),
  .site-nav:not(.site-nav--open) .account-menu { display: none; }

  /* Compact bar on mobile is brand + nav (stat/login/avatar) + burger —
     the theme toggle is dropped as a 4th icon (it was making the header
     feel crowded) and reappears repositioned over the open dropdown
     instead, further down. Same button, same click handler in
     theme-toggle.js; nothing here duplicates its logic. */
  .theme-toggle { display: none; }

  /* Opened by nav-burger.js toggling this class. The nav itself becomes the
     dropdown panel — stat/login/avatar relocate into it along with the
     links rather than staying in the compact bar, which is what lets this
     be pure CSS on top of markup nav.js already builds, no duplication. */
  .site-nav.site-nav--open {
    position: fixed;
    left: 0; right: 0; top: calc(50px + env(safe-area-inset-top));
    z-index: 80;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 0;
    overflow-x: visible;
    overflow-y: auto;
    max-height: calc(100vh - 50px - env(safe-area-inset-top));
    /* Top padding clears the floating theme/sfx toggle below (they float
       at drawerTop + 14px, 30px tall — see their rule further down) so
       those circles sit in blank space above the "Menu" label instead of
       covering it or the account/stat row right under it. */
    padding: 54px 18px 22px;
    background: var(--ink);
    border-radius: 0 0 14px 14px;
    box-shadow: 0 14px 28px rgba(0, 0, 0, .35);
  }
  /* A menu that's just a taller version of the desktop link row reads as an
     afterthought — bigger touch targets, a label so it's not just a wall of
     text, and the stat/login/avatar row set apart from the actual links
     with its own divider gives it a shape of its own. */
  .site-nav.site-nav--open::before {
    content: "Menu";
    display: block;
    padding: 14px 0 2px;
    font: 600 10px/1 var(--mono);
    letter-spacing: .14em;
    text-transform: uppercase;
    color: rgba(247, 245, 240, .45);
  }
  /* .site-nav__links is its own flex row (desktop's horizontal-scroll
     wrapper, added so an overflowing link list scrolls instead of pushing
     the login pill off-screen) — inside the open drawer's vertical layout
     that would make the links scroll sideways in their own strip instead
     of stacking, so it needs its own override here, not just the anchors. */
  .site-nav.site-nav--open .site-nav__links {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    overflow: visible;
    gap: 0;
    width: 100%;
    border-top: 1px solid rgba(247, 245, 240, .14);
    margin-top: 2px;
  }
  .site-nav.site-nav--open .site-nav__links > a:not(.site-nav__login):not(.site-nav__stat) {
    display: block;
    padding: 15px 2px;
    font-size: 16px;
    font-weight: 500;
    border-bottom: 1px solid rgba(247, 245, 240, .14);
  }
  /* The games trigger reads like just another row in the drawer — same
     padding/border as a plain link — but isn't one: it's not a real
     destination, so no aria-current underline and the caret is dropped
     (the panel is already open here, an arrow would be a leftover). The
     ::after rule was missing though — .site-nav a[aria-current="page"]
     still applied it at full width (the underline is normally a few px
     under short inline nav text; stretched across this row's full width
     by the drawer's block layout it read as a stray, misplaced bar). */
  /* A real, clickable toggle — same open/close behaviour nav.js already
     wires up for desktop's dropdown. Forcing the panel permanently open
     (as this used to) made the drawer a long scroll of game badges with
     Leaderboard and the rest pushed out of view below the fold; letting a
     reader open it only when they want it keeps the drawer short by
     default. */
  .site-nav.site-nav--open .site-nav__games-trigger {
    display: flex;
    padding: 15px 2px;
    font-size: 16px;
    font-weight: 500;
    border-bottom: 1px solid rgba(247, 245, 240, .14);
  }
  .site-nav.site-nav--open .site-nav__games-trigger::after { display: none; }
  /* Desktop's absolute-positioned popover would float over the drawer's
     own content if left as-is — dropped into the document flow instead,
     full width, but keeping its own light card background/border rather
     than inheriting the drawer's dark one, so the same badge-row list
     reads the same in both places without a separate light/dark text
     override. */
  .site-nav.site-nav--open .site-nav__games-panel {
    position: static;
    width: 100%;
    margin: 10px 0 4px;
  }
  .site-nav.site-nav--open .site-nav__games-panel a {
    padding: 12px 10px;
    font-size: 15px;
  }
  .site-nav.site-nav--open .site-nav__stat {
    margin: 4px 0 14px;
    align-self: flex-start;
  }
  /* Sign out and the avatar are two halves of one account row, not two
     stacked blocks — they sit side by side, avatar first since it's the
     thing a returning reader recognizes fastest. */
  .site-nav.site-nav--open .site-nav__login,
  .site-nav.site-nav--open .account-menu {
    margin: 14px 0 14px;
    align-self: flex-start;
  }
  .site-nav.site-nav--open .account-menu { order: 1; margin-right: 10px; }
  .site-nav.site-nav--open .site-nav__login { order: 2; }
  /* Desktop anchors the panel to the avatar's own right edge (position:
     relative on a ~26px-wide .account-menu), which is fine when that
     wrapper sits near the viewport's right edge. In the open mobile
     drawer the avatar sits near the left instead, so a right:0 panel
     hangs its 240px off to the left and runs past the viewport edge.
     Pinning it to the viewport with fixed positioning (centered, capped
     to the available width) keeps it on-screen regardless of where the
     avatar itself ends up in the drawer's flex-wrap layout. */
  .site-nav.site-nav--open .account-menu__panel {
    position: fixed;
    top: 70px;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: calc(100vw - 32px);
    max-width: 320px;
    max-height: calc(100vh - 90px);
  }

  /* Reveals the theme toggle again, repositioned to float over the open
     dropdown, when the nav has .site-nav--open. :has() rather than moving
     the button into the nav DOM — nav.js clears and rebuilds that
     element's children on every repaint (session resolving), which would
     silently delete a button living inside it, same reason it's a header
     sibling in the first place. Browsers without :has() just keep the
     toggle hidden on mobile, a minor loss of reach, not a broken control. */
  .site-header:has(.site-nav.site-nav--open) .theme-toggle {
    display: flex;
    position: fixed;
    /* Same top formula the drawer itself uses (plus a 14px gap), not a raw
       60px — a raw value only happened to clear the drawer's content on a
       device with a 0 safe-area-inset-top; anywhere that inset is nonzero
       the drawer (and its "Menu" label/first row) shifts down with it but
       this fixed button didn't, so it drifted from "just above the label"
       to "on top of the label/first row". */
    top: calc(50px + env(safe-area-inset-top) + 14px);
    right: 20px;
    z-index: 90;
  }
  /* Shares .theme-toggle for its base look (same reveal-over-the-dropdown
     trick), but needs its own slot so the two floating buttons don't stack
     on the same 20px corner. */
  .site-header:has(.site-nav.site-nav--open) .sfx-toggle {
    right: 62px;
  }
  .wordmark { font-size: 18px; }
  .hero h1 { font-size: 30px; }
  .page-title { font-size: 26px; }
  /* minmax(0, 1fr), not bare 1fr: a bare 1fr track's minimum width is
     "auto" — the size of its content's longest unbreakable run — so a grid
     item full of short-but-unbreakable text (an input suffix like "$/kWh")
     could still force the track wider than the viewport despite there
     being exactly one column. minmax(0, 1fr) is what actually lets a track
     shrink below its content's natural size. */
  .grid-2, .calc-grid, .result-grid, .desk-top, .streak-row, .input-pair {
    grid-template-columns: minmax(0, 1fr);
    display: grid;
    gap: 16px;
  }
  .call-card { padding: 18px; }
  .call-card__symbol { font-size: 28px; }
  .spark { height: 70px; }
  .calls--three { grid-template-columns: 1fr; }
  .result__total { font-size: 42px; }
  .trow { grid-template-columns: 20px 1.4fr .7fr .6fr; }
  .trow > :nth-child(n + 5) { display: none; }
  .compare-tray { margin-inline: 0; }

  /* Eight board chips wrapped into ragged, hard-to-scan rows at this width
     — collapsed behind the toggle button (.board-switch__toggle,
     display:none above 760px) instead. */
  .board-switch__toggle { display: flex; }
  .chips#board-switch {
    display: none;
    position: absolute;
    left: 0; right: 0; top: calc(100% + 6px);
    z-index: 30;
    flex-direction: column;
    gap: 0;
    background: var(--card);
    border: 1px solid var(--rule);
    box-shadow: 0 10px 24px rgba(0, 0, 0, .16);
  }
  .board-switch--open .chips#board-switch { display: flex; }
  .chips#board-switch .chip {
    border: 0;
    border-bottom: 1px solid var(--rule-2);
    border-radius: 0;
    text-align: left;
    padding: 12px 14px;
  }
  .chips#board-switch .chip:last-child { border-bottom: 0; }
  .chips#board-switch .chip[aria-pressed="true"] { background: var(--mint-bg); color: var(--ink); }
  .sticks { padding: 16px; }
  .sticks__head { flex-direction: column; gap: 14px; }
  .sticks__scores { text-align: left; }
  .sticks__controls { flex-direction: column; align-items: stretch; gap: 14px; }
  .sticks__deal { width: 100%; }
  .flappy, .wordle, .sudoku { padding: 16px; }
  .flappy__head, .wordle__head, .sudoku__head { flex-direction: column; gap: 14px; }
  .sudoku__body { flex-direction: column; align-items: stretch; }
  .sudoku__side { width: 100%; flex-direction: row; flex-wrap: wrap; }
  .sudoku__pad { flex: 1 1 160px; }
  /* The on-screen keyboard is the only input method on a phone, so its keys
     get the room the desktop layout doesn't need to spare: tighter gaps and
     no side padding on the row gets every key closer to a real thumb-sized
     tap target instead of the ~44px minimum getting eaten by panel padding. */
  .wordle__grid { max-width: 100%; }
  .wordle__keyboard { gap: 5px; }
  .wordle__krow { gap: 3px; }
  .wordle__key { padding: 14px 0; font-size: 11px; }
  .wordle__key--wide { font-size: 9px; }
  .ad-well--wall { height: 250px; margin-inline: 0; }
  .ad-wall__more { margin-inline: 0; }
  .wall-counter { gap: 12px; font-size: 10px; }

  /* Touch targets: a mouse pointer is forgiving of a 32px button, a thumb
     is not. Bumped toward the ~44px minimum here rather than everywhere,
     since desktop's tighter density is deliberate and correct for a
     pointer. */
  .btn { padding: 13px 18px; }
  .chip { padding: 11px 13px; font-size: 12px; }
  .check__box { width: 18px; height: 18px; }
  .fav { padding: 8px; font-size: 16px; }
  .site-nav__login { padding: 9px 14px; }
  .call-btn { padding: 18px 14px; }

  /* Panels and cards lose a little padding to give content more of a
     narrow screen's limited width, matching what .sticks/.flappy/.wordle
     already do above. */
  .panel { padding: 18px; }
  .entry-card { padding: 18px; }
  .call-card { padding: 16px; }

  /* Ad-clearance geometry is unaffected — these are typography/opacity
     changes on the mobile-only overlay dialogs, kept legible at arm's
     length on a phone screen rather than the wide-monitor sizing. */
  .intro-tour, .game-gate { width: 100%; }
  .intro-tour__title, .game-gate__title { font-size: 19px; }
}

/* ---------- motion ----------
   Baseline transitions + a light load-in for the whole site, not just the
   theme-paper pages. Kept to color/transform/opacity/shadow, since geometry
   changes here would fight the ad-clearance rules geometry.mjs checks. */

@media (prefers-reduced-motion: no-preference) {
  a, .site-nav a, .site-nav__login, .site-nav__stat, .avatar, .wordmark, .btn, .chip, .check__box,
  .call-btn, .trow, .streak-cell, .signin-nudge__dismiss, .theme-toggle,
  .entry-card__guide-link, .filters__reset, .filters__jump,
  .flappy__exit-fullscreen, .account-menu__save {
    transition: color .15s ease, background-color .15s ease,
      border-color .15s ease, opacity .15s ease, transform .15s ease,
      box-shadow .15s ease;
  }
  .site-nav a::after { transition: transform .18s ease; }

  .btn:hover:not([disabled]) { transform: translateY(-1px); }
  .btn:active:not([disabled]) { transform: translateY(0); }

  .call-btn:hover:not([disabled]) { transform: translateY(-2px); }
  .call-btn:active:not([disabled]) { transform: translateY(0); }

  .trow:not(.trow--head):hover { transform: translateX(2px); }
  .wordle__key:hover { transform: translateY(-1px); }
  .flappy__exit-fullscreen:hover { transform: scale(1.08); }
  .account-menu__save:hover:not(:disabled) { transform: translateY(-1px); }
  .account-menu__panel { animation: rise-in .18s ease both; }
  .site-nav__login:hover { transform: translateY(-1px); }
  .site-nav__login:active { transform: translateY(0); }
  /* The streak/time/score readout is not interactive, but sitting dead next
     to links and stats that do respond to a hover read as broken rather
     than "intentionally static" — a faint lift says "this is part of the
     same live header" without implying it's clickable. */
  .site-nav__stat:hover { transform: translateY(-1px); opacity: .85; }

  .theme-toggle:hover { transform: rotate(-14deg) scale(1.08); }
  .theme-toggle:active { transform: rotate(-14deg) scale(0.94); }

  /* A dark-mode toggle with no transition reads as a flash; a few hundred
     surfaces crossfading their color at once reads as a theme change. */
  body, .page, .site-header, .panel, .panel--dark, .entry-card, .table,
  .sticks, .flappy, .wordle, .input, .call-card, .wordle__cell, .wordle__key,
  .ad-well, .site-footer {
    transition: background-color .25s ease, color .25s ease, border-color .25s ease;
  }

  .avatar { transition: transform .15s ease; }
  .avatar:hover { transform: scale(1.08); }

  .wordmark:hover { transform: translateY(-1px); }

  /* No translate on hover: moving the card while its grid neighbor stays
     put opened a sliver of bare page background between the two along
     their shared edge — a "gap" appearing on hover rather than a lift.
     The shadow growing alone reads as raised without displacing the box. */
  a.entry-card { transition: border-color .15s ease, box-shadow .18s ease; }
  a.entry-card:hover { box-shadow: 0 10px 24px rgba(0, 0, 0, .12); }
  a.entry-card.panel--dark:hover { box-shadow: 0 10px 24px rgba(0, 0, 0, .3); }

  /* A daily game not yet played today breathes gently so it's the thing
     the eye lands on, without being an alarming flashing CTA. */
  .entry-card__cta--pending { animation: cta-breathe 2.2s ease-in-out infinite; }

  .flappy__fullscreen { transition: transform .15s ease, background-color .15s ease; }
  .flappy__fullscreen:hover { transform: scale(1.1) rotate(-8deg); }
  .flappy__fullscreen:active { transform: scale(.94); }

  #continue:not([disabled]) { animation: cta-breathe 1.8s ease-in-out infinite; }

  .wordle__swatch { animation: cell-pop .3s ease both; }
  .wordle__swatch--present { animation-delay: .05s; }
  .wordle__swatch--miss { animation-delay: .1s; }

  .ad-well[data-filled] { animation: fade-in .3s ease both; }

  .streak-cell--hit { animation: cell-pop .25s ease; }

  .site-nav__stat[data-streak-value],
  .site-nav__stat[data-score-value] { transition: color .2s ease; }

  /* The header itself settles in on load, same as the cards below it. */
  /* fill-mode: backwards, not both — a "both"/"forwards" fill keeps the
     animation engine applying its final transform value forever, and per
     spec ANY non-none transform on an ancestor (even a no-op
     translateY(0), which is what the last keyframe here is) makes that
     ancestor the containing block for position:fixed descendants. That
     silently broke the mobile nav dropdown: its "fixed to the viewport"
     positioning got scoped inside .site-header's own (DOM-early) stacking
     context instead, so later page content painted on top of it despite
     the dropdown's higher z-index. backwards only holds the FROM state
     during any pre-animation delay (preventing a flash), and stops
     applying anything once the animation completes — which is the same
     visual result here anyway, since the end keyframe matches the
     element's normal un-animated appearance. */
  .site-header { animation: header-in .3s ease backwards; }

  /* Cards & panels ease in on first paint; each stays put once settled so
     re-renders (e.g. nav repainting on session resolve) don't replay it. */
  .entry-card, .panel, .call-card, .table, .portfolio, .ledger, .sticks {
    animation: rise-in .38s cubic-bezier(.2, .7, .2, 1) both;
  }
  .intro-tour, .game-gate { animation: rise-in .25s ease both; }
  .intro-tour-backdrop, .game-gate-backdrop { animation: fade-in .18s ease both; }
  .grid-2 .entry-card:nth-child(2),
  .entry-card-wrap .entry-card:nth-child(2) { animation-delay: .05s; }

  .toast, .saved { animation: rise-in .2s ease both; }
}

@keyframes rise-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes cell-pop {
  from { transform: scale(.7); }
  to   { transform: scale(1); }
}

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

@keyframes header-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes cta-breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.06); }
}

@media (prefers-reduced-motion: reduce) {
  * { scroll-behavior: auto !important; }
}
