:root {
  --directional-text: 0.9rem;
  --bg: #D4DDC3;
  --tile: #fbf8f2;
  --ink: #1a1a1a;
  --hairline: rgba(26, 26, 26, 0.15);
  --accent: #3d5622;
  --accent-hover: #2d4018;
  --bg-dark: #495f4a;
  /* Colour and font are the only app-specific parts of the toast's styling;
     keeping them in tokens leaves the rule itself free of local names. */
  --toast-bg: var(--bg-dark);
  --toast-fg: var(--tile);
  --toast-font: var(--font-sans);
  --font-serif: "Roboto Serif", "Iowan Old Style", "Palatino Linotype", Palatino, Cambria, Georgia, serif;
  --font-sans: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html, body { overscroll-behavior-y: none; }

body {
  font-family: var(--font-sans);
  background: var(--bg);
}

.app {
  position: relative;
  width: 100%;
  max-width: 360px;
  margin: 0 auto;
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Thin back-link to the portal landing page. See
   back-link-to-portal-design-spec.md for the full visual spec and rationale.
   Rendered outside .app so it spans the full viewport width and sits flush
   at the very top of the page, unconstrained by .app's width cap/padding. */
.back-to-portal-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 100%;
  padding: 6px 0;
  background: var(--tile);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 0.85rem;
  text-decoration: none;
  border-bottom: 1px solid var(--hairline);
}

.back-to-portal-bar svg {
  position: relative;
  top: 0;
}

.back-to-portal-bar:focus { outline: none; }
.back-to-portal-bar:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--tile), 0 0 0 4px var(--ink);
}

/* Masthead — wordmark | score | instructions, all on one row */
.masthead {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: 0.5rem;
}

/* Explicit grid-column, not DOM order: .score-display isn't rendered at
   all until `game` loads (App.tsx), so with it absent, auto-placement
   would put .instructions-button in column 2 instead of 3 — visibly not
   right-aligned until the score display mounted and pushed it over. */
.masthead .wordmark { grid-column: 1; justify-self: start; }
.masthead .score-display { grid-column: 2; justify-self: center; }
.masthead .instructions-button { grid-column: 3; justify-self: end; }

.wordmark {
  font-family: var(--font-serif);
  font-size: 1.1rem;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.wordmark-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  cursor: default;
}
.wordmark-btn:not(:disabled) {
  cursor: pointer;
}
.wordmark-btn:focus { outline: none; }
.wordmark-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ink);
  border-radius: 2px;
}

.wordmark-tm {
  font-size: 0.45em;
  vertical-align: super;
  margin-left: 0.05em;
  font-weight: 400;
  letter-spacing: 0;
}

/* Instructions trigger — small "?" circle in the masthead */
.instructions-button {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid #1f2937;
  background: transparent;
  color: #1f2937;
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.instructions-button:hover {
  border-color: #000;
  color: #000;
}
.instructions-button:focus { outline: none; }
.instructions-button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ink);
}

/* ── Dialogs ───────────────────────────────────────────────────────────────
   Three roles, one vocabulary — see components/dialogRoles.ts, which is what
   decides who wears which. Rungs and Threads carry the same class names and
   the same --modal-* token names, so a look-and-feel change is the same edit
   in each game.

     .modal-panel                    sheet   — instructions
     .modal-panel--confirm           confirm — start over, expand row
     .modal-panel--info              info    — hint, longest word, share

   The base rules only ever read tokens; a role modifier rebinds those tokens
   rather than restating the properties. So retheming a role means editing
   values in one block, and a new property added to the base rule is picked up
   by every role at once instead of needing to be repeated per modifier. */
