/* ============================================================
 * Connect the Thoughts — shared arcade UI components.
 *
 * CANONICAL SOURCE. Edit only here (connectthethoughts/shared/), then run
 *   node scripts/sync-shared.mjs
 * to vend byte-identical copies into each game repo. `--check` flags drift.
 *
 * Token-driven via --accent. Per the arcade cohesion contract (tokens.css,
 * shared/README.md), games do NOT override --accent: there is ONE brand accent
 * (teal) across all games, so the chrome AND these components stay consistent.
 * Per-game identity lives on the homepage cards (data-tone) and in each game's
 * own board/illustration CSS — not in --accent. Text that sits ON the accent
 * uses --on-accent (white in light mode; a dark value in dark mode, set in
 * tokens.css — the dark teal accent needs dark on-accent text).
 * ============================================================ */

/* ── Semantic state tokens + feedback cues (shared; promoted from switchback) ──
 * --good = correct/satisfied · --warn = incomplete/not-yet · --bad = error/overshoot.
 * Pair every colour cue with a glyph/word (.cue-* or a caption) so feedback is never
 * colour-only. Live correctness cues belong ONLY where they don't spoil the answer. */
:root { --good: #16a34a; --warn: #ca8a04; --bad: #dc2626; }
[data-theme="dark"] { --good: #4ade80; --warn: #facc15; --bad: #f87171; }

/* Inline feedback text */
.feedback.good { color: var(--good); font-weight: 700; }
.feedback.warn { color: var(--warn); }
.feedback.bad  { color: var(--bad); }

/* Colour-blind-safe glyph helper — prefix any cue text/element with a symbol so the
 * meaning survives without colour. Unconditional (not gated behind a setting). */
.cue-good::before { content: "✓ "; }
.cue-warn::before { content: "~ "; }
.cue-bad::before  { content: "✗ "; }

/* Tri-state target counter (current / target): under = not-yet, match = satisfied,
 * over = overshoot. Colours the value + caption. Safe only for non-spoiler metrics. */
.con { display: flex; flex-direction: column; gap: 1px; }
.con-label { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-subtle); font-weight: 600; }
.con-value { font-size: 20px; font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; transition: color 0.15s ease; }
.con-note  { font-size: 11px; font-weight: 600; color: var(--fg-subtle); min-height: 1.1em; transition: color 0.15s ease; }
/* THE DASH-PLACEHOLDER CONVENTION (arcade-wide).
   "No value yet" is an EN dash in --fg-subtle, supplied by the stylesheet, not
   a literal typed into markup or a template string. Games had been hardcoding
   their own — tumbler's `best <b>–</b>` chip, arcade-placements' '—' — so the
   glyph, the ink and the alignment drifted, and an empty value that nobody
   remembered to fill rendered as a bare floating dash with no label ink to tie
   it to. Leave the element EMPTY and it gets the placeholder for free; write a
   value and the placeholder disappears. Never type the dash. */
.con-value:empty::before,
.stat-value:empty::before,
.results-stat:empty::before { content: "\2013"; color: var(--fg-subtle); font-weight: 400; }
.con.is-under .con-value, .con.is-under .con-note { color: var(--warn); }
.con.is-match .con-value, .con.is-match .con-note { color: var(--good); }
.con.is-over  .con-value, .con.is-over  .con-note { color: var(--bad); }

/* Self-announcing solve — pulse a primary action once the answer is provably correct.
 * Use #submit-btn.pulse (legacy) or .btn.is-solved. Text flips per theme for contrast. */
#submit-btn.pulse, .btn.is-solved {
  background: var(--good); border-color: var(--good); color: #fff;
  animation: submit-pulse 1.1s ease-in-out infinite;
}
[data-theme="dark"] #submit-btn.pulse, [data-theme="dark"] .btn.is-solved { color: #06302b; }
@keyframes submit-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--good) 55%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--good) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  #submit-btn.pulse, .btn.is-solved { animation: none; }
}

/* ════════════════════════════════════════════════════════════════════════
   THE GAME SHELL — the one page skeleton every arcade game uses.

   Three bands, always in this order, so a player who learns one game can
   read every other one at a glance:

     ┌──────────────────────────────────────────────┐
     │ 🏠  ◈ Cornerstone        📅  📊  🌙  ❓      │  .topbar   (chrome.css)
     ├──────────────────────────────────────────────┤
     │ [Daily|Practice] [Easy|Med|Hard] Daily·2026-8-8 │  .game-bar
     ├──────────────────────────────────────────────┤
     │ ╭──────────────────────────────────────────╮ │
     │ │  💡        Cornerstone            ↻      │ │  .play-head
     │ │  ────────────────────────────────────    │ │
     │ │              (the board)                 │ │  .play-body
     │ ╰──────────────────────────────────────────╯ │  .play-card
     └──────────────────────────────────────────────┘

   The band each control belongs to is decided by WHO OWNS IT, not by what
   fits:
     topbar    — the arcade owns it. Identical on every game, always.
     game bar  — which puzzle you're playing. Settings, not the game: kept
                 deliberately small so it never out-weighs the board.
     play head — the GAME owns it. Its title, and its own in-play controls
                 (hint, rotate, undo, mute) flanking that title.

   This replaced the old arrangement where a game's controls were mounted
   into the topbar. That put game-specific buttons inside arcade-wide chrome,
   so no two topbars matched and the brand had to be hidden to make room.
   Now the topbar is fixed and the game's controls sit with the game.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Band 2: the game bar ──
   ONE line under the topbar: mode on the left, difficulty next to it, an
   optional puzzle label pushed to the right. Wraps to a second line only
   when a game genuinely has too much to fit. Games mount into the children
   with createMode() / createDifficulty() — the bar itself is just markup. */
