/* ============================================================
   shell.css — TESSO SHARED LAYOUT SHELL
   ------------------------------------------------------------
   One stylesheet for every page on the site. Change the look of
   200 games by editing this file, not 200 files.

   It provides three page shells — index, category, game — plus
   four board "slot" profiles for the game shell. Nothing here
   knows about any individual game: a game supplies markup that
   drops into .tsl-board and the shell handles the rest.

   DESIGN TOKENS
   -------------
   Colours, fonts and the base atoms (.ctrl, .game-btn, .result,
   .badge) come from shared.css, imported below. This
   file NEVER redefines --bg / --surface / --border / --text /
   --muted / --hint / --accent — shared.css is the single source
   of truth for those.

   Two tokens the site already uses on index.html but which do not
   yet live in shared.css (--accent2, --radius) are declared here
   with the exact values index.html uses, so the shell is
   self-sufficient. When they move into shared.css, delete the
   block marked "PORTABLE TOKENS" and nothing else changes.

   NAMING
   ------
   Every class this file owns is prefixed `tsl-` (Tesso Shared
   Layout) so it can sit alongside the live cat_*.html pages —
   which use .page / .card / .game-area / .ph-* — with zero
   collisions. Migration can therefore be done one page at a time.

   FILE MAP
   --------
     0. Imports + portable tokens + layout metrics
     1. Base / global page behaviour (zoom, overscroll, taps)
     2. Chrome shared by all three shells (navbar, head, footer)
     3. Shell A — index page
     4. Shell B — category page
     5. Shell C — game page
     6. Board slot profiles + scale-to-fit
     7. Controls placement (top toolbar <-> bottom bar)
     8. Breakpoints: tablet, mobile, installed app
     9. iOS safe-area insets
    10. Accessibility / motion / print
    11. Cross-document view transitions
   ============================================================ */

@import url('../shared.css');

/* ── 0. PORTABLE TOKENS ──────────────────────────────────────
   Only tokens that shared.css does not define yet. Values are
   copied verbatim from index.html so the shell matches the live
   home page exactly. */
:root{
  --accent2:#7C6FE0;
  --radius:16px;
}
:root[data-theme="dark"]{
  --accent2:#a99bff;
}

/* ── 0b. LAYOUT METRICS ──────────────────────────────────────
   Structural numbers only — no colours. Every shell reads these,
   so retuning the whole site's rhythm is a one-line edit. */
:root{
  --tsl-nav-h:56px;          /* sticky navbar, excluding safe area   */
  --tsl-bar-h:64px;          /* mobile/app bottom control bar        */
  --tsl-tap:44px;            /* minimum touch target (Apple HIG)     */
  --tsl-gutter:1.5rem;       /* page side padding, desktop           */
  --tsl-gutter-sm:1rem;      /* page side padding, mobile            */
  --tsl-gap:12px;            /* grid gap — matches shared.css .cards */
  --tsl-stack:1rem;          /* vertical rhythm between blocks       */
  --tsl-max:960px;           /* reading width — matches .page        */
  --tsl-max-wide:1180px;     /* index card grid width                */
  --tsl-radius-sm:10px;
  --tsl-shadow:0 6px 20px rgba(0,0,0,.07);

  /* Game-shell chrome heights. Fixed from 720px up so the board can be
     centred on the viewport (see §5b-2); `auto` below that, where the
     board is bottom-bar-bound instead. */
  --tsl-head-h:auto;         /* breadcrumb + description block */
  --tsl-tbar-h:auto;         /* name + difficulty + score row   */
  --tsl-balance:0px;         /* spacer that mirrors the top chrome */

  /* The result row's height is a token, not a bare `2.8em`, because
     the balance calc below needs the same number. An em there would
     resolve against the root's 16px while the result box resolves it
     against its own 14px — a 5.6px error that shows up as the board
     sitting a few pixels low.

     Every game page's own inline <style> stacks .tsl-status and
     .tsl-result on top of each other inside a shared .tsl-status-result
     slot (position:absolute;inset:0 on both), and that wrapper is
     sized to THIS token, not --tsl-status-h -- so --tsl-result-h is
     what actually governs how much text either row can show before
     clipping, on every game page. Sized for three lines, not two:
     several engines' reset()/hint() text (rules intros, tips) runs
     long enough to wrap 2-3 times, and a shorter box was clipping
     real lines instead of just tolerating them. */
  --tsl-result-fz:14px;
  --tsl-result-h:calc(4.2 * var(--tsl-result-fz));

  /* Kept equal to --tsl-result-h, not sized independently: this token
     only drives the STATUS row's grid-track height in .tsl-game-stack
     below (see --tsl-balance) -- the element itself is always resized
     to --tsl-result-h by the per-page override above. A mismatch here
     would starve or overshoot that grid track relative to the box
     that actually renders in it. */
  --tsl-status-fz:13px;
  --tsl-status-h:var(--tsl-result-h);
}