:root {
  --modal-backdrop-bg: rgba(0, 0, 0, 0.5);
  --modal-backdrop-z: 50;
  --modal-backdrop-pad: 0;
  --modal-backdrop-animation: modal-backdrop-in 0.15s ease-out;

  --modal-panel-bg: #fff;
  --modal-panel-fg: inherit;
  --modal-panel-radius: 0.75rem;
  --modal-panel-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
  --modal-panel-width: 100%;
  --modal-panel-max-width: 24rem;
  --modal-panel-margin: 0 1rem;
  --modal-panel-pad: 1.5rem;
  --modal-panel-animation: modal-panel-in 0.15s ease-out;

  --modal-close-inset: 9px;
  --modal-close-pad: 0;
  --modal-close-color: #6b7280;
  --modal-close-hover-color: #374151;
  --modal-close-opacity: 1;
  --modal-close-hover-opacity: 1;

  --confirm-message-fg: #374151;
  --confirm-message-font-size: 0.95rem;
  --confirm-message-line-height: 1.55;
  --confirm-message-margin-bottom: 1.25rem;
  --confirm-btn-fg: var(--ink);
  --confirm-btn-border: 2px solid var(--ink);
  --confirm-btn-bg: transparent;
  /* Derived from the button's own ink rather than written out, so it keeps
     tracking --ink; the literal it replaced was rgba(26, 26, 26, .08), which
     is exactly today's #1a1a1a at 8%. */
  --confirm-btn-hover-bg: color-mix(in srgb, var(--ink) 8%, transparent);
  --confirm-btn-radius: 6px;
  --confirm-btn-pad: 0.45rem 1.1rem;
  --confirm-btn-font-size: var(--directional-text);
  --confirm-btn-weight: 400;
}

@keyframes modal-backdrop-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes modal-panel-in {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: scale(1); }
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--modal-backdrop-z);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--modal-backdrop-pad);
  background: var(--modal-backdrop-bg);
  animation: var(--modal-backdrop-animation);
}

.modal-panel {
  position: relative;
  background: var(--modal-panel-bg);
  color: var(--modal-panel-fg);
  border-radius: var(--modal-panel-radius);
  box-shadow: var(--modal-panel-shadow);
  max-width: var(--modal-panel-max-width);
  width: var(--modal-panel-width);
  margin: var(--modal-panel-margin);
  padding: var(--modal-panel-pad);
  font-family: var(--font-sans);
  font-size: var(--directional-text);
  animation: var(--modal-panel-animation);
}

/* confirm — a question and two buttons, narrower than a sheet */
.modal-panel--confirm {
  --modal-panel-max-width: 18rem;
}

/* info — a short statement that hugs its content, on a lighter backdrop */
.modal-backdrop--info {
  --modal-backdrop-bg: rgba(0, 0, 0, 0.4);
  --modal-backdrop-z: 100;
  --modal-backdrop-pad: 16px;
  --modal-backdrop-animation: none;
}

.modal-panel--info {
  --modal-panel-bg: var(--tile);
  --modal-panel-fg: var(--ink);
  --modal-panel-radius: 8px;
  --modal-panel-shadow: none;
  --modal-panel-width: fit-content;
  --modal-panel-max-width: none;
  --modal-panel-margin: 0;
  --modal-panel-pad: 28px 32px;
  --modal-panel-animation: none;
}

.modal-close {
  position: absolute;
  top: var(--modal-close-inset);
  right: var(--modal-close-inset);
  background: transparent;
  border: 0;
  padding: var(--modal-close-pad);
  color: var(--modal-close-color);
  opacity: var(--modal-close-opacity);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.15s ease, opacity 0.15s ease;
}

.modal-close--info {
  --modal-close-inset: 3px;
  --modal-close-pad: 6px;
  --modal-close-color: var(--ink);
  --modal-close-hover-color: var(--ink);
  --modal-close-opacity: 0.6;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
  .modal-close:hover {
    color: var(--modal-close-hover-color);
    opacity: var(--modal-close-hover-opacity);
  }
  .modal-close--info:hover {
    --modal-close-hover-opacity: 1;
  }
}

.confirm-message {
  font-family: var(--font-sans);
  font-size: var(--confirm-message-font-size);
  color: var(--confirm-message-fg);
  line-height: var(--confirm-message-line-height);
  margin-bottom: var(--confirm-message-margin-bottom);
  text-align: center;
}

.confirm-buttons {
  display: flex;
  gap: 0.625rem;
  justify-content: center;
}

.confirm-btn {
  padding: var(--confirm-btn-pad);
  border-radius: var(--confirm-btn-radius);
  font-family: var(--font-sans);
  font-size: var(--confirm-btn-font-size);
  font-weight: var(--confirm-btn-weight);
  cursor: pointer;
  border: var(--confirm-btn-border);
  background: var(--confirm-btn-bg);
  color: var(--confirm-btn-fg);
  transition: background 0.15s;
}

