/* ============================================================================
   3D WORD LADDER — page styles
   ----------------------------------------------------------------------------
   Only the play stage is styled here. The site top bar, breadcrumb and the
   fixed right-hand game sidebar are shared chrome and are deliberately left
   alone; the few page-scoped overrides at the end work around shared rules
   that break this game (listed with their measurements).

   The shared newspaper system (unified-3d-game.css + newspaper-theme.css)
   supplies every colour, rule and shadow: ink on paper, hard offset shadows,
   zero border radius. This sheet only arranges it.

   .word-display-overlay / .word-panel / .word-panel-label / .target-word /
   .current-word are shared with /3d/wordscramble, which loads this file too —
   their base rules are kept, and the scramble overrides them by id.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   0. TOKENS
   ------------------------------------------------------------------------ */

/* Message colours are per-theme because the shared pair does not clear AA in
   both directions: white on --news-incorrect measures 4.0:1 (light), and
   paper ink on the dark success green measures under 2:1. They are defined at
   the root on purpose: the recap modal is a sibling of the stage, not a
   descendant, so stage-scoped tokens left the "Perfect climb" banner and the
   streak line with a transparent background in every completion. */
:root {
  --wl3d-danger-bg: #a33a3a;
  --wl3d-danger-ink: #ffffff;
  --wl3d-ok-bg: #2f6b3a;
  --wl3d-ok-ink: #ffffff;
  --wl3d-ok-ink-on-paper: #2f6b3a;
}

html[data-theme='dark'] {
  --wl3d-danger-bg: #ff7a7a;
  --wl3d-danger-ink: #2a1010;
  --wl3d-ok-bg: #5cd68b;
  --wl3d-ok-ink: #0d2416;
  --wl3d-ok-ink-on-paper: #5cd68b;
}

#gameContainer.wl3d-stage {
  --wl3d-chip: 46px;
  --wl3d-inset-x: 10px;
  --wl3d-inset-bottom: 10px;
}

/* ---------------------------------------------------------------------------
   1. CANVAS — pointer ownership, focus and drag cursors
   ------------------------------------------------------------------------ */

#gameContainer.wl3d-stage {
  position: relative;
}

#gameContainer.wl3d-stage #canvas {
  display: block;
  touch-action: none;
  outline: none;
  cursor: grab;
  -webkit-tap-highlight-color: transparent;
}

#gameContainer.wl3d-stage #canvas.is-grabbing {
  cursor: grabbing;
}

#gameContainer.wl3d-stage #canvas.is-dragging-key {
  cursor: copy;
}

#gameContainer.wl3d-stage #canvas:focus-visible {
  outline: 3px solid var(--news-accent);
  outline-offset: -3px;
}

/* ---------------------------------------------------------------------------
   2. SHARED WORD PANELS (also used by /3d/wordscramble)
   ------------------------------------------------------------------------ */

.word-display-overlay {
  position: absolute;
  z-index: 50;
  pointer-events: none;
}

.word-display-overlay > * {
  pointer-events: auto;
}

.word-display-overlay-top {
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
}

.word-display-overlay-bottom {
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
}

.word-panel {
  background: var(--news-paper);
  border: 1px solid var(--news-rule-light);
  border-radius: 0;
  padding: var(--spacing-md) var(--spacing-lg);
  text-align: center;
  box-shadow: var(--shadow-hard);
}

.word-panel-label {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  margin-bottom: 4px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.target-word {
  font-family: var(--news-font-display);
  font-size: 2.5rem;
  font-weight: 900;
  color: var(--news-accent);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.current-word {
  font-family: var(--news-font-display);
  font-size: 2rem;
  font-weight: 700;
  color: var(--color-text-primary);
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

/* ---------------------------------------------------------------------------
   3. TARGET PANEL — target word, its clue, and the climb rail
   ------------------------------------------------------------------------ */

/* The top stack (target card, ladder trail, first-run coach) is a normal-flow
   column: the camera fit measures this element and reserves exactly its
   height, so no part of the board is ever hidden behind it. */
#gameContainer.wl3d-stage .word-display-overlay-top {
  top: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  width: min(520px, calc(100% - 24px));
}

/* The status card is a horizontal strip rather than a tall panel: on a 1440x900
   stage the old stacked card plus the trail and coach reserved 250px of a
   668px canvas (37%), which forced the whole board to shrink to fit beneath it.
   The same information in one row costs ~54px. */
#gameContainer.wl3d-stage .word-display-overlay-top .word-panel {
  width: min(700px, 100%);
}

#gameContainer.wl3d-stage .wl3d-topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 7px 16px 8px;
  text-align: left;
}

.wl3d-topbar-target {
  flex: 0 0 auto;
  text-align: center;
}

.wl3d-topbar-mid {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.wl3d-topbar-stats {
  flex: 0 0 auto;
  text-align: right;
}

#gameContainer.wl3d-stage .wl3d-topbar .word-panel-label {
  margin-bottom: 0;
  text-align: center;
}

