:root {
  --directional-text: 0.9rem;
  --bg: #D4DDC3;
  --tile: #fbf8f2;
  --ink: #1a1a1a;
  --ink-strong: #000;
  /* Keyboard focus on a button that sits on a light surface. */
  --focus-ring-ink-width: 2px;
  --focus-ring-ink: var(--focus-ring-ink-width) solid var(--ink);
  --focus-ring-ink-offset: 3px;
  /* How far the ring paints outside its element. */
  --focus-ring-ink-reach: calc(var(--focus-ring-ink-width) + var(--focus-ring-ink-offset));
  --hairline: rgba(26, 26, 26, 0.15);
  /* The back-to-portal bar's warm near-white. */
  --portal-bar-bg: #f9f7f2;
  --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%;
  /* The shared page column (shared-ui-patterns.md, "Page column"): Threads'
     width (its .threads-board: 24rem, with the same 16px padding), so
     switching games in the portal doesn't move the content's edges. Was
     360px, which on a typical phone set Forage's content about 12px further
     in on each side, for no recorded reason. The board is a fixed-size grid,
     centred, so it doesn't change. */
  --page-column: 384px;
  max-width: var(--page-column);
  /* Centred on the whole window (100vw counts a desktop scroll bar) rather
     than on the space beside the scroll bar, so the column stays put when the
     page starts or stops scrolling (seen when the portal's notice was
     dismissed). Capped at the space actually there, so a narrow window never
     pushes it under the scroll bar. */
  margin-left: clamp(0px, calc((100vw - var(--page-column)) / 2), calc(100% - var(--page-column)));
  margin-right: 0;
  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%;
  /* 5px on top, not 7px: the line above takes those 2px, so the bar is no
     taller for it. */
  padding: 5px 0 7px;
  /* The bar spans only the space beside a desktop scroll bar, so centring its
     content in the bar would move it when the page starts or stops scrolling.
     Padding the left by the scroll bar's width (100vw counts it; 100% doesn't)
     centres the icon and caption on the whole window, like .app. */
  padding-left: calc(100vw - 100%);
  background: var(--portal-bar-bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 0.95rem;
  text-decoration: none;
  /* The line above sets the near-white bar off from a phone browser's own
     controls. 2px, since 1px was too thin to see. */
  border-top: 2px solid var(--hairline);
  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 {
  /* Unseen beside the box-shadow, but drawn where a contrast theme (forced
     colours) drops box-shadows. */
  outline: 2px solid transparent;
  box-shadow: 0 0 0 2px var(--portal-bar-bg), 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-trigger 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-trigger { 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-trigger {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: transparent;
  color: var(--ink);
  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-trigger:hover {
  border-color: var(--ink-strong);
  color: var(--ink-strong);
}
.instructions-trigger:focus { outline: none; }
.instructions-trigger:focus-visible {
  /* Unseen beside the box-shadow, but drawn where a contrast theme (forced
     colours) drops box-shadows. */
  outline: 2px solid transparent;
  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;
  /* Tall dialogs stop here and scroll inside .modal-body. 100% is the
     backdrop, which is fixed to the visible screen — no 100vh problem. */
  --modal-panel-max-height: calc(100% - 2rem);
  --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;
  /* The focus ring sits just outside the sheet's ✕ glyph. */
  --modal-close-ring-offset: 2px;

  /* The info ✕'s box, and how far down the text must start to clear it. */
  --modal-close-info-inset: 3px;
  --modal-close-info-pad: 6px;
  --modal-close-info-icon: 20px;
  --modal-close-info-clearance: calc(var(--modal-close-info-inset) + 2 * var(--modal-close-info-pad) + var(--modal-close-info-icon) + 4px);

  --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); }
}

/* For a player who asks for less motion, a panel fades in without growing. */
@media (prefers-reduced-motion: reduce) {
  @keyframes modal-panel-in {
    from { opacity: 0; }
  }
}

.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;
  display: flex;
  flex-direction: column;
  max-height: var(--modal-panel-max-height);
  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);
}

/* The content scrolls; the panel doesn't. Deliberately not positioned, so
   the ✕ (absolute, measured from the panel) stays on the panel frame
   instead of scrolling away with the text.
   A scroller clips what paints past its edges, so it pads by a focus ring's
   reach — or the ring on a button at its edge is cut off — and gives the
   room back as a negative margin so the content doesn't move. */
.modal-body {
  overflow-y: auto;
  min-height: 0;
  overscroll-behavior: contain;
  padding: var(--focus-ring-ink-reach);
  margin: calc(-1 * var(--focus-ring-ink-reach));
}

/* 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;
  /* Its text is centred and runs the full width, so it starts below the ✕
     rather than running under it. */
  --modal-panel-pad: var(--modal-close-info-clearance) 32px 28px;
  --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:focus { outline: none; }
/* Full strength: the info ✕ is dimmed at rest, and so would its ring be. */
.modal-close:focus-visible {
  outline: var(--focus-ring-ink);
  outline-offset: var(--modal-close-ring-offset);
  opacity: 1;
}

.modal-close--info {
  --modal-close-inset: var(--modal-close-info-inset);
  --modal-close-pad: var(--modal-close-info-pad);
  --modal-close-color: var(--ink);
  --modal-close-hover-color: var(--ink);
  --modal-close-opacity: 0.6;
  /* Inside the ✕'s box, so the ring reaches no nearer the text than it. */
  --modal-close-ring-offset: calc(-1 * var(--focus-ring-ink-width));
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}

/* Sized here rather than by XIcon's own attributes, from the token the
   panel's clearance above is worked out from. */
.modal-close--info svg {
  width: var(--modal-close-info-icon);
  height: var(--modal-close-info-icon);
}

@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;
}