@media (hover: hover) {
  .confirm-btn:hover {
    background: var(--confirm-btn-hover-bg);
  }
}

.instructions-title {
  font-size: 1.125rem;
  font-weight: 600;
  color: #111827;
  margin-bottom: 0.75rem;
}
.instructions-title strong {
  font-weight: 600;
}
.instructions-tm {
  font-weight: 400;
}

.instructions-body {
  font-size: var(--directional-text);
  color: #374151;
  line-height: 1.625;
}
.instructions-body p + p,
.instructions-body ul,
.instructions-body ol {
  margin-top: 0.5rem;
}
.instructions-body ul,
.instructions-body ol {
  padding-left: 1.25rem;
}
.instructions-body ul { list-style: disc; }
.instructions-body ol { list-style: decimal; }

.instructions-body .a11y-notes {
  margin-top: 0.75rem;
}
.instructions-body .a11y-notes summary {
  cursor: pointer;
  font-weight: 600;
}
.instructions-body .a11y-notes dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.25rem 0.75rem;
  margin: 0.5rem 0 0;
}
.instructions-body .a11y-notes dt {
  font-weight: 600;
}
.instructions-body .a11y-notes dd {
  margin: 0;
}
.instructions-body .a11y-notes kbd {
  font: inherit;
  font-size: 0.8em;
  padding: 0 0.3em;
  border: 1px solid currentColor;
  border-radius: 3px;
  white-space: nowrap;
}

.instructions-footer {
  margin-top: 1rem;
  font-size: 0.75rem;
  color: #6b7280;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.instructions-feedback {
  color: #2563eb;
  text-decoration: underline;
}

.instructions-feedback:hover {
  color: #1e40af;
}

.loading {
  font-family: var(--font-sans);
  font-size: 1rem;
  font-style: italic;
  color: #666;
  text-align: center;
}

.app-error {
  min-height: 100vh;
  justify-content: center;
}

.load-error {
  text-align: center;
  padding: 0 16px;
}

.play-again {
  display: block;
  margin: 24px auto 0;
  padding: 10px 24px;
  font-family: var(--font-sans);
  font-size: 1rem;
  font-weight: 500;
  color: #fff;
  background: var(--bg-dark);
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s ease;
}
.play-again:hover {
  background: var(--accent-hover);
}
.play-again:focus { outline: none; }
.play-again:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.share-button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin: 20px auto 0;
  padding: 8px 18px;
  font-family: var(--font-sans);
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--ink);
  background: transparent;
  border: 1.5px solid var(--ink);
  border-radius: 8px;
  cursor: pointer;
  opacity: 0.85;
  transition: opacity 0.15s ease;
}
.share-button:hover { opacity: 1; }
.share-button:focus { outline: none; }
.share-button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.share-modal-text {
  display: block;
  width: 240px;
  max-width: 100%;
  padding: 8px 10px;
  font-family: var(--font-sans);
  font-size: 0.85rem;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--hairline);
  border-radius: 6px;
}

/* A transient status message, not a modal (see spec: doesn't need focus-trap
   machinery). Fades itself out; the leaving class drives the fade.
   Positioned absolutely within .app, whose box already runs exactly from the
   wordmark's top to the cross-promo row's bottom — the content area the
   toast is meant to center within — so no separate measurement is needed. */
@keyframes toast-fadein {
  from { opacity: 0; transform: translate(-50%, -50%) translateY(-10px); }
  to   { opacity: 1; transform: translate(-50%, -50%) translateY(0); }
}

.toast {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 200;
  background: var(--toast-bg);
  color: var(--toast-fg);
  padding: 0.625rem 1.25rem;
  border-radius: 0.5rem;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2);
  font-family: var(--toast-font);
  font-size: 0.9rem;
  font-weight: 500;
  white-space: nowrap;
  pointer-events: none;
  opacity: 1;
  animation: toast-fadein 0.3s ease-out;
  transition: opacity 0.5s ease;
}

.toast--leaving {
  opacity: 0;
  pointer-events: none;
}

/* Auto-expand / auto-end notice */
.auto-notice {
  text-align: center;
  font-family: var(--font-sans);
  font-style: italic;
  font-size: 0.95rem;
  color: var(--accent);
  padding: 4px 8px;
}
.auto-notice.end {
  margin-top: 4px;
}

