/* Criss Cross — presentation.
   The shell (header, menu, cards, toasts, overlay) is deliberately the same
   vocabulary as Word Sleuth's: a child who has played one word game on this
   site should not have to learn a second set of controls to play the other.

   Cell size is computed in JS. CSS aspect-ratio is Safari 15+ and container
   units are 16+, and both fail silently on the older iPads these run on.

   THE RULE THIS FILE EXISTS TO ENFORCE: a cell has FIVE things to say at once
   — which word you are in, which letter you are on, whether the letter is
   locked, whether Check called it wrong, and what its number is. Colour alone
   cannot carry five states, so each one owns a different channel: the current
   word is a fill, the active cell is a caret plus a bright border, a locked
   letter is a green fill with dark ink, a wrong letter is a red WEDGE in the
   top-right corner, and the number sits in the top-left where nothing else
   ever goes.
*/
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; overflow: hidden; overscroll-behavior: none; }
body {
  background: #0b1220;
  color: #e2e8f0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  height: 100vh;
  height: 100dvh;          /* fallback above: dvh is Safari 15.4+ */
  user-select: none; -webkit-user-select: none; touch-action: manipulation;
}
#app { display: flex; flex-direction: column; height: 100%; }

#hdr {
  flex: 0 0 auto; display: flex; align-items: center; gap: 5px;
  padding: calc(6px + env(safe-area-inset-top)) 7px 6px;
  background: #020617;
}
#hdrTitle {
  flex: 1; min-width: 0; text-align: center; font-weight: 700; font-size: 13px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pill {
  background: #1e293b; color: inherit; border: 0; border-radius: 10px;
  min-width: 36px; height: 36px; font-size: 15px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
}
/* R-C5: the 44px floor, which this game had been under since it shipped.
   Scoped to the header so a .pill used elsewhere keeps its own size. */
#hdr .pill { min-width: 44px; min-height: 44px; height: auto; }
.pill:active { transform: scale(.94); }
.pill[disabled] { opacity: .35; cursor: default; }
/* ---- the hint control (docs/offline-pwa.md R-C16) -------------------------
   ONE bulb, and the count as an OVERLAID badge. Absolutely positioned inside
   `.bulb`, so 10 -> 9 -> 8 can never change the button's width and shove
   whatever sits beside it — which is exactly what the inline count did on a
   phone. The number is always how many are LEFT, it is shown at 1 and at 0,
   and at 0 the button is disabled with the badge still on it, because a
   control that vanishes teaches a child nothing about why.
   The badge is dark with a pale ring at every game, on purpose: the games run
   from near-black to amber to a light theme, and a dark pill with a light halo
   is the one combination that reads on all of them. */
.hintBtn { position: relative; overflow: visible; }
.hintBtn .bulb { position: relative; display: inline-block; line-height: 1; }
.hintBtn .hintN {
  position: absolute; right: -9px; bottom: -6px;
  min-width: 16px; height: 16px; padding: 0 3px; border-radius: 999px;
  background: #0f172a; color: #f8fafc; box-shadow: 0 0 0 1.5px rgba(255, 255, 255, .6);
  font: 800 10px/16px inherit; letter-spacing: 0; pointer-events: none;
}
.hintBtn:disabled .hintN, .hintBtn[disabled] .hintN { background: #64748b; }
/* `wide` = the bulb is followed by a WORD. The badge hangs 9px past the
   bulb, so without this it sits on the first letter of the label. */
.hintBtn.wide .bulb { margin-right: 11px; }

/* The clock. Tabular figures so 1:11 and 0:00 are the same width — a header
   control that changes width every second drags the title around with it. */
#hdrClock {
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
  min-width: 46px; color: #94a3b8;
}
#hdrClock.off { min-width: 36px; color: #64748b; }
/* The per-game badge that used to live here is gone: an ID selector beats
   `.hintBtn .hintN` on specificity, so leaving it would have quietly kept two
   of the sixteen games on their own private version of the shared control —
   which is the exact failure mode the vocabulary audit was about. */

.scr { display: none; flex: 1 1 auto; min-height: 0; flex-direction: column; }
.scr.on { display: flex; }

/* ---------- menu ---------- */
/* R-C15a, applied to a SCREEN rather than an overlay — the defect is the same
   wherever a scroll region also centres. `justify-content: center` on a column
   whose content is taller than the box pushes the first child ABOVE the top
   edge, where no scrollbar can reach it. v1.6 made that reachable: ⚙️ Settings
   opens itself on a custom mix, six sizes wrap to two rows, and the title went
   off the top. Auto margins on the end children centre identically while the
   content fits and collapse to zero once it does not. */