.confirm-btn:focus { outline: none; }
.confirm-btn:focus-visible {
  outline: var(--focus-ring-ink);
  outline-offset: var(--focus-ring-ink-offset);
}

@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;
}

/* The error screen centres its message in the space below the bar back to
   the portal: a full screen's height of its own would push the page a bar's
   height past the screen, and make it scroll. */
#root:has(> .app-error) {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

.app-error {
  flex: 1;
  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: var(--focus-ring-ink);
  outline-offset: var(--focus-ring-ink-offset);
}

.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: var(--focus-ring-ink);
  outline-offset: var(--focus-ring-ink-offset);
}

.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;
}

/* A notice from the portal (design-for-notices.md, Part 3, Look), between
   "Try today's challenge again" and the cross-promo caption. A card in the
   tiles' off-white on the page's green, so it stands out from the page and
   doesn't read as a fourth game logo. Plain caption-coloured text went
   unnoticed on a phone. */
.notice {
  position: relative;
  width: 100%;
  margin-top: 24px;
  background: var(--tile);
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}

/* Centred like the rest of the screen, with equal margins: the ✕ sits in the
   corner, over the padding, not in a column that would push the text over. */
.notice-text {
  padding: 12px 32px;
  font-family: var(--font-sans);
  font-size: 0.875rem;
  /* Fixed, as Rungs' text-sm has: left to the font, the line holding the ↗
     (drawn from a fallback font with taller metrics) stood further from its
     neighbour than the other lines did. */
  line-height: 1.25rem;
  color: var(--ink);
  text-align: center;
}

/* A small caps line above the text, in the link's colour: a label for whoever
   is previewing, not part of the message. */
.notice-preview {
  padding: 10px 32px 0;
  font-family: var(--font-sans);
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  text-align: center;
}

.notice-preview + .notice-text {
  padding-top: 2px;
}

/* Never breaks inside: the line may break only in the space before it. */
.notice-link {
  font-weight: 500;
  color: var(--accent);
  text-decoration: underline;
  white-space: nowrap;
}

/* Still a 44px target, since it sits near the link, where a slip gives the
   wrong answer; the glyph is drawn in the target's corner so it reads as the
   card's own close control. */
.notice-dismiss {
  position: absolute;
  top: 0;
  right: 0;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  padding: 8px 10px 0 0;
  font-size: 0.875rem;
  color: var(--accent);
  background: none;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
}
.notice-dismiss:hover { color: var(--accent-hover); }
.notice-dismiss:focus { outline: none; }
.notice-dismiss:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

.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: var(--focus-ring-ink);
  outline-offset: var(--focus-ring-ink-offset);
  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. */