.game-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin: 0 0 14px;
}
/* `display: flex` above beats the UA's `[hidden] { display: none }` (same
   specificity, later rule wins), so `bar.hidden = true` — the normal arcade way
   to hide a band — did nothing and every game's bar stayed on screen. Restore
   it here so `hidden` works everywhere instead of each game re-discovering the
   problem and patching it in its own stylesheet. */
.game-bar[hidden] { display: none; }
/* Neither control brings its own outer margin inside the bar — the bar owns
   the spacing, so a game that shows only one of them looks the same.

   THAT IS AN INVARIANT, not a default: `align-items: center` above centres each
   child's MARGIN box, so ANY block margin on a bar child slides that child up
   or down by half of it while its neighbours stay put — the row silently stops
   being a row. It is invisible in the CSS that causes it (stencil's game.css
   said `#diffRow { margin: 0 0 .75rem }`, which reads like harmless spacing and
   rode the chips 6px above Daily/Practice for weeks). The `margin: 0` below is
   the statement, not the defence: a game's `#id` selector out-specifies it, so
   the enforcement is `scripts/check-vended-css.mjs`, which fails any game rule
   putting a block margin on a bar child — through the class OR through the
   game's own mount-point id.

   The mode toggle never shrinks (two short labels, and a squeezed Daily/Practice
   reads as broken). The difficulty row MUST be allowed to shrink: with captions
   under the labels ("Easy / 2×2", "Medium / 2×3") the three chips are wider than
   a 375px phone, and at `flex: 0 0 auto` they don't wrap — they push the whole
   bar past the viewport and take every other line with them, because a flex
   line wider than its container overflows rather than wrapping. `0 1 auto` plus
   `min-width: 0` lets the row shrink so its own flex-wrap can do its job. */
.game-bar > .mode-toggle { flex: 0 0 auto; margin: 0; }
.game-bar > .diff-row { flex: 0 1 auto; margin: 0; min-width: 0; }
/* The label (e.g. "Daily · 2026-8-8", "Archive · 2026-7-4") rides the right
   end — a calendar date, never a day number (standard control 7). It is
   context, not a control, so it is quiet and never grows the bar's height. */
.game-bar-note {
  /* `flex: 1 1 0` — a ZERO BASIS, and that is the whole point. A wrapping flex
     container breaks lines using each item's hypothetical size BEFORE it
     shrinks anything, so an `auto` basis made this note the item that fell off
     the line: connect-the-thoughts' bar is 432px inside its 480px frame, its
     mode toggle + difficulty chips + note came to 446px, and 14px of overflow
     dropped "Daily · 2026-8-9" onto a line of its own under the chips (Mike,
     2026-08-09 — "the 'daily <date>' text sits below the header instead of in
     line with it"). At basis 0 the note can never be the reason the bar wraps;
     it takes whatever room is left on the line and ellipsises into it, which is
     what the overflow/text-overflow pair below was always for. It also replaces
     `margin-left: auto` — growing into the free space puts it at the right end
     by itself, and `text-align` keeps the words there once it is full-width. */
  flex: 1 1 0;
  text-align: right;
  max-width: 100%;
  font-size: 12px;
  font-weight: 600;
  color: var(--fg-subtle);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.game-bar-note:empty { display: none; }

/* ── Band 3: the play card ──
   The board and everything that belongs to it. A game whose board is a bare
   canvas can skip the card and use .play-head on its own — the head is the
   part that has to be consistent. */
.play-card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 6px);
  padding: clamp(12px, 3vw, 22px);
}

/* ── The play head: [tools] title [tools] ──
   A 3-column grid, not a flex row, and that choice is load-bearing. `1fr` here
   means `minmax(auto, 1fr)`: the two side columns are always EQUAL to each
   other (so the title is optically centred whether a game mounts two tools,
   one, or none) but they can never shrink below the tools they hold. A flex row
   gets exactly one of those two properties at a time — with `flex: 1 1 0` the
   slots collapse to zero and the buttons sit on top of the title.
   The middle is `minmax(0, auto)` so a long title ellipsizes instead of pushing
   the tools off the card. */
.play-head {
  display: grid;
  /* The tool columns take what they NEED; the title takes the rest.
     This was `1fr minmax(0, auto) 1fr` — symmetric columns, which centres the
     title perfectly and pays for it in the one currency the head is short of.
     `1fr` is `minmax(auto, 1fr)`, so BOTH tool columns grew to the larger
     side's min-content and then split the free space evenly: connect-the-
     thoughts has three lead tools (undo/redo/restart ≈ 118px) and one trail
     tool (34px), so 84px was reserved on the right for nothing at all and the
     title ellipsised to "Connect the Tho…" (Mike, 2026-08-09).
     With `auto … auto` the trail column takes its 34px and the title gets the
     other 84. A head with matching sides — or none at all — is still exactly
     centred, because equal `auto` columns are equal; only an ASYMMETRIC head
     shifts, and being a few pixels off centre beats losing the words. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  margin: 0 0 12px;
}
.play-head-main {
  min-width: 0;
  text-align: center;
}
.play-title {
  margin: 0;
  font-size: clamp(19px, 5.2vw, 26px);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--fg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* WHAT THIS BOARD IS — its subject, in ≤ 4 words: a theme, a category, a
   puzzle name (README standard control 3). Never a rule, never a board
   descriptor, never a fact the board already shows. It survives on a phone —
   see the ≤480px block. */
.play-sub {
  margin: 3px 0 0;
  font-size: 13px;
  line-height: 1.35;
  color: var(--fg-muted);
}
/* A game with no board subject leaves it EMPTY, and an empty sub reserves no
   space. Canonized 2026-08-08 because the 2026-08-08 prose sweep emptied this
   element in fourteen games and produced TWO markup shapes for one decision —
   ten with a `hidden` attribute, four bare. Neither game should have had to
   choose: `:empty` collapses it either way, so `hidden` is now redundant. */
.play-sub:empty { display: none; }

/* ── .game-head — the play head's STATUS variant ──
   Same band, same place, same job: it's the top row of the play area. The
   difference is what the middle carries. A game with a fixed title uses
   .play-head (centred title, tools either side). A game with a RUNNING state —
   which case you're on, which category you're ranking, round 2 of 5 — uses this
   instead: the context line left-aligned, the live scorebox on the right.

   These rules were eight near-identical copies scattered through ladder,
   mosaic, switchback, witness, doublet-cross, mimic, auction and stencil. They
   are canonical here now; a game keeps only its own context-line classes
   (.case-name, .topic-name, .theme-label …). */
.game-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  padding-bottom: 12px;
  margin-bottom: 14px;
}
.scorebox { display: flex; align-items: center; gap: 16px; }
.stat { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.1; }
.stat-label { font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-subtle); }
.stat-value { font-size: 15px; color: var(--fg-muted); font-variant-numeric: tabular-nums; }