/* ── 1. BASE ─────────────────────────────────────────────────
   Applies to every shell. Keeps a game board from being zoomed,
   rubber-banded or text-inflated out of its fitted size. */
html{
  -webkit-text-size-adjust:100%;   /* iOS must not inflate our text */
  text-size-adjust:100%;
}
html,body{
  overscroll-behavior:none;        /* no pull-to-refresh / bounce   */
}
body{
  font-family:'DM Sans',sans-serif;
  background:var(--bg);
  color:var(--text);
  min-height:100vh;
  min-height:100dvh;
}

/* Interactive surfaces: kill the 300ms tap delay and double-tap
   zoom, but leave pinch-zoom alone so the page stays accessible.
   (We deliberately do NOT ship user-scalable=no in the viewport
   meta — see LAYOUT-SPEC.md, "Zoom".) */
.tsl-board,
.tsl-toolbar,
.tsl-controls,
.tsl-nav{
  touch-action:manipulation;
}
.tsl-board{
  -webkit-user-select:none;user-select:none;
  -webkit-touch-callout:none;
  -webkit-tap-highlight-color:transparent;
}

/* ── 2. SHARED CHROME ────────────────────────────────────────*/

/* 2a. Navbar — identical on all three shells */
.tsl-nav{
  position:sticky;top:0;z-index:100;
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  height:calc(var(--tsl-nav-h) + env(safe-area-inset-top));
  padding-top:env(safe-area-inset-top);
  padding-left:calc(var(--tsl-gutter) + env(safe-area-inset-left));
  padding-right:calc(var(--tsl-gutter) + env(safe-area-inset-right));
  background:color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  border-bottom:1px solid var(--border);
}
.tsl-nav-left{display:flex;align-items:center;gap:12px;min-width:0;}
.tsl-nav-right{display:flex;align-items:center;gap:6px;flex-shrink:0;}
.tsl-logo{
  display:flex;align-items:center;gap:8px;
  font-size:18px;font-weight:700;letter-spacing:-.3px;
  color:var(--text);text-decoration:none;white-space:nowrap;
}
.tsl-ham{
  width:38px;height:38px;flex-shrink:0;padding:0;cursor:pointer;
  border:1px solid var(--border);background:var(--surface);
  border-radius:var(--tsl-radius-sm);
  display:none;flex-direction:column;justify-content:center;align-items:center;gap:4px;
}
.tsl-ham span{display:block;width:18px;height:2px;background:var(--text);border-radius:2px;}
.tsl-nav-menu{display:flex;gap:3px;background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:3px;}
.tsl-nav-link{
  font-size:13px;font-weight:600;color:var(--muted);text-decoration:none;
  padding:6px 14px;border-radius:8px;white-space:nowrap;
  transition:background .15s,color .15s;
}
.tsl-nav-link:hover{color:var(--text);}
.tsl-nav-link.is-active{background:var(--accent);color:#fff;}
.tsl-nav-ico{
  width:34px;height:34px;padding:0;cursor:pointer;
  border:1px solid var(--border);background:var(--surface);border-radius:9px;
  display:inline-flex;align-items:center;justify-content:center;
  font-size:15px;color:var(--text);
  transition:border-color .15s,background .15s;
}
.tsl-nav-ico:hover{border-color:var(--accent2);}

/* 2b. Page body wrapper (index + category shells) */
.tsl-page{
  max-width:var(--tsl-max);
  margin:0 auto;
  padding:1.5rem var(--tsl-gutter) 3rem;
}
.tsl-page--wide{max-width:var(--tsl-max-wide);}

/* 2b-2. Breadcrumb — plain text with arrow separators.
   ------------------------------------------------------------
   Deliberately NOT the bordered pills that shared.css .breadcrumb
   renders. A breadcrumb is orientation, not an action: boxing each
   crumb gives it the visual weight of a button and competes with
   the real buttons on the page. Text in the site's muted grey, an
   arrow between, colour and underline on hover — that reads as a
   trail and costs a third of the vertical space.

   shared.css .breadcrumb is left exactly as it is, so the live
   cat_*.html pages are unaffected. This is a new class. */
.tsl-crumb{
  display:flex;align-items:center;flex-wrap:wrap;
  gap:2px 7px;
  font-size:13px;color:var(--muted);
}
.tsl-crumb a{
  color:var(--muted);text-decoration:none;
  padding:3px 0;                    /* vertical hit area, no visible box */
  border-radius:4px;
  transition:color .15s;
}
.tsl-crumb a:hover{color:var(--text);text-decoration:underline;text-underline-offset:3px;}
.tsl-crumb a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.tsl-crumb .sep{
  color:var(--hint);font-size:14px;line-height:1;
  user-select:none;pointer-events:none;
}
/* The current page: present for orientation, dimmed so it does not
   read as a link. Mark it up as <span aria-current="page">. */
.tsl-crumb [aria-current]{color:var(--hint);}

/* 2c. Title / description block — shared by all three shells.
   The shell only owns the heading. */
.tsl-head{margin-bottom:1.75rem;}
.tsl-head h1{font-size:26px;font-weight:600;letter-spacing:-.3px;margin-bottom:4px;}
.tsl-head p{font-size:14px;color:var(--muted);line-height:1.5;max-width:62ch;}

.tsl-section-head{
  display:flex;align-items:center;justify-content:space-between;
  padding-bottom:.75rem;
}
.tsl-section-head h2{
  font-size:13px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;
  color:var(--hint);font-family:'DM Mono',monospace;
}
.tsl-section-head span{font-size:12px;color:var(--hint);}

/* 2d. Cards — one card component, two densities */
.tsl-grid{display:grid;gap:var(--tsl-gap);}
.tsl-grid--cats{grid-template-columns:repeat(auto-fill,minmax(200px,1fr));}
.tsl-grid--games{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));}
/* Game cards are denser than category cards — shorter banner, same
   component. Matches the .sub-card density on the live pages. */