#gameContainer.wl3d-stage .wl3d-topbar .target-word {
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  line-height: 1.05;
}

#gameContainer.wl3d-stage .wl3d-topbar .wl3d-rail-meta {
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  margin-top: 0;
  white-space: nowrap;
}

#gameContainer.wl3d-stage .word-panel {
  padding: 9px 18px 11px;
}

@media (max-width: 768px) {
  #gameContainer.wl3d-stage .wl3d-topbar {
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 14px;
  }

  .wl3d-topbar-mid {
    flex: 1 1 100%;
    order: 3;
  }

  #gameContainer.wl3d-stage .wl3d-topbar .wl3d-rail-meta {
    align-items: center;
  }
}

#gameContainer.wl3d-stage .word-panel-label {
  margin-bottom: 2px;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--news-ink-light);
}

#gameContainer.wl3d-stage .target-word {
  font-size: clamp(1.75rem, 3.6vw, 2.5rem);
  line-height: 1.05;
  letter-spacing: 0.18em;
  /* Letter-spacing adds a trailing gap; pull the word back to optical centre. */
  text-indent: 0.18em;
}

.wl3d-clue {
  margin-top: 3px;
  font-family: var(--news-font-body);
  font-size: 0.75rem;
  line-height: 1.3;
  color: var(--news-ink-light);
  text-transform: none;
  letter-spacing: 0;
}

.wl3d-rail {
  margin-top: 8px;
}

.wl3d-rail-track {
  position: relative;
  height: 7px;
  background: var(--news-bg);
  border: 1px solid var(--news-rule-light);
  overflow: hidden;
}

.wl3d-rail-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background: repeating-linear-gradient(
    135deg,
    var(--news-accent) 0 6px,
    color-mix(in srgb, var(--news-accent) 72%, var(--news-paper)) 6px 12px
  );
  transition: width 340ms cubic-bezier(0.22, 1, 0.36, 1);
}

.wl3d-rail-meta {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 5px;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--news-ink-light);
}

.wl3d-rail-meta strong {
  color: var(--news-ink);
  font-variant-numeric: tabular-nums;
}

#wl3d-remain {
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   4. LADDER TRAIL — the text record of the climb
   Sized so it can never cover the board: it is one chip row under the target
   panel, horizontally scrollable on narrow stages.
   ------------------------------------------------------------------------ */

.wl3d-trail {
  width: 100%;
  display: flex;
  flex-wrap: nowrap;
  justify-content: safe center;
  gap: 5px;
  margin: 0;
  padding: 0 2px 2px;
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
  pointer-events: auto;
  z-index: 49;
}

.wl3d-trail::-webkit-scrollbar {
  display: none;
}

.wl3d-trail-chip {
  flex: 0 0 auto;
  padding: 2px 7px;
  font-family: var(--news-font-mono, monospace);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--news-paper);
  background: var(--news-ink);
  border: 1px solid var(--news-ink);
}

.wl3d-trail-chip.is-current {
  color: var(--news-paper);
  background: var(--news-accent);
  border-color: var(--news-accent);
}

.wl3d-trail-chip.is-start::before {
  content: 'start ';
  font-weight: 500;
  opacity: 0.72;
}

.wl3d-trail-chip.is-empty {
  color: var(--news-ink-light);
  background: transparent;
  border: 1px dashed var(--news-rule-light);
}

.wl3d-trail-chip.is-target::before {
  content: 'target ';
  font-weight: 500;
  opacity: 0.72;
}

.wl3d-trail-chip.is-target {
  color: var(--news-paper);
  background: var(--news-accent);
  border-color: var(--news-accent);
}

/* ---------------------------------------------------------------------------
   5. FLOATING "NOW SPELLING" CAPTION
   Projected from the 3D frontier rung every frame so the word you are editing
   is always labelled in text, at any camera angle.
   ------------------------------------------------------------------------ */

.wl3d-now {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 48;
  padding: 3px 10px;
  font-family: var(--news-font-mono, monospace);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--news-paper);
  background: var(--news-ink);
  border: 1px solid var(--news-ink);
  pointer-events: none;
  will-change: transform;
}

.wl3d-now::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -5px;
  width: 8px;
  height: 8px;
  margin-left: -4px;
  background: var(--news-ink);
  transform: rotate(45deg);
}

.wl3d-now.is-below::after {
  top: -5px;
  bottom: auto;
}

/* ---------------------------------------------------------------------------
   6. HUD CHIP ROW — Undo / Hint / Recentre / Sound
   Thumb-zone anchored bottom-left, clear of the shared help FAB on the right.
   ------------------------------------------------------------------------ */

.wl3d-hud {
  position: absolute;
  left: calc(var(--wl3d-inset-x) + 10px);
  bottom: calc(var(--wl3d-inset-bottom) + 8px);
  display: flex;
  gap: 8px;
  z-index: 45;
}