/* ── Game tools (shared/arcade-tools.js) ──
   A game's OWN always-visible controls. Accent-tinted, because they belong to
   the game rather than to the arcade chrome and have to read as tappable at a
   glance mid-solve. Empty slots keep their width (see above) but draw nothing. */
.play-tools {
  display: flex;
  align-items: center;
  gap: 6px;
}
.play-tools[data-play-tools-lead] { justify-content: flex-start; }
.play-tools[data-play-tools-trail] { justify-content: flex-end; }
.play-tool {
  position: relative;
  flex: none;
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.play-tool:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
.play-tool:disabled { opacity: 0.4; cursor: not-allowed; }
/* A LABELLED tool — the glyph plus its word, for a tool whose verb is core to
   play (tumbler's Rotate). Grows to fit the word, keeps the 44px floor, and the
   word never wraps or ellipsises: if it cannot fit, the label is too long. */
/* `.play-head .play-tool` (0,2,0) is what the ≤480px block narrows to 34px, so
   this carries one extra class to out-specify it — otherwise the word renders
   inside a 44px square and spills. */
.play-head .play-tool.has-label,
.play-tool.has-label {
  width: auto;
  min-width: 44px;
  min-height: 44px;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-radius: 999px;
}
.play-tool .play-tool-text {
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  letter-spacing: 0.01em;
}
/* A labelled tool carries its word, so the badge rides beside it, not over a
   corner of the glyph. */
.play-tool.has-label .play-tool-badge { position: static; margin-left: 2px; }
/* Count pill (e.g. hints remaining) riding the tool's corner, so "how many do
   I have left" is answerable without scrolling to the stats. */
.play-tool-badge {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent, #fff);
  font-size: 10px;
  font-weight: 700;
  line-height: 17px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* Games render the badge as `<span class="play-tool-badge hidden">0</span>` and
   toggle `hidden` from script, so the component must honour it itself — a game
   without its own page-wide `.hidden` utility would show a permanent "0". */
.play-tool-badge.hidden { display: none; }

@media (max-width: 480px) {
  .play-card { padding: 12px 10px; }
  .play-head { min-height: 34px; margin-bottom: 10px; }
  .play-head .icon-button { width: 34px; height: 34px; }
  /* The sub line STAYS on a phone (fixed 2026-08-08). Hiding it was cheap
     vertical space with an invisible cost: it is where games put the fact the
     player needs most — afterglow's "Round 1 of 5", the dictionary game's and
     stencil's theme of the day — so on the one viewport everybody plays on,
     that fact silently vanished. Kept to ONE ellipsised line (~18px) instead,
     which every game's measured chrome budget absorbs inside its clamp(). */
  .play-sub {
    font-size: 12px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .game-bar { gap: 6px 8px; margin-bottom: 10px; }
}

/* ── Buttons ── */
.btn {
  background: var(--accent);
  color: var(--on-accent, #fff);
  border: none;
  border-radius: 10px;
  padding: 12px 18px;
  /* The 44px touch floor, measured 2026-08-08: 12px + 15px line + 12px lands
     at 41px, so the arcade's most-pressed control — every game's primary
     action — sat 3px UNDER the floor chrome.css enforces on the icon button,
     the close-×, the give-up link and the play tool. `min-height` (not a
     transparent ::after) because a .btn's own box is the hit area and nothing
     inside it needs to keep its size. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  text-align: center;
  cursor: pointer;
  text-decoration: none; /* clean when .btn is applied to an <a> (e.g. the "Next daily" link) */
  transition: filter 0.12s, transform 0.08s;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.btn:hover:not(:disabled) { filter: brightness(1.08); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn.secondary { background: transparent; color: var(--fg-muted); border: 1px solid var(--border); }
.btn.secondary:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
.btn.ghost { background: transparent; color: var(--accent); border: 1px dashed var(--accent); }
.btn.danger { background: transparent; color: var(--bad); border: 1px solid var(--border); }
.btn.danger:hover:not(:disabled) { color: var(--bad); border-color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }

/* ── Give up & reveal (shared control — see shared/arcade-giveup.js) ──
   A last resort, so it is deliberately quiet: a muted underlined link, never a
   primary-looking button. Games mount it at the bottom of a hint sheet / clue
   area or as a foot link. The control carries its OWN top margin so a layout
   row holding nothing but a hidden give-up collapses to zero height.
   `[hidden]` needs an explicit rule here — the UA's `[hidden]{display:none}`
   would otherwise lose to the `display:block` below on specificity. */
/* The quiet foot link. It is a flex box sized to `fit-content` (not a block)
   purely so `min-height` can hold the arcade's 44px touch floor without the
   invisible target spanning the whole card width — `margin: 0 auto` still
   centres it. `position: relative` + the ::after target expansion come from the
   touch-target floor block in chrome.css, which owns that policy for every
   shared control; don't add a second copy here.
   `user-select: none` because a slow press on a phone selected the label
   instead of arming the control, and the player's next tap dismissed a
   selection rather than giving up. */
/* The give-up control's styling lived here and is GONE (2026-08-09, Mike:
   "'Give up and Reveal' is basically never appropriate. It should be purged
   entirely… well-enshrined drift and ugly to boot"). Eleven of the eighteen
   games that carried it revealed nothing you couldn't have worked out — a
   solved latin square, a path, a colour, a random flash pattern — and the six
   with a real answer now simply keep it. The control, its confirm card, its
   row and shared/arcade-giveup.js are all deleted; `arcade-results.js` keeps
   its `giveUp` option only to render results ALREADY stored from before the
   purge. Do not reintroduce either. */

/* ── Difficulty selector (Easy / Medium / Hard) ──
   Compact chips, sized to their labels — NOT three full-width slabs. Choosing
   a difficulty is a setting you touch once; at full width it read as the most
   important thing on the screen, above the board it was describing. This is
   guess-the-date's slim row promoted to the arcade default.

   NO OUTER MARGIN, on purpose. This row used to carry `margin-bottom: 14px`
   from the days when it sat on its own line above the board; every consumer
   now mounts it inside `.game-bar`, where the bar's `gap` owns the spacing and
   that legacy margin is something each game had to CANCEL. It never announced
   itself as a margin bug either — `.game-bar` centres its children, and flexbox
   centres the MARGIN box, so a bottom margin here silently lifts the whole
   Easy/Medium/Hard row by half of it and the chips stop sitting on the
   Daily/Practice line (stencil, 6px high, 2026-08-09). Two games had already
   hand-written a cancel for it. Spacing belongs to the bar — don't give this
   row (or a game's mount div for it) a block margin again. */
.diff-row {
  display: inline-flex;
  gap: 6px;
  flex-wrap: wrap;
}
.diff-btn {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  background: var(--bg-elev);
  color: var(--fg-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 4px);
  padding: 6px 12px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.diff-btn .diff-sub {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--fg-subtle);
  text-transform: uppercase;
}
.diff-btn:hover:not(.active) { border-color: var(--accent); color: var(--fg); }
.diff-btn.active { background: var(--accent); color: var(--on-accent, #fff); border-color: var(--accent); }
.diff-btn.active .diff-sub { color: var(--on-accent, #fff); opacity: 0.85; }

/* ── Results / completion card ── */
.results-card {
  text-align: center;
  background: var(--bg-elev);
  border: 1px solid var(--accent);
  border-radius: var(--radius-md, 12px);
  padding: 22px;
  margin-top: 18px;
  box-shadow: var(--shadow-md);
  animation: results-pop 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28) both;
}
@keyframes results-pop {
  from { opacity: 0; transform: scale(0.9) translateY(8px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}
/* Presentation is centralized in arcade-results.js (presentResults): the card
   is shown inside a centered modal over a dimmed board. Honor a reduced-motion
   preference by dropping the pop-in. */
@media (prefers-reduced-motion: reduce) {
  .results-card { animation: none; }
}

/* ── Card chrome INSIDE the results modal ──
   presentResults moves the card's mount into a canonical `.modal` (chrome.css
   already gives it a border, background, radius, shadow and 24px padding). The
   card's OWN border/background/margin/pop would double that chrome, so strip it
   here — the card becomes plain content and the modal is the surface. The
   celebratory pop moves up to the modal itself (guarded for reduced-motion). */
.results-modal .results-card {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  margin-top: 0;
  padding: 0;
  animation: none;
}
.results-modal {
  animation: results-modal-pop 0.34s cubic-bezier(0.18, 0.89, 0.32, 1.28) both;
}
@keyframes results-modal-pop {
  from { opacity: 0; transform: scale(0.94) translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .results-modal { animation: none; }
}
.results-headline {
  font-size: 20px;
  font-weight: 800;
  color: var(--fg);
  letter-spacing: 0.01em;
  margin-bottom: 8px;
}
.results-time,
.results-stat {
  font-size: 28px;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  margin-bottom: 2px;
}
/* The primary metric line may carry a small unit (e.g. "42<small>strokes</small>"). */
.results-stat small {
  font-size: 15px;
  font-weight: 600;
  color: var(--fg-muted);
  margin-left: 4px;
}
.results-sub {
  font-size: 13px;
  color: var(--fg-muted);
  margin: 8px 0 14px;
  font-variant-numeric: tabular-nums;
}
/* Arcade-wide streak line (streakLineHtml). Its own results-card SLOT
   (`streakHtml`) — see RESULTS-STANDARD.md §2. The legacy call sites that
   concatenate it into subHtml still work; they just pick up the tighter
   in-flow margin below instead of the slot's own rhythm. */
.results-streak {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  margin-top: 4px;
}
.results-card > .results-streak { margin: -6px 0 12px; }

/* Status note — the ONE styling for "this board was revealed" / "this is an
   archive replay". Slot 5 (`giveUp` / `archive` / `noteHtml`). Games used to
   each ship their own .xx-revealed-note class, which is exactly how three
   games ended up saying the same sentence three different sizes. */
.results-note {
  font-size: 13px;
  line-height: 1.45;
  color: var(--fg-muted);
  margin: 0 0 12px;
  text-align: center;
}
.results-note.is-unsolved { color: var(--warn, var(--fg-muted)); font-weight: 600; }
.daily-complete-note {
  margin: 12px 0 4px;
  font-size: 14px;
  font-weight: 600;
  color: var(--accent);
  text-align: center;
}
.placements { margin: 8px 0 4px; text-align: center; }
/* "Your placement today" chips (arcade-placements.js fills the #placements
   mount the results card creates on dailyComplete). Same pill idiom as the
   leaderboard difficulty tabs so the card reads as one family. */
.placements-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--fg-muted);
  margin-bottom: 6px;
}
.placements-row { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
.place-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.place-lbl { font-size: 12px; font-weight: 600; color: var(--fg-muted); }
.results-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}
.results-actions .btn { flex: 1; min-width: 100px; }

/* The results chip + review-note styles were RETIRED with the reopen chip
   (Mike, 2026-08-08) — see shared/arcade-results.js. */
.results-modal .results-card button[disabled],
.results-modal .results-card input[disabled] { opacity: 0.55; }

/* ── Leaderboard difficulty tabs (modal) ── */
.lb-diff-tabs { display: flex; gap: 4px; margin: 8px 0 4px; flex-wrap: wrap; }
.lb-diff-tab {
  background: transparent;
  color: var(--fg-muted);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
  touch-action: manipulation;
}
.lb-diff-tab.active { background: var(--accent); color: var(--on-accent, #fff); border-color: var(--accent); font-weight: 700; }

/* ── Mobile sizing (shared so every game's chrome stays cohesive) ── */
@media (max-width: 480px) {
  .btn { padding: 11px 14px; font-size: 14px; }
  .diff-row { gap: 5px; }
  .diff-btn { padding: 5px 10px; font-size: 12px; }
}

/* ── Daily / Practice mode toggle (arcade-mode.js) ──
 * Practice re-rolls on every tap, including when it's already active, so the
 * active button stays a live control — never style it as disabled.
 * Sized to its two labels and never stretched: a game that lets this span the
 * full width turns a two-word setting into the loudest element on the page. */
.mode-toggle {
  display: inline-flex;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 4px);
  overflow: hidden;
}
/* A game that mounts no modes must not leave an empty bordered box behind. */
.mode-toggle:empty { display: none; }
.mode-btn {
  background: transparent; color: var(--fg-muted); border: none;
  padding: 6px 13px; font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  white-space: nowrap;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
@media (max-width: 480px) {
  .mode-btn { padding: 5px 11px; font-size: 12px; }
}
.mode-btn.active { background: var(--accent); color: var(--on-accent, #fff); }
.mode-btn:not(.active):hover { color: var(--fg); }

/* ── Leaderboard modal + inline board ── */
.lb-blurb { font-size: 13px; color: var(--fg-muted); margin: -4px 0 14px; text-align: center; }
.lb-body { min-height: 60px; }
.lb-nav { display: flex; align-items: center; justify-content: space-between; margin: 4px 0 12px; gap: 8px; }
/* ~28px tall by its padding; the 44px tappable area comes from the touch-target
   floor in chrome.css (which owns that policy for every shared control). */
.lb-nav-btn { background: none; border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; cursor: pointer; color: var(--fg); font-size: 16px; touch-action: manipulation; }
.lb-nav-btn:disabled { opacity: 0.35; cursor: default; }
#lb-day-label, .lb-day-label { font-size: 13px; color: var(--fg-muted); font-weight: 600; }
.lb-scope-tabs { display: flex; gap: 6px; margin: 8px 0 4px; }
.lb-scope-tab {
  flex: 1; background: transparent; color: var(--fg-muted); border: 1px solid var(--border);
  border-radius: 8px; padding: 6px 8px; font-size: 13px; font-weight: 600; cursor: pointer; touch-action: manipulation;
}
.lb-scope-tab.active { background: var(--accent); color: var(--on-accent, #fff); border-color: var(--accent); }
.lb-diff-tabs { display: flex; gap: 4px; margin: 8px 0 4px; flex-wrap: wrap; }
.lb-you-head { font-size: 13px; font-weight: 700; margin: 2px 0 8px; }
.lb-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.lb-row { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 6px; font-size: 13px; }
.lb-row.me { background: color-mix(in srgb, var(--accent) 22%, var(--bg-elev)); outline: 1px solid var(--accent); }
.lb-rank { width: 1.6em; text-align: right; color: var(--fg-subtle); font-variant-numeric: tabular-nums; font-weight: 700; }
.lb-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-score { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--accent); }
.lb-status { font-size: 13px; color: var(--fg-muted); text-align: center; padding: 14px 0; }
.lb-you-note { padding-top: 8px; }
.lb-hint { font-size: 13px; color: var(--fg-subtle); text-align: center; }
.lb-inline { margin: 14px 0 4px; text-align: left; }
.lb-inline:empty { display: none; }
/* Results-card leaderboard: the small "Today's leaderboard" heading a game
   passes via presentResults({ detailHtml }), and the scroll cap on the
   #lb-inline mount beneath it. */
.results-lb-title { font-size: 0.82rem; font-weight: 700; color: var(--fg-muted); margin-bottom: 6px; text-align: center; }
.lb-scroll { max-height: 216px; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.lb-modal-inner { background: var(--bg-elev); border-radius: var(--radius-md, 12px); padding: 24px; max-width: 440px; width: 90%; max-height: 90vh; overflow-y: auto; position: relative; }
.lb-join { padding: 0.5rem 0; }
.lb-join-title { font-size: 14px; font-weight: 700; margin-bottom: 8px; text-align: center; }
.lb-join-row { display: flex; gap: 8px; }
.lb-input { flex: 1; padding: 8px 10px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); font-size: 14px; }
.lb-input:focus { outline: none; border-color: var(--accent); }

/* Leaderboard "You" tab — shared stats panel (solves, streaks, stars
   distribution). Rendered by the leaderboard factory for every game. */
.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 4px 0 16px; }
.stat-tile { background: var(--bg); border: 1px solid var(--border); padding: 12px; border-radius: var(--radius-sm); text-align: center; }
.stat-tile .label { font-size: 12px; color: var(--fg-subtle); text-transform: uppercase; letter-spacing: 0.5px; }
.stat-tile .value { font-size: 28px; font-weight: 700; margin-top: 4px; color: var(--fg); }
.stat-section { margin-top: 16px; }
.stat-section h3 { font-size: 13px; text-transform: uppercase; color: var(--fg-subtle); margin: 8px 0; letter-spacing: 0.5px; }
.bar-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; font-size: 14px; color: var(--fg); }
.bar-row .bar-label { width: 80px; text-align: right; color: var(--fg-muted); }
.bar-row .bar-track { flex: 1; height: 18px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.bar-row .bar-fill { height: 100%; background: var(--accent); transition: width 0.4s ease; display: flex; align-items: center; justify-content: flex-end; color: #fff; font-size: 12px; padding-right: 6px; min-width: 1.5em; }

/* "Past Daily Puzzles" archive calendar (arcade-archive.js). Modal chrome reuses
   the canonical .modal-backdrop/.modal; these style the day list. */
.archive-card { max-width: 460px; }
.archive-list { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; max-height: 62vh; overflow-y: auto; }
.archive-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: pointer; color: var(--fg); font: inherit; text-align: left; transition: border-color 0.15s ease; }
.archive-item:hover { border-color: var(--accent); }
.archive-item__date { font-weight: 600; }
/* `.archive-item__day` (the "Day 220" right column) was removed 2026-08-08 —
   calendar dates throughout, never a day number. See arcade-archive.js. */
.archive-item--today { border-color: var(--accent); }

/* First-play tutorial carousel (arcade-tutorial.js). Modal chrome reuses the
   canonical .modal-backdrop/.modal; these style the slides + nav. */
.tutorial-card { max-width: 460px; text-align: center; }
.tutorial-art { display: flex; align-items: center; justify-content: center; min-height: 84px; margin: 4px 0 14px; color: var(--accent); }
/* `display: flex` out-ranks the [hidden] attribute's UA `display: none`, so the
   art slot kept its 84px min-height + 18px margins on every slide that ships no
   artwork — a 98px hole above the heading of EVERY tutorial step in EVERY game
   (arcade-tutorial.js hides the slot when a step has no `art`). Same trap the
   .icon-button rule in chrome.css documents: any shared class that sets a
   display value must restate [hidden]. */
.tutorial-art[hidden] { display: none; }
.tutorial-art svg { max-width: 100%; height: auto; }
.tutorial-card h2 { margin-bottom: 8px; }
.tutorial-body { color: var(--fg-muted); line-height: 1.55; font-size: 15px; }
.tutorial-body b, .tutorial-body strong { color: var(--fg); }
.tutorial-dots { display: flex; justify-content: center; gap: 8px; margin: 18px 0; }
.tutorial-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border); transition: background 0.15s ease; }
.tutorial-dot.active { background: var(--accent); }
.tutorial-nav { display: flex; gap: 10px; justify-content: space-between; }
.tutorial-nav .tutorial-back[hidden] { display: none; }
.tutorial-nav .tutorial-back { margin-right: auto; }
.tutorial-nav .tutorial-next { margin-left: auto; }
/* "Show me how to play" launcher injected into each game's help/rules modal. */
.tutorial-launch { margin-top: 16px; width: 100%; }

/* ── Inline control chip (`.ui-chip`) ────────────────────────────────────────
 * Quotes a real in-game button inside tutorial slides and help bullets: the
 * button's own SVG icon + its label, styled like .icon-button/.btn at text
 * scale, so "press <chip>Hint</chip>" shows the thing the player will press.
 * Markup only — copy the <svg> inner content from the game's actual button
 * (game-bar controls, or arcade-topbar.html for arcade chrome). No JS.
 * See HELP-STANDARD.md → "Show the button". */
.ui-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 8px 1px 6px; border-radius: 999px;
  background: var(--bg-elev); border: 1px solid var(--border);
  color: var(--fg); font-weight: 700; font-size: 0.88em;
  line-height: 1.4; white-space: nowrap; vertical-align: -0.18em;
}
.ui-chip svg { width: 1em; height: 1em; flex: none; }

/* ── Help modal body (`<ul class="help-list">`) ──────────────────────────────
 * The canonical shape of a game's "How to play" modal, so every game's rules
 * read as one voice: a bulleted list, muted body text, the lead-in of each
 * bullet bolded to carry the scan. Promoted here from three private copies
 * (tumbler / word-sweep / pigeonhole) — put rules in the hub, not per game.
 * See HELP-STANDARD.md for the copy rules (bullet count + word caps). */
.help-list { margin: 0; padding-left: 1.1em; color: var(--fg-muted); line-height: 1.55; }
.help-list li { margin-bottom: 8px; }
.help-list li:last-child { margin-bottom: 0; }
.help-list b, .help-list strong { color: var(--fg); }
/* Feedback cues quoted inside a rule inherit the semantic state tokens above,
   so the help text names a colour the same way the board shows it. */
.help-list .cue-good { color: var(--good); font-weight: 700; }
.help-list .cue-warn { color: var(--warn); font-weight: 700; }
.help-list .cue-bad { color: var(--bad); font-weight: 700; }

/* ── Badges (arcade-badges.js) ───────────────────────────────────────────────
 * Two surfaces: the celebratory STRIP on a finish card, and the CASE (the full
 * collection) on the hub. The strip renders hidden when nothing was earned so a
 * game can append it unconditionally; a badge awarded a beat later (the
 * winboard's board read) un-hides it. */
.badge-strip { margin: 12px 0 4px; text-align: center; }
.badge-strip[hidden] { display: none; }
.badge-strip-title {
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--fg-subtle); font-weight: 700; margin-bottom: 6px;
}
.badge-strip-row { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.badge-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px 5px 7px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, var(--bg-elev));
  border: 1px solid var(--accent);
  font-size: 13px; font-weight: 700; color: var(--fg);
  animation: badge-pop 0.45s cubic-bezier(0.18, 0.89, 0.32, 1.28) both;
}
.badge-chip-icon { font-size: 16px; line-height: 1; }
@keyframes badge-pop { from { opacity: 0; transform: scale(0.7); } to { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .badge-chip { animation: none; } }

/* The case — earned tiles read full-strength, locked ones sit back but keep
 * their unlock rule legible, because the rule is the thing to chase. */
.badge-case-section { margin-top: 18px; }
.badge-case-section h3 {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--fg-subtle); margin: 0 0 10px;
}
.badge-case-count {
  display: inline-block; margin-left: 4px; padding: 1px 7px; border-radius: 999px;
  background: var(--bg); border: 1px solid var(--border); font-size: 11px; color: var(--fg-muted);
}
.badge-case-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 8px; }
.badge-case-empty { color: var(--fg-muted); font-size: 14px; margin: 0 0 4px; }
.badge-tile {
  display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm, 8px);
  position: relative;
}
.badge-tile.earned { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--bg)); }
.badge-tile.locked { opacity: 0.62; }
/* A badge earned since the case was last opened gets a quiet corner dot. */
.badge-tile.fresh::after {
  content: ""; position: absolute; top: 7px; right: 7px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
}
.badge-tile-icon { font-size: 22px; line-height: 1.1; flex: none; }
.badge-tile-body { min-width: 0; }
.badge-tile-name { font-size: 14px; font-weight: 700; color: var(--fg); }
.badge-tile-game {
  display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 999px;
  background: var(--bg-elev); border: 1px solid var(--border);
  font-size: 10px; font-weight: 600; color: var(--fg-muted); vertical-align: 1px;
}
.badge-tile-desc { font-size: 12px; color: var(--fg-muted); line-height: 1.4; margin-top: 2px; }
.badge-tile-date { margin-left: auto; font-size: 10px; color: var(--fg-subtle); white-space: nowrap; }

/* ── Rival nudge (arcade-rivals.js) ──────────────────────────────────────────
 * "Mom passed you in Cornerstone." Deliberately warm rather than alarming —
 * the point is to pull a player back in, not to scold them. */
.rivals-modal { max-width: 420px; text-align: center; }
.rivals-crest { font-size: 34px; line-height: 1; margin-bottom: 4px; }
.rivals-modal h2 { margin: 0 0 14px; }
.rivals-list { display: flex; flex-direction: column; gap: 10px; text-align: left; }
.rivals-row {
  padding: 12px; border-radius: var(--radius-sm, 8px);
  background: var(--bg); border: 1px solid var(--border);
}
.rivals-line { font-size: 15px; color: var(--fg); line-height: 1.45; }
.rivals-rank { font-size: 13px; color: var(--fg-muted); margin-top: 3px; }
.rivals-was { color: var(--fg-subtle); }
.rivals-play { display: inline-block; margin-top: 10px; text-decoration: none; }
.rivals-dismiss { width: 100%; margin-top: 16px; }
.badge-case-locked { margin-top: 22px; padding-top: 14px; border-top: 1px solid var(--border); }
.badge-case-locked-head {
  font-size: 13px; font-weight: 700; color: var(--fg-muted);
  text-transform: uppercase; letter-spacing: 0.08em;
}
.badge-case-locked .badge-case-section { margin-top: 14px; }

/* ── Control plate (`.control-plate` / `.plate-key`) ─────────────────────────
   The material grammar for a board's BUTTON CONTROLS — Futoshiki's number
   pad, Theseus's d-pad. A recessed page-bg tray holding raised bg-elev-2
   keys, so "board" and "controls" read as two different materials at a
   glance (cells sit on the elevated surface; keys sit IN a tray). Extracted
   2026-08-07 from the two identical hand-rolled copies (fu.css / theseus.css)
   — the drift test's bar is two.

   The game owns the GEOMETRY: grid-template columns/rows, key heights, glyph
   sizes, max-width. This file owns the material and the interaction grammar
   (hover, press, focus, disabled, reduced motion) so every plate presses the
   same way arcade-wide. */
.control-plate {
  display: grid;
  gap: 6px;
  padding: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.plate-key {
  display: flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-weight: 700;
  background: var(--bg-elev-2);
  color: var(--fg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease,
              transform 0.06s ease, box-shadow 0.06s ease;
}
.plate-key:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); background: var(--bg-elev); }
.plate-key:active:not(:disabled) {
  transform: translateY(1px) scale(0.96);
  box-shadow: none;
  color: var(--accent);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-elev-2));
}
.plate-key:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.plate-key:disabled { opacity: 0.3; box-shadow: none; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
  .plate-key { transition: none; }
  .plate-key:active:not(:disabled) { transform: none; }
}

/* ── Status line (`.play-status`) ────────────────────────────────────────────
   THE in-play status line: one centred line under the board that speaks in
   the arcade's tri-state voice. Every game was carrying its own copy of this
   exact rule (.ou-status, .fu-status, …) — the classes stay as game-owned
   hooks/overrides, the look lives here once. */
.play-status {
  min-height: 1.4em;
  margin: 8px 0 4px;
  text-align: center;
  font-size: 14px;
  line-height: 1.35;
  color: var(--fg-muted);
}
.play-status.is-good { color: var(--good); font-weight: 600; }
.play-status.is-warn { color: var(--warn); font-weight: 600; }
.play-status.is-bad  { color: var(--bad);  font-weight: 600; }

/* ── The HUD row (`.con-hud`) ────────────────────────────────────────────────
   THE row of live `.con` counters under the play head — the game's ranked
   metric and whatever else the player tracks. Four games had hand-rolled this
   exact centred flex row (`.fu-hud`, `.ag-hud`, `.ch-hud`, and Cornerstone's
   new one) before it was canonized 2026-08-08; a game that lacked the rule
   stacked its counters vertically and ate the board's height. The per-game
   classes stay as hooks/overrides — this owns the layout once. Pair it with
   `createMetricCounter` (arcade-metric-counter.js), never a bespoke panel. */
.con-hud {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 22px;
  margin: 4px 0 10px;
}
.con-hud .con { align-items: center; }
@media (max-width: 380px) { .con-hud { gap: 14px; } }

/* ── Under-board actions (`.play-actions`) ───────────────────────────────────
   THE one control row inside the play card, after the status line and before
   the give-up row. It holds the game's PRIMARY action (`.btn`) and, when a
   game has one, a MOVE-family board action beside it (`.btn.secondary`) —
   tumbler's Rotate costs a move, so it is a play action, not a play-head
   tool. Everything take-back/help/view belongs on the play head
   (arcade-tools.js → THE FAMILY LAW). Buttons HUG their labels: a card-wide
   slab out-weights the board it is about, and `.btn` hugs everywhere else in
   the arcade (measured 2026-08-08: two games shipped 341px slabs while the
   rest hugged at 120–200px). Max three children; a fourth is a design
   escalation, not a wrap. No game may re-declare this class or set a width
   on a `.btn` inside it. */
.play-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 12px 0 0;
}
.play-actions .btn { min-width: 140px; }
.play-actions:empty { display: none; }
@media (max-width: 480px) {
  .play-actions .btn { min-width: 120px; }
}

/* ── Attempt history (`.attempt-*`) ──────────────────────────────────────────
   THE record of judged attempts — the widget seven games hand-rolled before
   2026-08-07 (keyhole's ledger was the best-in-family reference; order-up
   contributed the tappable-row affordance). One anatomy:

     .attempt-head              small uppercase section label ("Attempts")
     .attempt-history           the list (flex column; add .is-empty +
                                data-empty-text for the before-first-try hint)
     .attempt-row               one judged attempt: grid [tag | content | trail]
       .attempt-tag             index / verdict label ("#3 ✗", "GUESS 2")
       .attempt-content         the game's own middle (chips, a word, letters)
       .attempt-trail           optional right column (pin counts, a bid)
     .attempt-chip              a small judged token in the content column

   Verdict grammar, on the arcade tri-state tokens — never a private green:
     row rails    .attempt-row.is-good / .is-bad   (inset rail + tag colour)
                  .attempt-row.is-latest           (accent rail — the row being
                                                    reasoned from)
                  .attempt-row.is-given            (dashed freebie: an opener /
                                                    dealt clue, accent tag)
     chips        .attempt-chip.is-good / .is-warn / .is-bad / .is-given
                  / .is-more (quiet +N overflow)

   A game whose history rows are TAPPABLE (order-up: restore the attempt)
   renders each row as <button class="attempt-row attempt-row--btn">. The
   verdict of a WHOLE line belongs on the tag, never on a single chip, when
   the game's judgement is batch-shaped — that policy stays the game's.

   What stays per-game: the content column's internals (keyhole's letter inks
   are arcade-keys; order-up's anchor chip leads every row), column widths
   beyond the default, and WHAT a row means. */
.attempt-head {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-subtle);
}
.attempt-history {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.attempt-history.is-empty::after {
  content: attr(data-empty-text);
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  padding: 10px 12px;
  color: var(--fg-subtle);
  font-size: 0.85rem;
  text-align: center;
}
.attempt-row {
  display: grid;
  grid-template-columns: var(--attempt-tag-w, 3.6em) minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  border: 1px solid var(--border);
}
.attempt-row--btn {
  width: 100%;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.attempt-row--btn:hover { background: color-mix(in srgb, var(--accent) 7%, var(--bg)); }
.attempt-row--btn:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.attempt-tag {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-subtle);
  font-variant-numeric: tabular-nums;
}
.attempt-content {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  align-items: baseline;
  min-width: 0;
}
.attempt-trail { justify-self: end; }

.attempt-row.is-given { background: transparent; border-style: dashed; border-color: var(--border-strong); }
.attempt-row.is-given .attempt-tag { color: var(--accent); }
.attempt-row.is-latest { border-color: var(--border-strong); box-shadow: inset 3px 0 0 var(--accent); }
.attempt-row.is-good { border-color: var(--good); box-shadow: inset 3px 0 0 var(--good); }
.attempt-row.is-good .attempt-tag { color: var(--good); }
.attempt-row.is-bad { border-color: var(--bad); box-shadow: inset 3px 0 0 var(--bad); }
.attempt-row.is-bad .attempt-tag { color: var(--bad); }

.attempt-chip {
  display: inline-block;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  font-size: 11px;
  font-weight: 600;
  color: var(--fg-muted);
  white-space: nowrap;
}
.attempt-chip.is-good {
  background: color-mix(in srgb, var(--good) 12%, var(--bg-elev-2));
  border-color: color-mix(in srgb, var(--good) 35%, var(--border));
  color: var(--good);
}
.attempt-chip.is-warn {
  background: color-mix(in srgb, var(--warn) 12%, var(--bg-elev-2));
  border-color: color-mix(in srgb, var(--warn) 35%, var(--border));
  color: var(--warn);
}
.attempt-chip.is-bad {
  background: color-mix(in srgb, var(--bad) 14%, var(--bg-elev-2));
  border-color: var(--bad);
  color: var(--bad);
}
.attempt-chip.is-given {
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-elev-2));
  border-color: var(--accent);
  border-style: dashed;
  color: var(--accent);
}
.attempt-chip.is-more { background: transparent; color: var(--fg-subtle); border-color: var(--border); }