.tsl-grid--games .tsl-card-banner{height:88px;font-size:34px;}
.tsl-grid--games .tsl-card-name{font-size:13px;}
.tsl-grid--games .tsl-card-sub{font-size:11px;}

.tsl-card{
  background:var(--surface);border:1.5px solid var(--border);
  border-radius:var(--radius);overflow:hidden;
  text-decoration:none;color:inherit;
  display:flex;flex-direction:column;
  transition:transform .18s,box-shadow .18s,border-color .18s;
}
.tsl-card:hover{transform:translateY(-4px);box-shadow:0 12px 32px rgba(0,0,0,.1);border-color:var(--accent2);}
.tsl-card-banner{
  height:110px;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  font-size:44px;background:var(--bg);
}
/* Emoji banners size via the font-size above; a generated SVG preview
   (many category pages render one per game — Marble Solitaire's board
   thumbnails, Indian Games' board/piece icons) has no font-size to key
   off, so it needs its own rule. shared.css already carries this exact
   rule sitewide for the old .sub-card .card-preview; this is the same
   contract under the shell's own class so a migrated card's SVG scales
   the same way it always has. */
.tsl-card-banner svg{width:70%;height:70%;}
.tsl-card-body{padding:12px 14px 14px;display:flex;flex-direction:column;gap:5px;flex:1;}
.tsl-card-name{font-size:14px;font-weight:600;letter-spacing:-.2px;display:flex;align-items:center;gap:6px;}
.tsl-card-sub{font-size:12px;color:var(--muted);line-height:1.4;}
.tsl-card-count{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:24px;height:18px;padding:0 5px;border-radius:10px;
  background:var(--accent);color:#fff;
  font-size:10px;font-weight:700;font-family:'DM Mono',monospace;flex-shrink:0;
}
.tsl-card-foot{margin-top:auto;padding-top:10px;display:flex;align-items:center;justify-content:space-between;}
.tsl-play{
  display:inline-flex;align-items:center;gap:4px;
  background:var(--accent);color:#fff;border-radius:7px;
  padding:5px 12px;font-size:11px;font-weight:600;
  transition:background .15s;
}
.tsl-card:hover .tsl-play{background:var(--accent2);}
.tsl-chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;}
.tsl-chip{
  font-size:9px;font-family:'DM Mono',monospace;
  padding:2px 7px;border-radius:20px;white-space:nowrap;
  background:var(--bg);border:1px solid var(--border);color:var(--muted);
}

/* ── 3. SHELL A — INDEX PAGE ─────────────────────────────────
   navbar → hero → search → section head → category grid */
/* The tagline sits close under the navbar — 2.5rem of air between a
   56px bar and one line of muted text read as a gap, not as breathing
   room, and it pushed the category grid below the fold. */