.wl3d-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-width: var(--wl3d-chip);
  height: var(--wl3d-chip);
  padding: 0 14px;
  font-family: var(--news-font-body);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--news-ink);
  background: var(--news-paper);
  border: 2px solid var(--news-ink);
  border-radius: 0;
  box-shadow: var(--shadow-hard);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 120ms ease, background 120ms ease, color 120ms ease;
}

.wl3d-chip svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.wl3d-chip:hover:not(:disabled) {
  background: var(--news-highlight);
}

.wl3d-chip:active:not(:disabled) {
  transform: translate(1px, 1px);
  box-shadow: none;
}

.wl3d-chip:focus-visible {
  outline: 3px solid var(--news-accent);
  outline-offset: 2px;
}

.wl3d-chip:disabled {
  opacity: 0.42;
  cursor: not-allowed;
}

.wl3d-chip.is-muted {
  background: var(--news-active-bg);
  border-color: var(--news-active-bg);
  color: var(--news-paper);
}

.wl3d-chip-label {
  display: none;
}

/* The hint chip's price. The text label only appears from 1400px, but the
   economy only works if the price is readable before the tap — so the badge
   rides along at every size. */
.wl3d-chip-cost {
  position: absolute;
  top: 3px;
  right: 4px;
  font-family: var(--news-font-mono, monospace);
  font-size: 0.5625rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  color: var(--news-ink-light);
  pointer-events: none;
}

@media (min-width: 1400px) {
  #wl3d-hint .wl3d-chip-cost {
    display: none;
  }
}

/* Labelled chips measure 450px wide; on a 1280px stage that reached 5px into
   the letter tray. Labels appear only where there is room for them. */
@media (min-width: 1400px) {
  .wl3d-chip-label {
    display: inline;
  }
}

/* ---------------------------------------------------------------------------
   6b. LETTER KEYPAD — the phone's letter input
   The 3D letter tray is a plane lying almost parallel to the camera's line of
   sight; on a portrait phone it foreshortens to a few-pixel sliver (measured:
   row one rendered 4px tall with the other three rows not visible at all, and
   the pixel at every one of the 26 reported tile centres was the bare floor).
   A plane cannot win that fight from any tilt, so stages where the tray is
   stacked under the ladder get this DOM keypad instead: crisp at any device
   pixel ratio, 44px keys on a 390px phone against the tray's ~4px sliver.
   The module decides which surface is live; this file only draws it.
   ------------------------------------------------------------------------ */

#gameContainer.wl3d-stage {
  /* Key geometry. One place to change it — the JS measures the result rather
     than repeating this arithmetic. Nine columns over three rows because the
     letters are a fixed 26: fewer columns means more rows, and every extra row
     is ~52px the ladder cannot have on a stage this short. */
  --wl3d-kb-tile: 46px;
  --wl3d-kb-gap: 4px;
  --wl3d-kb-pad: 8px;
  /* Keypad material. The mat is ink-tinted paper so the paper keys read as
     separate objects on it, and every key carries a solid-ink shadow the
     height of a key's travel: the newspaper system's hard offset edge, used
     the way a physical key uses its throw. Pressing a key moves it down onto
     that line and erases the shadow. */
  --wl3d-kb-mat: color-mix(in srgb, var(--news-ink) 7%, var(--news-paper));
  --wl3d-kb-key-shadow: 0 3px 0 var(--news-ink);
  /* Vowel keys echo the 3D cubes' ochre vowel tiles (cubeVowel #c58a35 in the
     shared stage palette), washed over the paper face so the keys stay one
     paper set with a hint of the ladder's colour logic. */
  --wl3d-kb-vowel-face: color-mix(in srgb, #c58a35 30%, var(--news-paper));
}

/* While the keys are folded the keypad is just its own frame: the grid keeps
   one column, the 26 keys step aside, and the board takes the band. */
.wl3d-kb.is-folded {
  display: none;
}

body.wl3d-page:not(.has-keypad) .wl3d-kb {
  display: none;
}