#scr-menu, #scr-how {
  align-items: center; gap: 11px;
  padding: 16px; overflow-y: auto; overscroll-behavior: contain;
}
#scr-menu > :first-child, #scr-how > :first-child { margin-top: auto; }
#scr-menu > :last-child, #scr-how > :last-child { margin-bottom: auto; }
#scr-menu h1 { font-size: 30px; }
.sub { color: #94a3b8; font-size: 14px; text-align: center; max-width: 320px; line-height: 1.45; }
#tierRow { display: flex; gap: 8px; width: 100%; max-width: 380px; }
.tierCard {
  flex: 1 1 0; min-width: 0;
  background: #1e293b; color: inherit; border: 2px solid transparent;
  border-radius: 14px; padding: 10px 4px; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  font-weight: 700; font-size: 13px;
}
.tierCard .te { font-size: 22px; }
.tierCard small { color: #94a3b8; font-weight: 500; font-size: 11px; }
.tierCard.sel { border-color: #38bdf8; background: #0c4a6e; }
.big {
  width: 100%; max-width: 380px;
  background: #1e293b; color: inherit; border: 0; border-radius: 14px;
  padding: 14px; font-size: 16px; font-weight: 700; cursor: pointer;
}
.big.primary { background: #38bdf8; color: #042f49; }
.big:active { transform: translateY(2px); }
#statsLine { color: #94a3b8; font-size: 13px; text-align: center; min-height: 18px; }

/* ---------- how to play ---------- */
.card {
  background: #1e293b; border-radius: 14px; padding: 12px 14px;
  width: 100%; max-width: 380px; font-size: 14px; line-height: 1.5;
}
.card h2 { font-size: 15px; margin-bottom: 6px; }

/* ---------- the grid ---------- */
/* `margin: auto` on the child rather than `align-items/justify-content: center`
   on the parent, and it is not a style preference. A centred flex item that is
   BIGGER than its box overflows equally in both directions, and the overflow
   above the top edge is unreachable — no scrollbar can get to it. `margin:auto`
   centres while there is room and pins to the start when there is not, so the
   scroll below can actually reach the whole grid.

   The scroll is a NET, not a feature: sizeGrid floors the cell at 18px, and on
   a 320px-wide handset a 17x17 wants 338 and cannot have it. Before v1.6 that
   was silently clipped by `overflow:hidden` on body, and a clipped square is
   one a child can never fill in. */
#gridWrap {
  flex: 1 1 auto; min-height: 0; min-width: 0;
  display: flex; overflow: auto; overscroll-behavior: contain; padding: 5px;
}
#grid { display: flex; flex-direction: column; gap: 2px; margin: auto; }
.grow { display: flex; gap: 2px; }

/* A block is the paper the puzzle is printed on, not a cell you failed to
   fill. It gets no border at all, so it can never be mistaken for an empty
   square waiting for a letter -- the same distinction word-sleuth got wrong
   between "not in the word" and "untouched". */
.cell {
  position: relative; flex: 0 0 auto; border-radius: 3px;
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; text-transform: uppercase; line-height: 1;
  background: transparent;
}
.cell.open {
  background: #f1f5f9; color: #0f172a;
  box-shadow: inset 0 0 0 1px #94a3b8;
}
/* The number lives in the top-left and NOTHING else is ever drawn there.
   Flock shipped three fixes for "blurry" gate numbers that turned out to be a
   badge drawn on top of the numeral; a legibility complaint is a layout
   suspect first. */
.cell .no {
  position: absolute; top: 1px; left: 2px;
  /* `max(7px, …)`: at 17x17 the cell floors at 18px and .32em is a 5.8px
     numeral, which is a smudge rather than a number. The em keeps it
     proportional on the big cells and the 7px keeps it readable on the small
     ones. */
  font-size: max(7px, .32em); font-weight: 700; line-height: 1; color: #475569;
  /* <i> for brevity in the markup, but a clue number is not emphasis and an
     italic 4 at 13px is measurably harder to read than an upright one. */
  font-style: normal;
  pointer-events: none;
}
.cell .ch { font-size: .62em; }

/* the word you are working on, and the letter you are on within it */
.cell.inword { background: #bae6fd; }
.cell.active { background: #38bdf8; box-shadow: inset 0 0 0 3px #0284c7; }
.cell.active .no { color: #0c4a6e; }
/* The caret is the non-colour channel for "the next letter lands HERE". */
.cell.active.empty::after {
  content: ''; position: absolute; left: 50%; top: 24%;
  width: 2px; height: 46%; margin-left: -1px;
  background: #0f172a; border-radius: 1px;
  animation: blink 1.05s steps(1) infinite;
}
@keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

/* Locked: given at the start, revealed by a hint, or (on Sprout and Scout)
   a word the player spelled correctly. Fill AND ink both change, so the state
   survives a colour-blind reading. */
.cell.lock { background: #86efac; color: #052e16; box-shadow: inset 0 0 0 1px #16a34a; }
.cell.lock.inword { background: #6ee7b7; }
.cell.lock.active { background: #34d399; box-shadow: inset 0 0 0 3px #047857; }
.cell.lock .no { color: #14532d; }

/* Check said this letter is wrong. A WEDGE in the top-right corner, not a
   colour: the corner is empty on every other state, so the mark is legible
   without being able to tell red from green. It is deliberately silent about
   what the right letter is. */
.cell.wrong::before {
  content: ''; position: absolute; top: 0; right: 0;
  border-style: solid; border-width: 0 .38em .38em 0;
  border-color: transparent #dc2626 transparent transparent;
}
.cell.wrong { color: #7f1d1d; }

.cell.pop { animation: pop .12s; }
@keyframes pop { 50% { transform: scale(1.12); } }
.cell.done { animation: hop .5s ease; }
@keyframes hop {
  0%, 100% { transform: translateY(0); }
  40% { transform: translateY(-9px); }
  70% { transform: translateY(0); }
}

/* ---------- the clue bar ----------
   Permanently on the play screen. R18: a clue a child has to open a list to
   read is one they stop reading. */
#clueBar {
  flex: 0 0 auto; display: flex; align-items: stretch; gap: 5px;
  padding: 0 6px 5px;
}
#clueBody {
  flex: 1 1 auto; min-width: 0;
  background: #1e293b; border-radius: 11px; padding: 6px 10px;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
}
#clueTag { font-size: 11px; font-weight: 800; color: #38bdf8; letter-spacing: .4px; }
/* TWO LINES ALWAYS — a fixed height, not a min-height. Clamping to two lines
   still lets a one-line clue render 11px shorter, and stepping through the
   clues then walks the whole grid up and down under a finger that is already
   aiming at a square. Same family as the status-shift rule in the playbook:
   nothing the finger is aiming at may move between taps.
   Two lines is enough with margin, measured rather than assumed: at 320px the
   box is 210px wide and 0 of the 684 clues wrap past two lines (longest is 60
   characters). The clamp is the backstop, not the plan. */
#clueText {
  font-size: 13px; line-height: 1.32; color: #e2e8f0;
  height: 2.64em; flex: 0 0 auto;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.nav {
  flex: 0 0 auto; width: 34px; border: 0; border-radius: 11px;
  background: #1e293b; color: #cbd5e1; font-size: 22px; font-weight: 700;
  cursor: pointer; line-height: 1;
}
.nav:active { transform: scale(.94); }

/* ---------- tools ---------- */
#tools { flex: 0 0 auto; display: flex; gap: 6px; padding: 0 6px 5px; }
.tool {
  flex: 1 1 0; min-width: 0; height: 34px;
  background: #334155; color: #e2e8f0; border: 0; border-radius: 9px;
  font-size: 13px; font-weight: 700; cursor: pointer;
}
.tool:active { transform: translateY(2px); }

/* ---------- keyboard ---------- */
#kb {
  flex: 0 0 auto;
  display: flex; flex-direction: column; gap: 5px;
  padding: 0 4px calc(6px + env(safe-area-inset-bottom));
}
.kbrow { display: flex; gap: 4px; justify-content: center; }
/* flex: 1 1 0 with min-width: 0 is what keeps ten keys on ONE row at 320px —
   fixed-width keys wrap, which is the bug this rule exists to prevent. */
.key {
  flex: 1 1 0; min-width: 0; height: 44px;
  background: #475569; color: #f1f5f9; border: 0; border-radius: 7px;
  font-size: 15px; font-weight: 700; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.key.wide { flex: 1.7 1 0; font-size: 16px; }
.key:active { transform: translateY(2px); }

/* ---------- overlays ---------- */
#scr-play { position: relative; z-index: 0; }
.ovl {
  position: absolute; inset: 0; z-index: 30;
  display: none; align-items: center; justify-content: center; padding: 16px;
  background: rgba(11, 18, 32, .95);
}
.ovl.on { display: flex; }
.ovlCard {
  background: #1e293b; border-radius: 18px; padding: 18px;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  width: 100%; max-width: 340px; text-align: center;
}
.ovlCard.wide { max-width: 420px; max-height: 100%; }
#ovlTitle { font-size: 24px; font-weight: 800; }
#ovlSub { color: #94a3b8; font-size: 14px; line-height: 1.45; }
#listTitle { font-size: 17px; font-weight: 800; }
#listBody {
  width: 100%; overflow-y: auto; text-align: left;
  display: flex; flex-direction: column; gap: 3px; min-height: 0;
}
.listHead {
  font-size: 12px; font-weight: 800; color: #38bdf8;
  letter-spacing: .6px; margin-top: 6px;
}
.clueRow {
  display: flex; gap: 7px; align-items: baseline;
  background: #0f172a; border: 0; border-radius: 8px;
  padding: 7px 9px; color: #e2e8f0; font-size: 13px; text-align: left;
  cursor: pointer; width: 100%; line-height: 1.3;
}
.clueRow b { flex: 0 0 auto; min-width: 20px; color: #94a3b8; }
.clueRow span { flex: 1 1 auto; min-width: 0; }
.clueRow.solved { color: #6ee7b7; }
.clueRow.solved b { color: #34d399; }
.clueRow.cur { background: #0c4a6e; }

/* ---------- toasts ----------
   A modal is for a decision; a toast is for information. */
.toast-host {
  position: fixed; left: 50%; transform: translateX(-50%);
  top: calc(10px + env(safe-area-inset-top));
  z-index: 9000; display: flex; flex-direction: column; gap: 8px;
  align-items: center; pointer-events: none;
  width: max-content; max-width: calc(100vw - 24px);
}
.toast {
  pointer-events: auto; cursor: pointer;
  padding: 11px 16px; border-radius: 13px;
  background: #f8fafc; color: #0f172a;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  font-size: 14px; font-weight: 700; max-width: calc(100vw - 24px);
  opacity: 0; transform: translateY(-10px);
  transition: opacity .2s ease, transform .2s ease;
}
.toast.in { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .toast, .cell.pop, .cell.done, .cell.active.empty::after { transition: none; animation: none; }
}

/* ---------------------------------------------------------------------------
   Landscape: the board takes the full HEIGHT and everything else moves beside
   it (docs/offline-pwa.md, "A container sized by the thing it is supposed to
   be sizing" — the sibling of that bug).

   A square board is bounded by the smaller of the two dimensions. On a wide
   short window that is the height, and a COLUMN spends the scarce axis on
   furniture: the clue bar, the tool row and the on-screen keyboard. Measured with tests/browser/board-fill.js at 844x390, this
   game's board reached only 61% of the space it could have had, while the two
   games that already had a row layout scored 98-99%.

   `min-aspect-ratio: 1/1` rather than `orientation: landscape`, because the
   question is "is this box wider than it is tall", which is also true of a
   short desktop window that no phone would call landscape.

   Grid rather than flex: the board has to span every row of the side column,
   and `#scr-play.on` (1,1,0) is specific enough to beat `.scr { display: none }`
   without also un-hiding the screen when `.on` is absent.
   --------------------------------------------------------------------------- */
/* `and (max-height: 620px)` — the row layout is for a window whose HEIGHT is
   scarce, not for any window that happens to be wide.
   `min-aspect-ratio: 1/1` alone caught two different situations and only one
   of them wanted this. On a 1440x900 desktop the board ended up bounded by the
   side COLUMN while 200px+ of height sat unused; the ordinary column layout is
   larger there. Measured across 1024x600 (row wins), 1280x800 and 1440x900
   (column wins), the crossover sits just above a short laptop, so 620px keeps
   every phone landscape on the row layout and hands every real desktop window
   back to the column. */
@media (min-aspect-ratio: 1/1) and (max-height: 620px) {
  /* Two grid gotchas, both of which this hit:
     `align-items: center` becomes `align-self: center` on every item, so the
     board cell stops STRETCHING and shrink-wraps its content — the container
     sized by the thing it is meant to size, one more time. And a bare `1fr`
     track has an automatic minimum of `min-content`, so a board that measures
     its wrapper and grows can push the very track that was supposed to bound
     it: go's board reached 750px inside a 286px screen. `minmax(0, 1fr)` plus
     an explicit `align-self: stretch` and `min-height: 0` closes both. */
  #scr-play.on {
    display: grid;
    /* minmax(0, 1fr), NOT 1fr — the SAME trap as the rows below, and I fixed
       the rows and left the columns. A `1fr` track has an automatic minimum of
       min-content, so it cannot shrink under its contents; the board measures
       its wrapper, grows, and the track grows with it. On a phone in landscape
       the height budget is small so the board never reaches the limit and the
       bug is invisible. On a DESKTOP window the height is plentiful, so the
       board is sized off the height, exceeds the column, and the play screen
       overflows sideways inside a centred 820px app — measured on production
       at 1680x1050: scrollWidth 1076 in a clientWidth of 820. The board did
       not overflow the WINDOW, which is why every "is it on screen" check
       passed; you just had to drag to reach the far files. */
    grid-template-columns: minmax(0, 1fr) minmax(300px, 52%);
    grid-template-rows: auto auto minmax(0, 1fr);
    column-gap: 12px; align-items: center; min-height: 0;
  }
  #gridWrap { grid-column: 1; grid-row: 1 / -1; align-self: stretch; min-height: 0; min-width: 0; }
  #clueBar  { grid-column: 2; grid-row: 1; }
  #tools    { grid-column: 2; grid-row: 2; }
  #kb       { grid-column: 2; grid-row: 3; align-self: start; }
  /* Both overlays cover the whole screen, not just their own cell. */
  #ovl, #listOvl { grid-area: 1 / 1 / -1 / -1; }
}

/* On "pad the axis you have to spare" (see chess/go): this play screen pads
   NEITHER axis, so it already satisfies the rule and there is nothing to trim.
   Adding the 4px gutter here was a regression — it took landscape from 96% to
   93%, which is exactly the 8px it added on a 290px budget.

   The rule is *do not pad the binding axis*, and zero is the best possible
   compliance. A rule written as "set it to 4" instead of "do not exceed 4"
   raises the floor for everyone already below it — and CSS has no
   `max-padding`, so this has to be a decision per stylesheet rather than one
   shared declaration. */

/* ---------------------------------------------------------------------------
   The ⚙️ Settings disclosure (v1.5)

   Same shape as maze's and word-search's, and the same reason: the presets are
   on the surface and the switches are one tap away, so the menu is a choice of
   three for a parent and a workbench for a child who wants a big gentle grid.
   --------------------------------------------------------------------------- */
#tierBlurb, .blurb {
  margin: 6px 0 0; font-size: 12.5px; line-height: 1.35;
  color: #94a3b8; text-align: center; max-width: 380px;
}
#custom { width: 100%; max-width: 380px; margin: 10px 0 2px; text-align: left; }
#custom > summary {
  cursor: pointer; list-style: none;
  font: 800 14px/1 inherit; color: #94a3b8;
  padding: 12px 4px; min-height: 44px;
  display: flex; align-items: center; gap: 6px;
}
#custom > summary::-webkit-details-marker { display: none; }
#custom > summary::before { content: "\25B8"; transition: transform .12s; }
#custom[open] > summary::before { transform: rotate(90deg); }
.field { display: flex; align-items: center; gap: 10px; margin: 8px 0; }
.field .lbl { flex: 0 0 46px; font-size: 13px; color: #94a3b8; font-weight: 700; }
.seg {
  flex: 1; min-width: 0; display: flex; gap: 4px;
  background: #0f172a; border: 2px solid #1e293b; border-radius: 999px;
  overflow: hidden; padding: 2px;
}
.seg .opt {
  flex: 1; min-width: 0; border: 0; background: transparent;
  color: #cbd5e1; font: 700 13px/1 inherit; font-family: inherit;
  padding: 9px 4px; border-radius: 999px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* 44px is out of reach for six chips on a 320px phone — the same honest
     compromise R7 already makes for the grid — so the row is the tap target
     and each chip is as tall as the row allows. */
  min-height: 38px;
}
.seg .opt.on { background: #0c4a6e; color: #e2e8f0; box-shadow: inset 0 0 0 1px #38bdf8; }
/* Six sizes do not fit one row on a 375px phone: "17×17" needs 38px of glyph
   and the row has 45px a chip once the label and the padding are out. Wrapping
   is the honest answer — ellipsising the option that says how big the puzzle is
   would be a control that hides its own value. Same shape as word-search's
   `seg wrap`. */
.seg.wrap { flex-wrap: wrap; border-radius: 20px; }
.seg.wrap .opt { flex: 1 1 28%; }