.tsl-hero{text-align:center;padding:1.25rem var(--tsl-gutter) .875rem;}
.tsl-hero h1{font-size:15px;font-weight:400;color:var(--muted);max-width:380px;margin:0 auto;}
.tsl-search{padding:0 var(--tsl-gutter) 1.25rem;max-width:500px;margin:0 auto;}
.tsl-search input{
  width:100%;padding:11px 16px 11px 42px;
  border:1.5px solid var(--border);border-radius:12px;
  font-size:14px;font-family:'DM Sans',sans-serif;
  background:var(--surface);color:var(--text);outline:none;
  transition:border-color .15s,box-shadow .15s;
}
.tsl-search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(83,74,183,.12);}

/* ── 4. SHELL B — CATEGORY PAGE ──────────────────────────────
   navbar → breadcrumb → head → game grid. Structure is the index
   shell at a tighter card density; no extra rules needed beyond
   the grid modifier, which is the point of sharing them. */

/* ── 5. SHELL C — GAME PAGE ──────────────────────────────────
   The only shell with a fixed-height region.

     .tsl-game-stack  height:100dvh, never scrolls
       ├ .tsl-nav          auto
       ├ .tsl-game-head    breadcrumb + description
       ├ .tsl-toolbar      game name + difficulty | score + controls
       ├ .tsl-status       fixed-height in-progress line ("Round 3/5",
                           "AI is thinking…") — omit the element for
                           games with nothing to report between moves
       ├ .tsl-board-frame  1fr   ← all leftover height, board fits here
       ├ .tsl-result       auto
       └ .tsl-balance      desktop only — mirrors the chrome above so
                           the board lands on the viewport's centre
     .tsl-howto        normal flow, below the fold, scrolls into view

   Because the stack is exactly one viewport tall the board can
   never push anything off-screen, and the "how to play" copy is
   still reachable by scrolling — good for SEO and for players who
   want it, invisible to players who don't.

   The game NAME lives in the toolbar, not in the head, so the
   difficulty control can sit directly beside it. One row of chrome
   instead of two, which also buys the board height back. */
.tsl-game-stack{
  height:100vh;
  height:100dvh;
  display:grid;
  grid-template-rows:auto auto auto auto minmax(0,1fr) auto auto;
  overflow:hidden;
}
/* The head is the breadcrumb and nothing else. The game's description
   used to sit here, but above the board it is a block of prose nobody
   reads mid-play that pushes the board down on every device. It lives
   in "how to play" now (§5c), where someone looking for it will
   actually go. That leaves the top of a game page as breadcrumb,
   name, controls — and the board. */
.tsl-game-head{
  padding:.6rem var(--tsl-gutter) 0;
  max-width:var(--tsl-max);width:100%;margin:0 auto;
}

/* 5a. Toolbar — name + difficulty on the left, score (and on desktop
   the controls, see §7) on the right. */
.tsl-toolbar{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding:.6rem var(--tsl-gutter) .5rem;
  max-width:var(--tsl-max);width:100%;margin:0 auto;
}
.tsl-toolbar-left{display:flex;align-items:center;gap:10px;min-width:0;flex:1 1 auto;flex-wrap:wrap;}
.tsl-toolbar-right{display:flex;align-items:center;gap:10px;margin-left:auto;flex-shrink:0;}
.tsl-game-name{
  font-size:20px;font-weight:600;letter-spacing:-.2px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;
}
.tsl-score{
  display:inline-flex;align-items:center;gap:6px;
  font-family:'DM Mono',monospace;font-size:13px;font-weight:500;
  color:var(--text);
  background:var(--surface);border:1px solid var(--border);
  border-radius:9px;padding:6px 12px;white-space:nowrap;
}
.tsl-score b{font-weight:500;color:var(--accent);}
.tsl-score small{font-size:11px;color:var(--muted);font-family:'DM Sans',sans-serif;}

/* 5a-2. Status line — the in-progress state a game reports between
   moves: "24 pegs left", "AI is thinking…", "Round 3/5", whose turn
   it is. Distinct from .tsl-result, which is the fixed end-of-round
   verdict; a game typically drives both from the same two functions
   it already has (setStatus/setResult in the current per-category
   pages) with no new plumbing.

   Optional: a puzzle with nothing to report between moves (most
   solitaire-style games) can simply omit the element and leave the
   row blank. The row's height is part of the stack's fixed layout
   budget either way — same reasoning as --tsl-head-h/--tsl-tbar-h,
   every game page keeps the same rhythm whether or not it uses the
   slot — so leaving it out does not grow the board; it trades a
   sliver of blank space for one less thing to wire up. */
.tsl-status{
  height:var(--tsl-status-h);overflow:hidden;
  display:flex;align-items:flex-start;justify-content:center;
  padding:2px var(--tsl-gutter) 0;
  max-width:var(--tsl-max);width:100%;margin:0 auto;
  font-size:var(--tsl-status-fz);color:var(--muted);line-height:1.4;text-align:center;
}