.wl3d-kb {
  position: absolute;
  left: var(--wl3d-inset-x);
  right: var(--wl3d-inset-x);
  bottom: var(--wl3d-inset-bottom);
  display: grid;
  grid-template-columns: repeat(9, minmax(0, 1fr));
  gap: var(--wl3d-kb-gap);
  /* The extra right padding is the column held clear for the help button; the
     JS sets it, and it is 0 wherever the button cannot reach the keys. */
  padding: var(--wl3d-kb-pad);
  padding-right: calc(var(--wl3d-kb-pad) + var(--wl3d-kb-fab, 0px));
  background: var(--wl3d-kb-mat);
  border: 2px solid var(--news-ink);
  box-shadow: var(--shadow-hard);
  z-index: 40;
  /* The buttons own the gestures here: without this a tap that drifts a pixel
     scrolls the page or starts an orbit under the keypad. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* `display: grid` above beats the user agent's `[hidden] { display: none }`,
   so the attribute has to be honoured explicitly — same trap as .wl3d-keys. */
.wl3d-kb[hidden] {
  display: none;
}

/* Three rows of nine, eight, nine: the bottom row is inset by half a key, the
   way a real keyboard's bottom row is. Tap-first input still reads left to
   right like the word. */
.wl3d-kb-key:nth-child(9n + 1):nth-last-child(-n + 8) {
  grid-column-start: 2;
}

.wl3d-kb-key {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--wl3d-kb-tile);
  min-width: 0;
  padding: 0;
  font-family: var(--news-font-body);
  font-size: 1rem;
  font-weight: 800;
  line-height: 1;
  color: var(--news-ink);
  background: var(--news-paper);
  border: 2px solid var(--news-ink);
  border-radius: 0;
  box-shadow: var(--wl3d-kb-key-shadow);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 90ms ease, background 90ms ease, box-shadow 90ms ease;
}

.wl3d-kb-key.is-vowel {
  background: var(--wl3d-kb-vowel-face);
}

.wl3d-kb-key:active:not(:disabled) {
  transform: translateY(3px);
  box-shadow: none;
  background: var(--news-highlight);
}

.wl3d-kb-key:focus-visible {
  outline: 3px solid var(--news-accent);
  outline-offset: -5px;
}

/* A letter already tried at this position. Mirrors the strike the 3D tray
   draws into its tile texture; its travel shadow fades with the key. */
.wl3d-kb-key.is-struck {
  color: var(--news-ink-light);
  background: color-mix(in srgb, var(--news-ink) 10%, var(--news-paper));
  box-shadow: 0 3px 0 color-mix(in srgb, var(--news-ink) 35%, transparent);
  text-decoration: line-through;
  text-decoration-thickness: 2px;
}

/* The letter the paid hint is pointing at. */
.wl3d-kb-key.is-hinted {
  background: var(--news-active-bg);
  border-color: var(--news-active-bg);
  color: var(--news-paper);
}

.wl3d-kb-key:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ---- the chip row, folded for the keypad layout -------------------------
   The chip row and the keypad both want the bottom band, and the keys span the
   full width: at 390px the chips sat squarely on the S-X keys and at 820px on
   the S-V keys, making five letters untappable. Undo and Hint move to a shallow
   strip directly above the keypad — the mobile sidebar is a 50vh sheet whose
   Controls card scrolls out of reach, so they have no other home on a phone —
   and Recentre and Sound fold into the More chip, which is keypad-only. The
   hint price badge goes with them: it widened that chip from 42px to 76px,
   which is what wrapped a 236px row into a 130px block that stood on the
   ladder. The price is still stated in the status line when a hint is bought.
   The stage id is named because the base `.wl3d-hud` rule carries left/bottom
   and has to be out-specified. */

body.wl3d-page.has-keypad #gameContainer.wl3d-stage .wl3d-hud {
  left: calc(var(--wl3d-inset-x) + 10px);
  right: calc(var(--wl3d-inset-x) + 10px);
  bottom: calc(var(--wl3d-inset-bottom) + var(--wl3d-kb-height, 0px) + 6px);
  justify-content: center;
  gap: 6px;
  flex-wrap: nowrap;
}

body.wl3d-page.has-keypad #gameContainer.wl3d-stage .wl3d-chip {
  min-width: 38px;
  height: 38px;
  padding: 0 10px;
}

body.wl3d-page.has-keypad #gameContainer.wl3d-stage .wl3d-chip-label,
body.wl3d-page.has-keypad #gameContainer.wl3d-stage .wl3d-chip-cost {
  display: none;
}

body.wl3d-page.has-keypad #gameContainer.wl3d-stage #wl3d-reset-view,
body.wl3d-page.has-keypad #gameContainer.wl3d-stage #wl3d-sound {
  display: none;
}

/* The fold is keypad-only: everywhere else the full row is already visible. */
.wl3d-more-menu {
  display: none;
}

body.wl3d-page.has-keypad #gameContainer.wl3d-stage .wl3d-more-menu {
  position: absolute;
  left: calc(var(--wl3d-inset-x) + 10px);
  right: calc(var(--wl3d-inset-x) + 10px);
  bottom: calc(var(--wl3d-inset-bottom) + var(--wl3d-kb-height, 0px) + 6px);
  display: flex;
  justify-content: center;
  gap: 6px;
  z-index: 46;
}

body.wl3d-page.has-keypad #gameContainer.wl3d-stage .wl3d-more-menu[hidden] {
  display: none;
}

body.wl3d-page.has-keypad #gameContainer.wl3d-stage #wl3d-more[aria-expanded='true'] {
  background: var(--news-active-bg);
  border-color: var(--news-active-bg);
  color: var(--news-paper);
}