.percentile-notice {
  text-align: center;
  font-family: var(--font-sans);
  font-size: 0.95rem;
  color: var(--accent);
  padding: 0 8px;
  margin-top: 1rem;
}
.percentile-notice strong {
  font-size: calc(1em + 1pt);
}

/* Score */
.score-display {
  font-family: var(--font-sans);
  font-size: 2rem;
  font-weight: 400;
  text-align: center;
  color: var(--ink);
  letter-spacing: 0.03em;
  transform-origin: center;
  transition: transform 0.5s ease;
}

/* Game-over transition — playing UI fades, then unmounts; score grows. */
.app.ended .grid,
.app.ended .grid-meta-strip,
.app.ended .assembly-area,
.app.ended .controls {
  opacity: 0;
  transition: opacity 0.35s ease;
  pointer-events: none;
}

/* Score grows once the playing UI has unmounted, in sync with the FLIP into
   the column layout.

   Both halves of this are transforms rather than the properties you'd reach
   for first (font-size to grow, padding-top to nudge) for the same reason:
   transforms don't lay out, so the masthead's height stays fixed. Anything
   that grew the masthead for real would push the FoundWords list down
   mid-transition, after the FLIP had already captured where it was heading.

   No masthead alignment change is needed to go with it — the score is already
   the row's tallest item (2rem against the wordmark's 1.1rem), so it fills the
   row on its own, and touching align-items would shift the wordmark and
   instructions button too.

   Scaling up makes the score overhang the masthead row it's laid out in, so
   its real neighbours become the portal bar above and whatever `main` starts
   with below — and the row sits close enough to the bar that centring the
   score in it leaves the two crowded equally. The offset buys clearance from
   the bar; `main`'s margin below gives back what the offset takes from the
   other side, so the score ends up with room on both sides rather than
   trading one squeeze for the other. It was 24px once, with 16px beneath,
   which read as the score floating detached below the masthead and cost the
   screen 40px it doesn't have. Vertical room here is scarce enough to be worth
   measuring after any change to it — see tools/measure-endscreen.mjs. */
.app.ended.ui-gone .score-display {
  transform: translateY(14px) scale(1.5);
}
.app.ended.ui-gone main {
  margin-top: 8px;
}

.cross-promo-caption {
  width: 100%;
  margin-top: 24px;
  font-family: var(--font-sans);
  font-size: 0.875rem;
  color: var(--accent);
  text-align: center;
}

/* Each link is only as wide as it needs to be, not the row's full width — so
   a short tagline wraps within that narrower box and stays centered under
   its icon. One link's width matches its icon's width exactly, so it sits
   flush with the row's outer edge; the other link is wider than its icon
   (see its own comment below), so it sits a bit inset from the edge.
   `justify-content: space-between` anchors both to the row's outer edges
   rather than centering them within the full row width. */
.cross-promo-row {
  width: 100%;
  display: flex;
  justify-content: space-between;
  margin-top: 12px;
}

.cross-promo-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-align: center;
  text-decoration: none;
}
.cross-promo-link:focus { outline: none; }
.cross-promo-link:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}

/* One wordmark is a single row of tiles; the other is a staircase of the
   same tile unit across three rows. Sizing both icons to the same height
   would make the single-row icon's tiles dwarf the staircase icon's, so
   instead each is sized so the staircase icon's middle "U N G" row of tiles
   (278/908 of its image height) renders at the same height as the
   single-row icon's tile row (278/317 of its image height) — chosen at a
   32px height for the single-row icon, which is why the staircase icon ends
   up taller overall (92px) despite matching tile size.
   The single-row icon has no such middle row — its one row of tiles sits
   near the very top of its image (starting ~2px in) — so top-aligning both
   links (the row's default behavior) leaves the single-row icon's tiles
   level with the staircase icon's *top* tile, not its "U N G" row. Nudging
   the single-row link down by the "U N G" row's own offset (~30px, minus
   the single-row icon's ~2px of built-in top padding) brings the two tile
   rows level. */
.cross-promo-link--threads { width: 164px; margin-top: 30px; }
/* 140px (not the 72px icon width) so "Find the 5 words that form a ladder"
   wraps across ~2 lines instead of 4 — the icon, narrower than this box,
   centers within it via .cross-promo-link's align-items: center, same as the
   tagline below it. The pair sits a bit inset from the row's left edge as a
   result, but reads as one centered unit instead of the icon hugging the
   edge while the wider tagline sits off-center under it. */