/* 5b. Result message — fixed height so the board never shifts
   under the player's thumb when the text changes length. Same
   rule and same value as shared.css .result. */
.tsl-result{
  height:var(--tsl-result-h);overflow:hidden;
  display:flex;align-items:center;justify-content:center;
  padding:0 var(--tsl-gutter);
  font-size:var(--tsl-result-fz);font-weight:500;line-height:1.4;text-align:center;
  color:var(--muted);
}
.tsl-result.is-win{color:var(--win);}
.tsl-result.is-lose{color:var(--lose);}

/* 5b-2. Balance row — how the board gets centred on the browser.
   ------------------------------------------------------------
   The board frame is the 1fr row, so it takes whatever is left
   over. That centres the board inside the LEFTOVER space, not
   inside the window: with heavy chrome above (nav + breadcrumb +
   toolbar) and only a one-line result below, the board ends up
   sitting low.

   Making the two sides equal fixes it exactly. If T is everything
   above the frame and B everything below, the frame's midpoint is
   T + (H − T − B)/2, which equals H/2 exactly when B == T. So the
   balance row is sized to whatever the top chrome is, minus the
   result row that is already down there:

     balance = nav + head + toolbar − result

   That needs the top chrome to be a KNOWN height, which is why
   --tsl-head-h and --tsl-tbar-h are fixed on desktop. That is a
   feature, not a workaround: it is the same reason .tsl-result has
   a fixed height — a long game name or a two-line description must
   not move the board relative to every other game in the category.

   The trade is real and worth knowing: perfect centring costs board
   size, because the board can only use the symmetric space. To
   centre on the play area (below the navbar) instead and win some
   of it back, drop var(--tsl-nav-h) from the calc below. */
.tsl-balance{height:var(--tsl-balance);}

/* 5c. How to play — below the fold, normal document flow.
   Opens with the game's description as a lead paragraph, so the
   section reads "what this is, then how to play it". */
.tsl-howto{
  max-width:var(--tsl-max);margin:0 auto;
  padding:2rem var(--tsl-gutter) 3rem;
}
.tsl-howto h2{font-size:17px;font-weight:600;margin-bottom:.5rem;}
.tsl-game-desc{
  font-size:15px;color:var(--text);line-height:1.6;
  margin-bottom:1rem;padding-bottom:1rem;
  border-bottom:1px solid var(--border);
  max-width:64ch;
}
.tsl-howto h3{font-size:14px;font-weight:600;margin:1.25rem 0 .35rem;}
.tsl-howto p,.tsl-howto li{font-size:14px;color:var(--muted);line-height:1.65;}
.tsl-howto ul,.tsl-howto ol{padding-left:1.25rem;display:flex;flex-direction:column;gap:4px;}
.tsl-howto-card{
  background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius);padding:18px 20px;
}

/* ── 6. BOARD SLOTS + SCALE-TO-FIT ───────────────────────────
   The frame takes every pixel the stack has left over, and the
   board fits inside it — never taller, never wider, never scrolled.

   HOW THE FIT WORKS
   -----------------
   The frame is a size container, so the board can compute its own
   width from BOTH of the frame's axes:

     width: min(100cqw, 100cqh * ratio, --tsl-board-max)

   ...i.e. "as wide as the frame allows", "as wide as the frame's
   HEIGHT allows once the ratio is applied", and "no wider than this
   profile's cap" — whichever is smallest wins. aspect-ratio then
   derives the height from that width, so the ratio is exact on both
   axes at every viewport size.

   The obvious alternative — width:100% plus max-height:100% — does
   NOT work: the height clamp shrinks the box but the width stays
   put, so a "square" board silently renders as a rectangle. Use the
   min() form.

   Games size their own contents in % / fr of .tsl-board-inner, so
   they scale with the board automatically.

   --tsl-arn is a plain NUMBER (width ÷ height), not a fraction, so
   it can be multiplied in the calc above. Override it per game when
   the profile default is wrong — a 10x20 Tetris well is .5, a 7x6
   Connect Four grid is 1.167.

   The frame must have a definite height for the cq units to mean
   anything — inside .tsl-game-stack the minmax(0,1fr) grid row
   provides one. If you ever use .tsl-board-frame outside the stack,
   give it an explicit height first, or size containment will
   collapse it to nothing. */