body.wl3d-page:not(.has-keypad) #gameContainer.wl3d-stage #wl3d-more {
  display: none;
}

/* The shared help FAB (68px, fixed 28px from the viewport's bottom-right) sat
   ON the keypad's bottom-right keys: measured at 390px it covered Y and Z
   outright. On this page, and only while the keypad is up, it shrinks and tucks
   against the stage's bottom-right, with --wl3d-kb-fab holding its column open.
   Help itself is unaffected — the shared menu still carries it. */
@media (max-width: 768px) {
  body.wl3d-page.has-keypad .help-toggle {
    width: 56px;
    height: 56px;
    right: 10px;
    bottom: calc(var(--wl3d-inset-bottom, 10px) + 4px);
    font-size: 24px;
  }
}

/* A phone whose nine columns would fall under the 24px minimum target once the
   help button's column is reserved: tighten the gaps and the key padding rather
   than the keys. Measured at 390px, this lands the keys back on 24px. */
body.wl3d-page.is-kb-tight .wl3d-kb {
  --wl3d-kb-gap: 2px;
  --wl3d-kb-pad: 5px;
}

/* A 256px-wide stage is the floor: the keypad, the chip row and the ladder are
   all competing for 386px of height and the ladder loses (measured: it ran 10px
   under the keypad). The keypad gives up 4px of key rather than the board
   giving up its word — 42px keys still clear the 24px minimum by a wide margin. */
@media (max-width: 330px) {
  #gameContainer.wl3d-stage {
    --wl3d-kb-tile: 42px;
  }

  #gameContainer.wl3d-stage .wl3d-kb-key {
    font-size: 0.875rem;
  }
}

/* A short landscape phone gets a ~145px stage. The keypad keeps all 26 keys
   (they are the only way to type) and gives up key size instead: 34px keys are
   still well clear of the 24px minimum target. */
#gameContainer.wl3d-stage.is-short {
  --wl3d-kb-tile: 34px;
  --wl3d-kb-gap: 3px;
  --wl3d-kb-pad: 6px;
}

/* On a wide stage the keypad is centred and capped rather than stretched: nine
   keys spanning a 700px canvas would put each letter ~74px from the last and
   make the row a scanning exercise. The cap keeps a tablet's keys thumb-sized
   and the rows readable. */
@media (min-width: 560px) {
  .wl3d-kb {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: min(460px, calc(100% - 2 * var(--wl3d-inset-x)));
  }
}

/* ---------------------------------------------------------------------------
   7. KEYBOARD LEGEND — revealed once a letter key has been used
   ------------------------------------------------------------------------ */

.wl3d-keys {
  position: absolute;
  right: calc(var(--wl3d-inset-x) + 10px);
  /* Sits under the reserved top stack: the letter tray owns the bottom-right
     corner, and the legend used to land on the S-Z row. */
  top: calc(var(--wl3d-reserve-top, 170px) + 8px);
  opacity: 1;
  transition: opacity 400ms ease;
  z-index: 44;
  display: grid;
  gap: 3px;
  padding: 8px 12px;
  font-family: var(--news-font-mono, monospace);
  font-size: 0.6875rem;
  line-height: 1.35;
  color: var(--news-ink);
  background: var(--news-paper);
  border: 2px solid var(--news-ink);
  box-shadow: var(--shadow-hard);
  pointer-events: none;
}

/* `display: grid` above beats the user agent's `[hidden] { display: none }`,
   so the attribute has to be honoured explicitly or the panel is always up. */
.wl3d-keys[hidden] {
  display: none;
}

.wl3d-keys kbd {
  display: inline-block;
  min-width: 1.4em;
  padding: 0 3px;
  font: inherit;
  text-align: center;
  color: var(--news-paper);
  background: var(--news-ink);
}

/* ---------------------------------------------------------------------------
   8. FIRST-RUN COACH MARK
   ------------------------------------------------------------------------ */

.wl3d-coach {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 9px 12px;
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--news-ink);
  background: var(--news-paper);
  border: 2px solid var(--news-ink);
  box-shadow: var(--shadow-hard);
  /* The coach is a DOM layer over the canvas: without this it eats the first
     drag. Only the dismiss button is interactive. */
  pointer-events: none;
}

.wl3d-coach[hidden] {
  display: none;
}

.wl3d-coach-text strong {
  font-weight: 800;
}

.wl3d-coach-dismiss {
  flex: 0 0 auto;
  padding: 6px 12px;
  font-family: var(--news-font-body);
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--news-paper);
  background: var(--news-ink);
  border: 2px solid var(--news-ink);
  cursor: pointer;
  pointer-events: auto;
}