.cross-promo-link--rungs { width: 140px; }

.cross-promo-icon {
  display: block;
}
.cross-promo-icon--threads { width: 164px; height: 32px; }
.cross-promo-icon--rungs { width: 72px; height: 92px; }

.cross-promo-tagline {
  width: 100%;
  margin: 0;
  text-align: center;
  font-family: var(--font-sans);
  font-size: 0.9rem;
  color: var(--accent);
}

/* Grid — fixed height reserves space for the optional 4th row at top */
.grid-wrapper {
  position: relative;
}

.bonus-popup {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--ink);
  color: var(--tile);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 1.4rem;
  padding: 0.6em 1.2em;
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
  pointer-events: none;
  z-index: 10;
  white-space: nowrap;
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, 64px);
  gap: 8px;
  height: 280px; /* 4 × 64px rows + 3 × 8px gaps */
  align-content: end;
  justify-content: center;
  overflow: hidden;
}

button.fragment {
  height: 64px;
  font-family: var(--font-serif);
  font-size: 1.3rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  color: var(--ink);
  border: 1px solid var(--hairline);
  border-radius: 0;
  background: var(--tile);
  cursor: pointer;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
  touch-action: none; /* let pointer drag fire cleanly; browser no longer hijacks as scroll */
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

@media (hover: hover) {
  button.fragment:hover {
    background: #efece6;
  }
}

button.fragment:focus { outline: none; }
button.fragment:focus-visible {
  outline: 1.5px solid rgba(26, 26, 26, 0.55);
  outline-offset: -3px;
}

/* Lift-and-place: a third distinct state. Default is cream+ink, selected is
   ink+cream, lifted is accent+cream. The fill change persists regardless of
   focus so keyboard users can navigate away and come back. */
button.fragment.lifted {
  background: var(--accent);
  color: var(--tile);
  border-color: var(--accent);
}
button.fragment.lifted:focus-visible {
  outline: 1.5px solid rgba(251, 248, 242, 0.7);
  outline-offset: -3px;
}
/* Selected + lifted: keep the ink fill (so assembly membership stays legible)
   but add a thick accent ring around it to mark the lift. */
button.fragment.selected.lifted,
button.fragment.selected.lifted:focus-visible {
  background: var(--ink);
  color: var(--tile);
  border-color: var(--ink);
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.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;
}
/* Selected fill is dark (--bg-dark), so the default ink-toned ring from
   button.fragment:focus-visible would nearly vanish against it. Same
   treatment as .lifted:focus-visible: a light ring keyed to --tile instead. */
button.fragment.selected:focus-visible {
  outline: 1.5px solid rgba(251, 248, 242, 0.7);
  outline-offset: -3px;
}

button.fragment.selected {
  background: var(--bg-dark);
  color: var(--tile);
  border-color: var(--bg-dark);
}

.fragment.empty {
  height: 64px;
}

/* Grid-meta strip — dots cluster + hint button, centered as a tight pair */
.grid-meta-strip {
  display: grid;
  grid-template-columns: repeat(3, 64px);
  gap: 8px;
  justify-content: center;
  align-items: center;
  margin-top: 8px;
}

.bullseye-button { justify-self: end; }
.words-left-indicator { justify-self: center; }
.hint-button { justify-self: start; }

/* Reserve indicator — quiet row of dots beneath the grid */
.words-left-indicator {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  justify-content: center;
  align-items: center;
  min-height: 8px;
  font-family: var(--font-sans);
  font-size: var(--directional-text);
}

/* Shared styling for quiet icon buttons (hint, bullseye) */
.icon-button {
  background: none;
  border: none;
  cursor: pointer;
  padding: 2px;
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.55;
  transition: opacity 0.15s;
  -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
  .icon-button:hover { opacity: 1; }
}

.hint-glyph {
  font-family: var(--font-sans);
  font-size: var(--directional-text);
  font-weight: 700;
}

.hint-content {
  text-align: center;
}


.words-left-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink);
  opacity: 0.25;
}