.tsl-board-frame{
  min-height:0;min-width:0;overflow:hidden;
  display:grid;place-items:center;
  padding:4px var(--tsl-gutter);
  container-type:size;
  container-name:tslboard;
}
.tsl-board{
  --tsl-arn:1;
  --tsl-board-max:640px;
  aspect-ratio:var(--tsl-arn);
  width:min(100cqw, calc(100cqh * var(--tsl-arn)), var(--tsl-board-max));
  height:auto;
  max-width:100%;                /* belt and braces if cq units are unsupported */
  max-height:100%;
  background:var(--surface);
  border:1px solid var(--border);
  border-radius:var(--radius);
  overflow:hidden;
}
.tsl-board-inner{width:100%;height:100%;display:flex;flex-direction:column;}

/* 6a. square — Sudoku, chess, checkers, 15-puzzle, memory grids */
.tsl-board[data-slot="square"]{--tsl-arn:1;--tsl-board-max:640px;}

/* 6b. tall-narrow — Tetris, Connect Four, drop/stack games.
   Portrait ratio, capped narrow so it doesn't sprawl on desktop. */
.tsl-board[data-slot="tall"]{--tsl-arn:.625;--tsl-board-max:420px;}

/* 6c. wide — word games, keyboards, trivia, card rows */
.tsl-board[data-slot="wide"]{--tsl-arn:1.778;--tsl-board-max:880px;}

/* 6d. free-canvas — Snake, arcade, anything that reads its own
   pixel size at runtime. No fixed ratio: it fills the frame, and
   the game measures .tsl-board on resize. */
.tsl-board[data-slot="canvas"]{
  --tsl-board-max:900px;
  aspect-ratio:auto;
  width:min(100cqw,var(--tsl-board-max));
  height:100cqh;
}
.tsl-board[data-slot="canvas"] canvas{display:block;width:100%;height:100%;}

/* ── 7. CONTROLS PLACEMENT ───────────────────────────────────
   ONE set of buttons in the markup, inside .tsl-toolbar. Desktop
   leaves them there; mobile and app pin the same element to the
   bottom of the screen. No duplicated DOM, no JS. */
.tsl-controls{
  display:flex;align-items:center;gap:8px;flex-wrap:nowrap;
}
.tsl-controls .ctrl{min-height:32px;}

/* ── 8. BREAKPOINTS ──────────────────────────────────────────
   desktop  >= 1024px   full chrome, controls in the top toolbar
   tablet   720–1023px  full chrome, tighter gutters
   mobile   <= 719px    hamburger nav, controls in a bottom bar
   app      display-mode:standalone  mobile layout minus web chrome
   Values are literal because CSS custom properties cannot be used
   inside media query conditions. */

/* 8a. Desktop + tablet — centre the board on the browser.
   Fixed chrome heights make the balance row computable; see §5b-2
   for the arithmetic. --tsl-head-h is now one breadcrumb line rather
   than a breadcrumb plus a two-line description — moving the copy
   into "how to play" gave 32px of it straight back to the board. */
@media (min-width:720px){
  :root{
    --tsl-head-h:34px;
    --tsl-tbar-h:52px;
    --tsl-balance:calc(var(--tsl-nav-h) + var(--tsl-head-h) + var(--tsl-tbar-h) + var(--tsl-status-h) - var(--tsl-result-h));
  }
  .tsl-game-stack{
    grid-template-rows:auto var(--tsl-head-h) var(--tsl-tbar-h) var(--tsl-status-h) minmax(0,1fr) auto var(--tsl-balance);
  }
  .tsl-game-head,.tsl-toolbar{overflow:hidden;}
}

/* 8b. Tablet */
@media (max-width:1023px){
  :root{--tsl-gutter:1.25rem;}
  .tsl-board[data-slot="wide"]{--tsl-board-max:720px;}
}