.wl3d-coach-dismiss:focus-visible {
  outline: 3px solid var(--news-accent);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   9. TOAST + LIVE REGION
   ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
   9. STATUS LINE + LIVE REGION
   The status line sits inside the top strip. Every floating position for a
   toast collided with something real: the letter tray (bottom right), the word
   being edited (bottom left of the ladder) or the HUD chips; and reserving a
   band for it cost 11% of the board's size.
   ------------------------------------------------------------------------ */

.wl3d-status {
  min-height: 15px;
  font-family: var(--news-font-body);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--news-ink-light);
  transition: color 140ms ease;
}

.wl3d-status.is-error {
  color: var(--news-incorrect);
}

.wl3d-status.is-success {
  color: var(--wl3d-ok-ink-on-paper);
}

.wl3d-status.is-visible {
  animation: wl3d-status-flash 420ms ease-out;
}

@keyframes wl3d-status-flash {
  0% { background: var(--news-highlight); }
  100% { background: transparent; }
}

.wl3d-sr-only {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   10. PARTICLES (projected from the frontier cube)
   ------------------------------------------------------------------------ */

.wl3d-particle {
  position: absolute;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  background: var(--news-accent);
  border-radius: 50%;
  pointer-events: none;
  z-index: 43;
  animation: wl3d-particle 820ms cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}

@keyframes wl3d-particle {
  0% { opacity: 1; transform: translate(0, 0) scale(1); }
  100% { opacity: 0; transform: translate(var(--dx, 0), var(--dy, -40px)) scale(0.4); }
}

/* ---------------------------------------------------------------------------
   11. LOADING VEIL
   ------------------------------------------------------------------------ */

.wl3d-veil {
  position: absolute;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 20px;
  text-align: center;
  background: color-mix(in srgb, var(--news-bg) 88%, transparent);
  backdrop-filter: blur(2px);
}

.wl3d-veil[hidden] {
  display: none;
}

.wl3d-spinner {
  width: 38px;
  height: 38px;
  border: 3px solid var(--news-rule-light);
  border-top-color: var(--news-accent);
  border-radius: 50%;
  animation: wl3d-spin 900ms linear infinite;
}

@keyframes wl3d-spin {
  to { transform: rotate(360deg); }
}

.wl3d-veil-title {
  font-family: var(--news-font-display);
  font-size: 1.25rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--news-ink);
}

.wl3d-veil-copy {
  max-width: 34ch;
  font-size: 0.875rem;
  color: var(--news-ink-light);
}

.wl3d-retry {
  margin-top: 4px;
  padding: 8px 16px;
  font-family: var(--news-font-body);
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--news-paper);
  background: var(--news-accent);
  border: 2px solid var(--news-accent);
  cursor: pointer;
}

/* ---------------------------------------------------------------------------
   12. COMPLETION RECAP
   ------------------------------------------------------------------------ */

#successModal .modal-dialog {
  max-width: 470px;
}

html[data-theme='dark'] #successModal .modal-dialog {
  border: 2px solid var(--news-ink);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 10px 10px 0 rgba(0, 0, 0, 0.55);
}

.wl3d-recap-title {
  font-family: var(--news-font-display);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--news-ink);
  font-size: var(--font-size-2xl);
  margin-bottom: var(--spacing-sm);
}

.wl3d-recap-copy {
  color: var(--news-ink-light);
  margin-bottom: 4px;
}

.wl3d-recap-moves {
  color: var(--news-accent);
  font-weight: 800;
}

.wl3d-recap-ladder {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 5px;
  margin: 10px 0 4px;
}

.wl3d-recap-ladder span {
  padding: 2px 7px;
  font-family: var(--news-font-mono, monospace);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--news-paper);
  background: var(--news-ink);
}

/* The letter that changed on each rung is the move the player made. */
.wl3d-recap-ladder span b {
  font-weight: 800;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 2px;
}

.wl3d-stars {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin: 12px 0 4px;
  font-size: 2.25rem;
  line-height: 1;
}

.wl3d-star {
  color: var(--news-rule-light);
  transform: scale(0.92);
  transition: transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1), color 200ms ease;
}

.wl3d-star.is-earned {
  color: #d8a12a;
  transform: scale(1.06);
}

.wl3d-recap-banner {
  display: inline-block;
  margin: 4px auto 0;
  padding: 5px 14px;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--wl3d-ok-ink);
  background: var(--wl3d-ok-bg);
}

.wl3d-recap-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  margin: 14px 0 10px;
  background: var(--news-rule-light);
  border: 1px solid var(--news-rule-light);
}

/* Five cells read 2 + 2 + 1; the odd trailing cell spans so the last row is
   not a lone half-width box. */
.wl3d-recap-grid .wl3d-recap-cell:last-child:nth-child(odd) {
  grid-column: 1 / -1;
}

.wl3d-recap-cell {
  padding: 9px 12px;
  background: var(--news-paper);
  text-align: left;
}

.wl3d-recap-label {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--news-ink-light);
}

.wl3d-recap-value {
  font-family: var(--news-font-display);
  font-size: 1.25rem;
  font-weight: 900;
  color: var(--news-ink);
  font-variant-numeric: tabular-nums;
}