/* Assembly area — holds the assembled word and its score preview */
.assembly-area {
  min-height: 56px;
  padding: 8px 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: max-content;
  min-width: 208px; /* matches grid width: 3 × 64px + 2 × 8px gap */
  max-width: 100%;
  margin: 0 auto;
}

.word-display {
  font-family: var(--font-serif);
  font-size: 1.6rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  color: var(--ink);
  line-height: 1.8rem;
  min-height: 1.8rem;
  text-align: center;
}

button.word-display {
  display: block;
  width: 100%;
  border: none;
  padding: 0;
  background: transparent;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.15s;
}

button.word-display.valid {
  color: var(--accent);
}

@media (hover: hover) {
  button.word-display.valid:hover {
    color: var(--accent-hover);
  }
}

.word-score-preview {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--directional-text);
  text-align: center;
  color: var(--ink);
  line-height: 1.4rem;
  min-height: 1.4rem;
}

/* Found-words row — accrues during play, below the assembly area.
   Single horizontal line; overflow scrolls. align-self lets the row shrink
   to its content (so a few words sit centred), max-width caps it at the
   parent so overflow can scroll; the edge-mask hints at scrollability.
   justify-content uses the `safe` keyword so that once the row overflows,
   it falls back to start-alignment — plain `center` would centre the
   overflow itself, leaving the start of the list (the earliest word)
   unreachable by scrolling in Chrome/WebKit. */
.found-words-row {
  list-style: none;
  display: flex;
  flex-wrap: nowrap;
  justify-content: safe center;
  gap: 6px 22px;
  align-self: center;
  max-width: 100%;
  padding: 0 12px 6px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scrollbar-color: rgba(26, 26, 26, 0.3) transparent;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
  font-family: var(--font-serif);
  font-size: var(--directional-text);
  letter-spacing: 0.03em;
  color: var(--ink);
}
.found-words-row::-webkit-scrollbar { height: 8px; }
.found-words-row::-webkit-scrollbar-track { background: transparent; }
.found-words-row::-webkit-scrollbar-thumb {
  background: rgba(26, 26, 26, 0.3);
  border-radius: 4px;
}

.found-words-row li {
  position: relative;
  user-select: none;
  -webkit-user-select: none;
}

/* Middle-dot separator sits absolutely in the column gap so its presence
   doesn't change the item's width — otherwise toggling .row-start (below)
   would feed back into flex-wrap and oscillate the layout.
   JS adds .row-start to the first item of each wrapped row to suppress
   a leading dot there. */
.found-words-row li:not(:first-child)::before {
  content: '\00B7';
  position: absolute;
  left: -11px;
  top: 50%;
  transform: translate(-50%, -50%);
  color: var(--ink);
  font-weight: 700;
}

.found-words-row li.row-start::before {
  content: none;
}

.found-words-row.column {
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 0;
  max-width: none;
  overflow-x: visible;
  overflow-y: visible;
  -webkit-mask-image: none;
  mask-image: none;
}

.found-words-row.column li::before {
  content: none;
}

.found-words-heading {
  font-family: var(--font-sans);
  font-size: 1rem;
  text-align: center;
  color: var(--ink);
  margin: 20px 0 6px;
}

/* Expand-row confirm modal */
/* Controls */
.controls {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 16px;
  margin-bottom: 16px;
}

.controls-secondary {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: center;
}

.controls-secondary > button {
  flex: 0 0 64px;
}

.controls-stack {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 0 0 64px;
}

.controls button {
  padding: 10px 8px;
  font-family: var(--font-sans);
  font-size: var(--directional-text);
  font-weight: 400;
  border: 2px solid var(--ink);
  border-radius: 8px;
  background: var(--ink);
  color: var(--tile);
  cursor: pointer;
  transition: opacity 0.15s, background 0.15s;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
}

.controls button:disabled {
  opacity: 0.3;
  cursor: not-allowed;
  pointer-events: none;
}

.controls button:focus { outline: none; }
.controls button:focus-visible {
  outline: 1.5px solid rgba(251, 248, 242, 0.7);
  outline-offset: -5px;
}

@media (hover: hover) {
  .controls button:not(:disabled):hover {
    background: #333;
  }
}

/* Splash CSS lives inline in index.html's <head>, not here — it must paint
   before this stylesheet is guaranteed to have loaded. See
   design-for-splash-screens.md and src/splash/inline.js. */