/* 8c. Mobile browser */
@media (max-width:719px){
  :root{--tsl-gutter:var(--tsl-gutter-sm);}

  .tsl-ham{display:flex;}
  .tsl-nav-menu{display:none;}
  .tsl-page{padding:1rem var(--tsl-gutter) 2.5rem;}
  .tsl-head{margin-bottom:1.25rem;}
  .tsl-head h1{font-size:22px;}
  .tsl-grid--cats{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));}
  .tsl-grid--games{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));}
  .tsl-card-banner{height:88px;font-size:34px;}

  /* A 16:9 word board on a portrait phone wastes most of the screen.
     Squarer ratio here so the grid and the on-screen keyboard both
     get room. */
  .tsl-board[data-slot="wide"]{--tsl-arn:1;}

  /* Game shell: reclaim vertical space for the board */
  .tsl-game-head{padding-top:.45rem;}
  .tsl-crumb{font-size:12px;gap:2px 6px;}
  .tsl-game-desc{font-size:14px;}
  .tsl-toolbar{padding:.4rem var(--tsl-gutter) .35rem;gap:8px;}
  :root{--tsl-result-fz:13px;--tsl-result-h:calc(4.2 * var(--tsl-result-fz));}

  /* Name and difficulty share the first line — the difficulty control
     belongs beside the thing it applies to, and on a phone that row is
     the only chrome there is room for. The score drops to the next
     line if the two of them fill the width. */
  .tsl-game-name{font-size:17px;flex:0 1 auto;}
  .tsl-toolbar-left{flex:1 1 100%;gap:8px;flex-wrap:nowrap;}
  .tsl-toolbar-right{margin-left:auto;}

  /* Reserve the bottom bar's height inside the fixed stack so the
     board frame shrinks instead of hiding behind the bar. */
  .tsl-game-stack{
    padding-bottom:calc(var(--tsl-bar-h) + env(safe-area-inset-bottom));
  }

  /* Controls jump out of the toolbar and pin to the bottom */
  .tsl-controls{
    position:fixed;left:0;right:0;bottom:0;z-index:60;
    justify-content:center;flex-wrap:nowrap;
    min-height:var(--tsl-bar-h);
    background:var(--surface);
    border-top:1px solid var(--border);
    padding-top:8px;
    padding-bottom:calc(8px + env(safe-area-inset-bottom));
    padding-left:calc(12px + env(safe-area-inset-left));
    padding-right:calc(12px + env(safe-area-inset-right));
  }
  .tsl-controls .ctrl{
    min-height:var(--tsl-tap);
    min-width:var(--tsl-tap);
    flex:1 1 0;max-width:180px;
    padding:0 12px;font-size:13px;
  }
  /* Keep the fixed bar from covering the end of the how-to copy */
  .tsl-howto{padding-bottom:calc(2.5rem + var(--tsl-bar-h) + env(safe-area-inset-bottom));}
}

/* 8d. Installed app (PWA / Capacitor).
   Duplicated deliberately: the real app matches display-mode, and
   the wireframe forces the same rules with <html data-app="1"> so
   app layout can be reviewed in a normal browser tab. Keep the two
   blocks identical.

   Note the app does NOT use the desktop balance row even on a tablet.
   Centring on the window is a browser concern — an installed app has
   no browser chrome to centre against, and the right instinct there
   is to give the board every pixel between the toolbar and the
   bottom bar. --tsl-head-h/--tsl-tbar-h go back to auto for the
   same reason. */
@media (display-mode:standalone){
  :root{--tsl-head-h:auto;--tsl-tbar-h:auto;--tsl-balance:0px;}
  .tsl-game-stack{grid-template-rows:auto auto auto auto minmax(0,1fr) auto auto;}
  .tsl-nav-menu{display:none;}
  .tsl-nav{position:static;}            /* no URL bar to stick under */
  .tsl-game-head .tsl-crumb{display:none;}
  .tsl-hero{display:none;}
  .tsl-howto{display:none;}             /* surfaced by an in-app sheet */
  /* Difficulty sits beside the game name, as on mobile */
  .tsl-toolbar-left{flex:1 1 100%;gap:8px;flex-wrap:nowrap;}
  .tsl-toolbar-right{margin-left:auto;}
  .tsl-game-name{font-size:17px;flex:0 1 auto;}
  .tsl-controls{
    position:fixed;left:0;right:0;bottom:0;z-index:60;
    justify-content:center;min-height:var(--tsl-bar-h);
    background:var(--surface);border-top:1px solid var(--border);
    padding-top:8px;
    padding-bottom:calc(8px + env(safe-area-inset-bottom));
    padding-left:calc(12px + env(safe-area-inset-left));
    padding-right:calc(12px + env(safe-area-inset-right));
  }
  .tsl-controls .ctrl{min-height:var(--tsl-tap);min-width:var(--tsl-tap);flex:1 1 0;max-width:180px;font-size:13px;}
  .tsl-game-stack{padding-bottom:calc(var(--tsl-bar-h) + env(safe-area-inset-bottom));}
}
:root[data-app="1"]{--tsl-head-h:auto;--tsl-tbar-h:auto;--tsl-balance:0px;}
:root[data-app="1"] .tsl-game-stack{grid-template-rows:auto auto auto auto minmax(0,1fr) auto auto;}
:root[data-app="1"] .tsl-nav-menu{display:none;}
:root[data-app="1"] .tsl-nav{position:static;}
:root[data-app="1"] .tsl-game-head .tsl-crumb{display:none;}
:root[data-app="1"] .tsl-hero{display:none;}
:root[data-app="1"] .tsl-howto{display:none;}
:root[data-app="1"] .tsl-toolbar-left{flex:1 1 100%;gap:8px;flex-wrap:nowrap;}
:root[data-app="1"] .tsl-toolbar-right{margin-left:auto;}
:root[data-app="1"] .tsl-game-name{font-size:17px;flex:0 1 auto;}
:root[data-app="1"] .tsl-controls{
  position:fixed;left:0;right:0;bottom:0;z-index:60;
  justify-content:center;min-height:var(--tsl-bar-h);
  background:var(--surface);border-top:1px solid var(--border);
  padding-top:8px;
  padding-bottom:calc(8px + env(safe-area-inset-bottom));
  padding-left:calc(12px + env(safe-area-inset-left));
  padding-right:calc(12px + env(safe-area-inset-right));
}
:root[data-app="1"] .tsl-controls .ctrl{min-height:var(--tsl-tap);min-width:var(--tsl-tap);flex:1 1 0;max-width:180px;font-size:13px;}
:root[data-app="1"] .tsl-game-stack{padding-bottom:calc(var(--tsl-bar-h) + env(safe-area-inset-bottom));}
:root[data-app="1"] .tsl-ham{display:flex;}