.wl3d-recap-math {
  margin: 0 0 10px;
  font-family: var(--news-font-mono, monospace);
  font-size: 0.6875rem;
  line-height: 1.5;
  color: var(--news-ink-light);
}

.wl3d-recap-best {
  margin-bottom: 6px;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--news-ink-light);
}

.wl3d-recap-streak {
  margin-bottom: 12px;
  font-size: 0.8125rem;
  font-weight: 700;
  /* On the modal paper, not on a pill: --wl3d-ok-ink is the on-pill ink and
     measures 1.03:1 here. The on-paper variant of the same green is the one
     the status line already uses. */
  color: var(--wl3d-ok-ink-on-paper);
}

.wl3d-recap-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}

/* ---------------------------------------------------------------------------
   13. REDUCED MOTION
   ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .wl3d-particle {
    display: none;
  }

  .wl3d-rail-fill,
  .wl3d-star,
  .wl3d-toast {
    transition: none;
  }

  .wl3d-spinner {
    animation-duration: 2400ms;
  }
}

/* ---------------------------------------------------------------------------
   14. SHORT AND NARROW STAGES
   ------------------------------------------------------------------------ */

/* Two shared behaviours break this page on real devices; both are corrected
   for this page only, leaving the other eight 3D games untouched.
   1. `@media (hover:none) and (pointer:coarse) { .game-sidebar { width:100% } }`
      makes the fixed sidebar cover the whole viewport on any touch device
      wider than the 768px breakpoint.
   2. The framed stage carries `min-height: 300px`, which on a 390px-tall
      landscape phone pushes the chip row and the board below the fold. */

@media (hover: none) and (pointer: coarse) and (min-width: 769px) {
  body.wl3d-page .game-sidebar {
    width: var(--stage-sidebar-w, 300px) !important;
  }
}

@media (max-height: 620px) and (orientation: landscape) {
  /* Above the 768px breakpoint the framed stage caps its width from the
     viewport height and translates itself left of the sidebar, which leaves a
     short landscape phone with a ~230x140 canvas. Taking the stage full-bleed
     for this case (as the sub-768px layout already does) restores the board. */
  body.wl3d-page .game-canvas-container {
    width: auto !important;
    margin: 8px 8px 0 !important;
    translate: none !important;
    height: calc(100dvh - var(--site-header-height, 80px) - 24px) !important;
    min-height: 170px !important;
  }

  body.wl3d-page .game-canvas-container {
    margin-right: calc(var(--stage-sidebar-w, 300px) + 8px) !important;
  }

  .wl3d-keys {
    display: none;
  }

  .wl3d-chip {
    height: 40px;
    min-width: 40px;
    padding: 0 9px;
  }

  #gameContainer.wl3d-stage {
    --wl3d-chip: 40px;
  }

  .wl3d-coach {
    padding: 7px 10px;
    font-size: 0.75rem;
  }

  .wl3d-toast {
    padding: 6px 12px;
    font-size: 0.8125rem;
  }
}

@media (max-width: 768px) {
  /* The collapsed mobile bottom sheet keeps its padding and top rule even at
     max-height 0, leaving a ~12px dead bar across the bottom of the stage. */
  body.wl3d-page .game-sidebar.menu-open,
  body.wl3d-page .game-sidebar:not(.menu-open) {
    padding-top: 0;
  }

  #gameContainer.wl3d-stage .word-display-overlay-top {
    top: 10px;
    width: min(360px, calc(100% - 20px));
  }

  #gameContainer.wl3d-stage .word-panel {
    padding: 7px 14px 9px;
  }

  #gameContainer.wl3d-stage .target-word {
    font-size: clamp(1.5rem, 7vw, 2rem);
  }

  .wl3d-clue {
    font-size: 0.6875rem;
  }

  .wl3d-rail-meta,
  #gameContainer.wl3d-stage .word-panel-label {
    font-size: 0.6875rem;
  }

  .wl3d-now {
    font-size: 0.75rem;
  }

  .wl3d-trail {
    top: calc(100% + 4px);
  }
}

@media (max-width: 420px) {
  #gameContainer.wl3d-stage {
    --wl3d-chip: 44px;
  }

  /* One row of icon-only chips: the labels are already hidden below 1024px,
     and a 2x2 grid grew 100px tall, which the camera fit then had to reserve
     out of the board's height. */
  .wl3d-chip {
    padding: 0 10px;
    justify-content: center;
  }

  .wl3d-coach {
    font-size: 0.75rem;
  }
}

/* ---------------------------------------------------------------------------
   15. SHORT STAGES
   The HUD stack is measured before the camera fit, so every pixel it keeps is
   a pixel the board loses. On a short stage the clue and the text trail are
   dropped (the 3D ladder still shows the route, and the strip still carries
   the target, the rung and the distance left).
   ------------------------------------------------------------------------ */