/* ── 9. SAFE AREA ────────────────────────────────────────────
   Every edge-anchored surface pads itself with env(). The navbar
   and controls do it inline above; these are the leftovers.
   Nothing bottom-anchored may use a bare px value. */
.tsl-page{
  padding-left:calc(var(--tsl-gutter) + env(safe-area-inset-left));
  padding-right:calc(var(--tsl-gutter) + env(safe-area-inset-right));
  padding-bottom:calc(3rem + env(safe-area-inset-bottom));
}
.tsl-howto{
  padding-left:calc(var(--tsl-gutter) + env(safe-area-inset-left));
  padding-right:calc(var(--tsl-gutter) + env(safe-area-inset-right));
}
.tsl-drawer{
  padding-top:env(safe-area-inset-top);
  padding-left:env(safe-area-inset-left);
  padding-bottom:env(safe-area-inset-bottom);
}

/* ── 10. A11Y / MOTION / PRINT ───────────────────────────────*/
.tsl-nav-link:focus-visible,
.tsl-card:focus-visible,
.tsl-controls .ctrl:focus-visible,
.tsl-search input:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;
}
.tsl-sr{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
@media (prefers-reduced-motion:reduce){
  .tsl-card,.tsl-card:hover,.tsl-play,.tsl-nav-link{transition:none;transform:none;}
  /* Same preference, same treatment, for §11's cross-document
     transitions below: keep the API active (so navigation still
     works) but skip the actual cross-fade animation. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*){
    animation:none !important;
  }
}
@media print{
  .tsl-nav,.tsl-controls,.tsl-toolbar{display:none;}
  .tsl-game-stack{height:auto;overflow:visible;}
}

/* ── 11. CROSS-DOCUMENT VIEW TRANSITIONS ─────────────────────
   Prev/Next between game pages is a real navigation (a plain
   <a href>, on purpose — that's what makes each game its own
   indexable, linkable page; see LAYOUT-SPEC.md). That means every
   switch is a full document load, and every browser has some kind
   of hand-off moment between tearing down the old page and painting
   the new one. No CSS on either page can remove that by itself,
   because the moment isn't happening "in" either page — it's
   between them.

   Opting in to the View Transitions API's cross-document mode asks
   the browser to cross-fade the old document into the new one
   instead of a hard cut. BOTH the outgoing and incoming page need
   this rule for a transition to run between them, which is exactly
   why it lives in the shared shell rather than per-page: every page
   that includes shell.css opts in together, automatically, with
   nothing for an individual game page to remember to add.

   Chromium only (Chrome/Edge) as of this writing. Everywhere else
   this rule is simply inert — Safari/Firefox ignore it and fall
   back to today's ordinary navigation, so this is a strict
   enhancement with no fallback code required and nothing to break
   where it isn't supported. */
@view-transition{
  navigation:auto;
}

/* Named, rather than left to the default whole-page cross-fade, for
   the two things this was actually built to fix: the navbar and the
   breadcrumb should read as the SAME element continuing across the
   navigation, not fade out and back in as part of a generic
   page-wide dissolve. Every page has exactly one of each, so a fixed
   name (not derived per-page) is safe — view-transition-name must be
   unique within a single page's tree, not across the whole site.
   .ph-nav's matching name lives with its other styles in menu.js,
   not here, since that's an injected element shell.css never touches. */
.tsl-crumb{view-transition-name:site-crumb;}