@media (max-height: 640px) {
  .wl3d-clue,
  .wl3d-trail {
    display: none;
  }

  .wl3d-coach {
    padding: 6px 10px;
    font-size: 0.75rem;
  }

  #gameContainer.wl3d-stage .word-display-overlay-top {
    top: 8px;
  }
}

@media (max-height: 470px) {
  .wl3d-coach {
    display: none;
  }

  #gameContainer.wl3d-stage .wl3d-topbar {
    padding: 5px 12px 6px;
    gap: 10px;
  }

  .wl3d-status {
    font-size: 0.6875rem;
  }
}


/* ---------------------------------------------------------------------------
   17. VERY SHORT STAGES
   Under ~380px of canvas the chip row cannot coexist with the board: on an
   844x390 phone it covered the word tiles entirely, so the row is dropped
   (every chip has a keyboard shortcut, and Hint also lives in the sidebar).
   ------------------------------------------------------------------------ */

#gameContainer.wl3d-stage.is-short .wl3d-hud,
#gameContainer.wl3d-stage.is-short .wl3d-keys {
  display: none;
}

/* Between 240px and 430px of canvas the chip row cannot sit at the bottom
   without either covering the board or costing it ~58px of height (which on a
   320px phone is the difference between 19px and 25px letter tiles). It moves
   into the empty top-right corner instead, and its height joins the reserved
   stack. */
#gameContainer.wl3d-stage.is-compact .wl3d-hud {
  top: calc(var(--wl3d-reserve-top, 150px) + 8px);
  right: calc(var(--wl3d-inset-x) + 8px);
  bottom: auto;
  left: auto;
}

#gameContainer.wl3d-stage.is-compact .wl3d-keys {
  display: none;
}

/* The legend's home is the empty corner under the top stack. It is shown on the
   first letter keypress wherever it fits without covering the board — the
   compact and short stages hide it in JS, keyed off the canvas rather than the
   window, so a portrait tablet with a hardware keyboard keeps it. */

/* The shared mobile clock is a fixed pill at the top-right of the stage. At
   320px it covered the "RUNG n · PAR n" readout (969px^2 of overlap, 5x7px of
   the digit itself), so on narrow stages the strip keeps its right edge clear
   of it. */
@media (max-width: 420px) {
  /* The clock pill sits over the top-right of the stage; dropping the strip
     below it keeps the full width for the target and the readout, which is
     what keeps the card shallow enough for the board to have room. */
  #gameContainer.wl3d-stage .word-display-overlay-top {
    top: 26px;
  }
}

@media (max-width: 380px) {
  .wl3d-clue {
    display: none;
  }

  /* A 320px phone has a 386px canvas: every pixel the strip keeps is a pixel
     the board loses, so the strip compresses to one tight row. */
  #gameContainer.wl3d-stage .wl3d-topbar {
    padding: 5px 10px 6px;
    gap: 8px;
  }

  #gameContainer.wl3d-stage .wl3d-topbar .target-word {
    font-size: 1.35rem;
  }

  #gameContainer.wl3d-stage .wl3d-topbar .word-panel-label {
    font-size: 0.625rem;
  }

  .wl3d-rail-meta,
  .wl3d-status {
    font-size: 0.625rem;
  }

  .wl3d-rail-track {
    height: 6px;
  }

  /* Put the rail beside the rung readout instead of above it: on a 256px-wide
     canvas that row is worth 12px of board height, which is the difference
     between a 23px and a 25px letter tile. */
  #gameContainer.wl3d-stage .wl3d-topbar-mid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 4px 8px;
  }

  #gameContainer.wl3d-stage .wl3d-topbar-mid .wl3d-clue {
    grid-column: 1 / -1;
  }

  #gameContainer.wl3d-stage .wl3d-rail-track {
    grid-column: 1;
  }

  #gameContainer.wl3d-stage .wl3d-status {
    grid-column: 1 / -1;
  }

  .wl3d-trail {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
   16. VERY SHORT STAGES
   On a 390px-tall landscape phone the stage is ~144px and the top stack has to
   fit inside it. The progress rail is the one element that can go: the rung
   readout beside it carries the same numbers.
   ------------------------------------------------------------------------ */

/* Keyed off the CANVAS size (classes the module sets), not the viewport: a
   320x568 phone has a 386px canvas and keeps its rail, while a 256px-wide
   canvas drops it because that 12px is the difference between a 22px and a
   25px letter target. */
#gameContainer.wl3d-stage.is-tiny .wl3d-rail-track,
#gameContainer.wl3d-stage.is-narrow .wl3d-rail-track {
  display: none;
}

#gameContainer.wl3d-stage.is-narrow .wl3d-topbar {
  padding: 4px 10px 5px;
  gap: 6px 10px;
}

#gameContainer.wl3d-stage.is-narrow .wl3d-status {
  min-height: 13px;
  font-size: 0.625rem;
}
