/* system/apps/wordflex/styles-live.css -- WordFlex Live's own styles
 *
 * Sept 23 2026. Stage 3 of the real-multiplayer redesign (see README.md
 * and app-live.js's own header). Linked by the build the same way
 * styles.css and styles-test.css are (see scripts/generate-registry.ts).
 *
 * Board sizing: a straightforward CSS grid, 15x15, sized to fill the
 * available width up to a sane cap -- no zoom/pan physics in this v1
 * (see app-live.js's header for why that's a deliberate, easily-revised
 * scope call, not a decision about the final design).
 *
 * Sept 28 2026 (Ron: "we've done good textures for 3-D stuff in the
 * Hideaway app, can we do something similar here?"): checked --
 * Hideaway's wood/stone look is NOT a CSS trick. It's a real Three.js
 * WebGL scene (apps/hideaway/app.js) using genuine PBR photo textures
 * (e.g. `oak_wood_planks`) fetched live from Poly Haven at runtime and
 * mapped onto 3D geometry, with a procedural gradient only as its
 * fallback when a texture fails to load. WordFlex is a flat DOM/CSS
 * app with no canvas, no bundler, and no texture-asset pipeline (this
 * whole studio has none beyond what's already wired up), so that
 * technique doesn't carry over -- the wood grain below is a from-
 * scratch, CSS-gradient-only approximation (layered repeating-linear-
 * gradient streaks over the original flat two-stop gradient), aimed at
 * the same "not just a flat color" goal at whatever quality level CSS
 * alone can reach, not a port of Hideaway's actual method.
 */

/* ---- theming ----
 * Sept 24 2026 ("what if for each we have a matching bright mode" --
 * Ron), extended the same day ("you are greenlit!" -- the felt/wood
 * chrome itself, see .wfl-chrome below). These custom properties are
 * scoped to .wordflex-live, so overriding --brand here never leaks to
 * any other app -- it just shadows the shell's own --brand for
 * everything under this root, which is what lets every existing
 * `var(--brand)` reference below (buttons, the acting-player chip,
 * selected-tile outlines) pick up a theme's accent color for free,
 * with no per-rule changes.
 *
 * The default block (no [data-wfl-theme]) used to preserve the app's
 * old, unstyled shell look byte-for-byte -- there is no such baseline
 * to preserve anymore now that the felt/wood chrome is real, so the
 * default IS the light Tabletop look from the design mockups, same as
 * picking "Espresso -- Light" on purpose (see that var block's own
 * note). Six real palettes -- Espresso/Midnight/Charcoal, each Light
 * and Dark -- cover board premium squares, tile faces, accent, AND
 * (as of this pass) the chrome card itself: --wfl-chrome-bg (the wood
 * card), --wfl-board-surface/--wfl-board-grid-line (the felt behind
 * the grid), --wfl-tray-bg (the rack tray). Selecting a theme is real
 * and persists (see app-live.js's THEME_OPTIONS + room.ts's
 * setTheme/prefs).
 *
 * What's still NOT built (Ron chose "core chrome first" for the first
 * pass -- the play screen, header, exit well; the unified Setup
 * screen below was the next slice, Sept 24 2026): the tile-and-channel
 * history slider, real dictionary search, the system-icon well. Those
 * stay as-is until their own pass.
 */
.wordflex-live {
  --wfl-cell-bg: #e9e2cf;
  --wfl-center-bg: #f1d9a6;
  --wfl-tw-bg: #c15c46; --wfl-tw-text: #fff;
  --wfl-dw-bg: #d99486;
  --wfl-tl-bg: #3f76b8; --wfl-tl-text: #fff;
  --wfl-dl-bg: #8fb6d9;
  --wfl-tile-bg: #f6e3b4;
  --wfl-tile-border: #b8895a;
  --wfl-tile-text: #3b2410;
  --wfl-pending-bg: #fff3cf;
  --wfl-pending-border: #d4a017;
  /* Sept 24 2026 ("core chrome first" -- Ron): the felt-and-wood card
   * itself -- see .wfl-chrome below. Unlike the board/tile vars above
   * (which had a real "unstyled shell" baseline to preserve), there
   * was never a neutral default for this -- the app had no chrome of
   * its own before. So the DEFAULT here (no [data-wfl-theme]) IS the
   * light Tabletop look from the design mockups -- "App default" in
   * the theme picker and "Espresso -- Light" are now the same look on
   * purpose, not a bug; picking either shows this. */
  /* Sept 28 2026 (Ron: "do we have a wood texture we can use for the
   * table? Instead of just brown?... it just looks like a weird brown
   * color. (It looks like poop.)"): layered CSS gradients standing in
   * for real wood grain -- see this file's own header note on WHY it's
   * CSS-only, not Hideaway's technique. Four comma-separated layers,
   * painted first-listed-on-top:
   *   1. a soft diagonal sheen (light catching the "varnish")
   *   2. a tight 3px repeating streak (the actual grain lines)
   *   3. a wider ~60-80px repeating band (plank/knot waviness, so it
   *      doesn't read as a uniform tiled pattern up close)
   *   4. the original two-stop linear-gradient, unchanged, as the base
   *      tone/lighting direction every theme already tuned.
   * Same recipe reused (different alphas/base colors) on --wfl-tray-bg
   * just below and on both Espresso vars in this theme's own [data-wfl-
   * mode="dark"] block further down. Deliberately NOT applied to the
   * Midnight/Charcoal families below -- those are their own material
   * (slate, brushed graphite), not wood, and Ron's complaint was
   * specifically about the brown "table" this default/Espresso block
   * covers; flagged here as an easy follow-up if he wants the other two
   * themes textured to match. */
  --wfl-chrome-bg:
    linear-gradient(115deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0) 30%),
    repeating-linear-gradient(91deg, rgba(0, 0, 0, 0.10) 0px, rgba(0, 0, 0, 0.10) 1px, transparent 1px, transparent 3px, rgba(255, 255, 255, 0.045) 3px, transparent 4px),
    repeating-linear-gradient(89deg, rgba(0, 0, 0, 0.06) 0px, transparent 18px, rgba(0, 0, 0, 0.06) 20px, transparent 62px),
    linear-gradient(160deg, #6b4a2e, #5a3c24);
  --wfl-chrome-fg: #f0e6d2;
  --wfl-chrome-fg-dim: #d7c9ad;
  --wfl-board-surface: #2f4a3a;
  --wfl-board-grid-line: #20362a;
  --wfl-tray-bg:
    linear-gradient(100deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0) 25%),
    repeating-linear-gradient(91deg, rgba(0, 0, 0, 0.10) 0px, rgba(0, 0, 0, 0.10) 1px, transparent 1px, transparent 3px, rgba(255, 255, 255, 0.045) 3px, transparent 4px),
    repeating-linear-gradient(89deg, rgba(0, 0, 0, 0.06) 0px, transparent 16px, rgba(0, 0, 0, 0.06) 18px, transparent 56px),
    linear-gradient(180deg, #8a6440, #6f4e30);

  /* ---- "paper" tokens -- Settings/Dictionary chrome (Sept 30 2026) ----
   * Sept 30 2026 (bug fix, Ron: "in dictionary page... it seems to be
   * stuck halfway between light mode and dark mode... on settings and
   * dictionary they both behave oddly: some stuff seems to change, some
   * stuff doesn't"): audited every color declaration inside the Setup
   * sheet (.wfl-setup-*), the shared disclosure widget (.wfl-disclosure
   * and everything built on it -- Board layout/Word Placement/Themes/
   * Light-Dark mode/Table Background row items, the switch/stepper/
   * category controls) and the Dictionary sheet's own reuse of that same
   * widget. Every one of them was a bare hex color with NO
   * [data-wfl-mode="dark"] counterpart at all -- unlike the board/tile/
   * chrome variables just above (already fixed an earlier round), these
   * never had a dark half to begin with, so toggling Light/Dark mode had
   * zero effect on them: exactly "stuck halfway," not a subtle contrast
   * bug. These --wfl-paper-* tokens are the fix -- one small set of
   * variables every one of those rules now reads from (see this file's
   * .wfl-setup-*/.wfl-disclosure/etc. rules further down), light values
   * here, dark values in the [data-wfl-mode="dark"] block right below.
   * Named "paper" for the cream Setup-sheet card look these values were
   * literally copied from (#fff/#f4ecdc/#e3d6b8/#3b2b18/#8a7454 etc. --
   * every value here is one already used somewhere in this file before
   * this pass, not a new palette). */
  --wfl-paper-page-bg: #f4ecdc;
  --wfl-paper-card-bg: #fff;
  --wfl-paper-card-active-bg: #f4ecdc;
  --wfl-paper-border: #e3d6b8;
  --wfl-paper-text: #3b2b18;
  --wfl-paper-text-dim: #8a7454;
  --wfl-paper-text-faint: #6b5a3e;
  --wfl-paper-text-faintest: #a3937a;
  --wfl-paper-solid-btn-bg: #8a7454;
  --wfl-paper-switch-track-bg: #ddd0af;
  --wfl-paper-stepper-border: #c9a86a;
  --wfl-paper-texture-placeholder-bg: #cbbfa0;
}
/* Sept 29 2026 (bug fix, Ron: "Light/dark mode: doesn't seem to work"):
 * root cause -- every dark-mode override below is scoped to
 * [data-wfl-theme="X"][data-wfl-mode="dark"] for a NAMED theme family,
 * but the default/"Classic" family (no [data-wfl-theme] attribute at
 * all -- see applyTheme()'s `rootEl.removeAttribute('data-wfl-theme')`
 * for family === 'default') never had a dark-mode rule of its own.
 * Since Classic is the actual default every new session starts on,
 * toggling Light/Dark mode had ZERO visible effect for the overwhelming
 * common case -- not a subtle bug, just never wired for this one theme.
 * Fixed the same way "Classic" already borrows Espresso Light's values
 * (see the base .wordflex-live block's own header comment, "App
 * default... and Espresso -- Light are now the same look on purpose"):
 * this is that same block's dark-mode counterpart, copied verbatim from
 * .wordflex-live[data-wfl-theme="espresso"][data-wfl-mode="dark"] below.
 * Specificity note: this selector (class + 1 attribute) is LOWER
 * specificity than every `[data-wfl-theme="X"][data-wfl-mode="Y"]` rule
 * (class + 2 attributes), so it only ever applies when no named theme
 * attribute is present -- picking a real theme family still wins,
 * regardless of source order. */
.wordflex-live[data-wfl-mode="dark"] {
  --brand: #d97757;
  --wfl-cell-bg: #20241f;
  --wfl-center-bg: #3a2c1a;
  --wfl-tw-bg: #7a3626; --wfl-tw-text: #f0e6d2;
  --wfl-dw-bg: #7a4a40;
  --wfl-tl-bg: #1f3f5c; --wfl-tl-text: #f0e6d2;
  --wfl-dl-bg: #3a5a70;
  /* Sept 29 2026 (board-letter contrast pass, see .wfl-board
   * .wfl-tile-letter's own comment): this pairing measured ~6.85:1 (WCAG
   * relative-luminance formula) -- noticeably lower than this app's
   * other five palettes (which all clear 8-12+:1) because #5c4a2e is a
   * mid-tone brown, not a genuinely dark one. Darkened the tile face
   * toward #4a3a22 and brightened the text toward #f8efd9 (still the
   * same cream hue family as --wfl-chrome-fg, just pushed closer to its
   * own white point) -- new ratio ~9.5:1, in line with the rest. */
  --wfl-tile-bg: #4a3a22; --wfl-tile-border: #7a5a38; --wfl-tile-text: #f8efd9;
  --wfl-pending-bg: #4a3a1f; --wfl-pending-border: #d4a017;
  --wfl-chrome-bg:
    linear-gradient(115deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0) 30%),
    repeating-linear-gradient(91deg, rgba(0, 0, 0, 0.22) 0px, rgba(0, 0, 0, 0.22) 1px, transparent 1px, transparent 3px, rgba(255, 255, 255, 0.03) 3px, transparent 4px),
    repeating-linear-gradient(89deg, rgba(0, 0, 0, 0.12) 0px, transparent 18px, rgba(0, 0, 0, 0.12) 20px, transparent 62px),
    linear-gradient(160deg, #2b1d10, #1c130a);
  --wfl-chrome-fg: #f0e6d2;
  --wfl-chrome-fg-dim: #c9b48d;
  --wfl-board-surface: #0f1a13;
  --wfl-board-grid-line: #081009;
  --wfl-tray-bg:
    linear-gradient(100deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0) 25%),
    repeating-linear-gradient(91deg, rgba(0, 0, 0, 0.22) 0px, rgba(0, 0, 0, 0.22) 1px, transparent 1px, transparent 3px, rgba(255, 255, 255, 0.03) 3px, transparent 4px),
    repeating-linear-gradient(89deg, rgba(0, 0, 0, 0.12) 0px, transparent 16px, rgba(0, 0, 0, 0.12) 18px, transparent 56px),
    linear-gradient(180deg, #3a2717, #2a1c10);

  /* Sept 30 2026 -- the dark half of the --wfl-paper-* tokens declared in
   * the base .wordflex-live block above (see that block's own header
   * comment for the full "stuck halfway" root-cause writeup). One shared
   * set regardless of theme FAMILY (same "Classic is the only family
   * without its own dark block, everyone else falls back to it" shape
   * this selector's own Sept 29 2026 fix already established) -- the
   * cream Setup-sheet card and the Dictionary sheet's reused disclosure
   * widget both read these, so both get a real, checked dark counterpart
   * from this one place. */
  --wfl-paper-page-bg: #241b10;
  --wfl-paper-card-bg: #3a2c1b;
  --wfl-paper-card-active-bg: #4a3a22;
  --wfl-paper-border: #5c4a2e;
  --wfl-paper-text: #f0e6d2;
  --wfl-paper-text-dim: #cbb17e;
  --wfl-paper-text-faint: #b39d72;
  --wfl-paper-text-faintest: #a08a66;
  --wfl-paper-solid-btn-bg: #6b5636;
  --wfl-paper-switch-track-bg: #5c4a2e;
  --wfl-paper-stepper-border: #7a6440;
  --wfl-paper-texture-placeholder-bg: #4a4030;
}
.wordflex-live[data-wfl-theme="espresso"][data-wfl-mode="light"] {
  --brand: #d97757;
  --wfl-cell-bg: #e9e2cf;
  --wfl-center-bg: #f1d9a6;
  --wfl-tw-bg: #c15c46; --wfl-tw-text: #fff;
  --wfl-dw-bg: #d99486;
  --wfl-tl-bg: #3f76b8; --wfl-tl-text: #fff;
  --wfl-dl-bg: #8fb6d9;
  --wfl-tile-bg: #f6e3b4; --wfl-tile-border: #b8895a; --wfl-tile-text: #3b2410;
  --wfl-pending-bg: #fff3cf; --wfl-pending-border: #d4a017;
}
.wordflex-live[data-wfl-theme="espresso"][data-wfl-mode="dark"] {
  --brand: #d97757;
  --wfl-cell-bg: #20241f;
  --wfl-center-bg: #3a2c1a;
  --wfl-tw-bg: #7a3626; --wfl-tw-text: #f0e6d2;
  --wfl-dw-bg: #7a4a40;
  --wfl-tl-bg: #1f3f5c; --wfl-tl-text: #f0e6d2;
  --wfl-dl-bg: #3a5a70;
  /* Sept 29 2026: same contrast fix as the Classic-dark block above
   * (identical original values, same ~6.85:1 measurement) -- see that
   * block's own comment. */
  --wfl-tile-bg: #4a3a22; --wfl-tile-border: #7a5a38; --wfl-tile-text: #f8efd9;
  --wfl-pending-bg: #4a3a1f; --wfl-pending-border: #d4a017;
  /* Sept 28 2026: same wood-grain recipe as the default (Espresso Light)
   * block above, alphas re-tuned for a near-black base -- darker grain
   * (0.22/0.12 vs 0.10/0.06) so the lines stay visible against #2b1d10,
   * dimmer sheen (0.04 vs 0.07) since a bright diagonal highlight reads
   * wrong on a dark-mode surface. */
  --wfl-chrome-bg:
    linear-gradient(115deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0) 30%),
    repeating-linear-gradient(91deg, rgba(0, 0, 0, 0.22) 0px, rgba(0, 0, 0, 0.22) 1px, transparent 1px, transparent 3px, rgba(255, 255, 255, 0.03) 3px, transparent 4px),
    repeating-linear-gradient(89deg, rgba(0, 0, 0, 0.12) 0px, transparent 18px, rgba(0, 0, 0, 0.12) 20px, transparent 62px),
    linear-gradient(160deg, #2b1d10, #1c130a);
  --wfl-chrome-fg: #f0e6d2;
  --wfl-chrome-fg-dim: #c9b48d;
  --wfl-board-surface: #0f1a13;
  --wfl-board-grid-line: #081009;
  --wfl-tray-bg:
    linear-gradient(100deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0) 25%),
    repeating-linear-gradient(91deg, rgba(0, 0, 0, 0.22) 0px, rgba(0, 0, 0, 0.22) 1px, transparent 1px, transparent 3px, rgba(255, 255, 255, 0.03) 3px, transparent 4px),
    repeating-linear-gradient(89deg, rgba(0, 0, 0, 0.12) 0px, transparent 16px, rgba(0, 0, 0, 0.12) 18px, transparent 56px),
    linear-gradient(180deg, #3a2717, #2a1c10);
}
.wordflex-live[data-wfl-theme="midnight"][data-wfl-mode="light"] {
  --brand: #8a6a2e;
  --wfl-cell-bg: #eef1f6;
  --wfl-center-bg: #dfe6f2;
  --wfl-tw-bg: #b06a92; --wfl-tw-text: #fff;
  --wfl-dw-bg: #c7d0e8;
  --wfl-tl-bg: #4a7d94; --wfl-tl-text: #fff;
  --wfl-dl-bg: #a9d4d4;
  --wfl-tile-bg: #e3e7f2; --wfl-tile-border: #a8b2c9; --wfl-tile-text: #23283a;
  --wfl-pending-bg: #fff3cf; --wfl-pending-border: #c9a35a;
  --wfl-chrome-bg: linear-gradient(160deg, #4a5570, #39435c);
  --wfl-chrome-fg: #1c2030;
  --wfl-chrome-fg-dim: #3a3f52;
  --wfl-board-surface: #c7d0e2;
  --wfl-board-grid-line: #9aa6c2;
  --wfl-tray-bg: linear-gradient(180deg, #8a94ab, #6e7890);
}
.wordflex-live[data-wfl-theme="midnight"][data-wfl-mode="dark"] {
  --brand: #c9a35a;
  --wfl-cell-bg: #161a26;
  --wfl-center-bg: #232a3c;
  --wfl-tw-bg: #5a2a4a; --wfl-tw-text: #eef1f8;
  --wfl-dw-bg: #3a4a6a;
  --wfl-tl-bg: #1f4a5c; --wfl-tl-text: #eef1f8;
  --wfl-dl-bg: #2a5a5c;
  /* Sept 29 2026 (board-letter contrast pass, see .wfl-board
   * .wfl-tile-letter's own comment): this pairing measured ~8.4:1 --
   * fine on paper, but still the second-weakest of the six palettes.
   * Reused this SAME theme's own --chrome-fg white (#eef1f8, already
   * established a few lines below as "the white" this palette uses)
   * instead of the warmer cream every other theme's tile text borrows,
   * rather than inventing a new color -- new ratio ~9.2:1. */
  --wfl-tile-bg: #3a3f52; --wfl-tile-border: #c9a35a; --wfl-tile-text: #eef1f8;
  --wfl-pending-bg: #4a3a1f; --wfl-pending-border: #c9a35a;
  --wfl-chrome-bg: linear-gradient(160deg, #171b28, #0f1219);
  --wfl-chrome-fg: #eef1f8;
  --wfl-chrome-fg-dim: #a8b2c9;
  --wfl-board-surface: #0d1420;
  --wfl-board-grid-line: #070a10;
  --wfl-tray-bg: linear-gradient(180deg, #232a3c, #171c2a);
}
.wordflex-live[data-wfl-theme="charcoal"][data-wfl-mode="light"] {
  --brand: #d97757;
  --wfl-cell-bg: #f0efec;
  --wfl-center-bg: #f6ddd2;
  --wfl-tw-bg: #d98a76; --wfl-tw-text: #2a2a28;
  --wfl-dw-bg: #f0c9ba;
  --wfl-tl-bg: #6fa0ac; --wfl-tl-text: #2a2a28;
  --wfl-dl-bg: #bfe0e2;
  --wfl-tile-bg: #ffffff; --wfl-tile-border: #d97757; --wfl-tile-text: #2a2a28;
  --wfl-pending-bg: #fff3cf; --wfl-pending-border: #d97757;
  --wfl-chrome-bg: linear-gradient(160deg, #e4e4e0, #cfcfca);
  --wfl-chrome-fg: #2a2a28;
  --wfl-chrome-fg-dim: #5a5a56;
  --wfl-board-surface: #dcdbd6;
  --wfl-board-grid-line: #b8b7b2;
  --wfl-tray-bg: linear-gradient(180deg, #f2f1ee, #dedcd6);
}
.wordflex-live[data-wfl-theme="charcoal"][data-wfl-mode="dark"] {
  --brand: #d97757;
  --wfl-cell-bg: #1a1f1c;
  --wfl-center-bg: #2e241f;
  --wfl-tw-bg: #7a4438; --wfl-tw-text: #f2f2f0;
  --wfl-dw-bg: #5a3a34;
  --wfl-tl-bg: #2a4a52; --wfl-tl-text: #f2f2f0;
  --wfl-dl-bg: #33565c;
  --wfl-tile-bg: #3a3a38; --wfl-tile-border: #d97757; --wfl-tile-text: #f2f2f0;
  --wfl-pending-bg: #4a3a1f; --wfl-pending-border: #d97757;
  --wfl-chrome-bg: linear-gradient(160deg, #242424, #181818);
  --wfl-chrome-fg: #f2f2f0;
  --wfl-chrome-fg-dim: #a8a8a2;
  --wfl-board-surface: #122019;
  --wfl-board-grid-line: #0a120e;
  --wfl-tray-bg: linear-gradient(180deg, #2e2e2e, #202020);
}

/* Sept 29 2026 (Table Background feature, Ron: "an edge to edge worn
 * leather-background for this app"): a real photo texture, when
 * selected, has to win over EVERY theme's own --wfl-chrome-bg/
 * --wfl-tray-bg gradient above -- .wfl-table-photo is added to rootEl
 * by app-live.js's applyTableBackground() alongside whatever
 * [data-wfl-theme]/[data-wfl-mode] combo is already set, not instead of
 * it (Light/Dark mode and Themes stay independently choosable; a photo
 * texture only replaces these TWO properties, nothing else a theme
 * sets -- tile colors, board felt, accent color are all untouched).
 * `[data-wfl-mode]` is tacked onto the selector purely to MATCH each
 * themed block's own specificity (class + 2 attributes) so this wins on
 * source order (declared after every theme block) rather than needing
 * `!important` -- data-wfl-mode is always present once applyTheme() has
 * run at all (see that function), so this never fails to match for want
 * of that attribute. --wfl-chrome-bg-photo itself is a plain
 * `url("...")` set inline by applyTableBackground() -- background-size/
 * position/repeat still come from .wfl-chrome/.wfl-rack-area's own
 * existing rules (background: var(--wfl-chrome-bg), sized by their
 * normal box), so this only ever swaps WHAT the picture is. */
.wordflex-live.wfl-table-photo[data-wfl-mode] {
  --wfl-chrome-bg: var(--wfl-chrome-bg-photo) center/cover no-repeat;
  --wfl-tray-bg: var(--wfl-chrome-bg-photo) center/cover no-repeat;
}

/* Sept 27 2026 (Ron's layout feedback -- "the resulting dead space...
 * the same app-themed color", "removing the need for what seems to be
 * a triple border we have now: white outermost margin, then
 * table-brown, then felt-green"): app-live.js's mount() now colors
 * #appContentWrapper's own gutter via ctx.chrome.setMarginColor() (see
 * that call's own comment), but #appMainContent -- the shell's content
 * CARD itself, sitting inside that gutter -- still carries its own
 * hardcoded white background + border (index.html's base
 * #appMainContent rule), which isn't reachable by any per-frame chrome
 * setter. Left alone, that would still show as a separate white ring
 * between the now-colored gutter and this app's own wood chrome --
 * the "white outermost margin" Ron is describing. Neutralized here
 * instead, scoped with :has() to only take effect while THIS app is
 * actually the one mounted inside it, so no other app's use of the
 * shell's normal white card is touched. This is an app-side workaround
 * for a real gap, not a shell change -- flagged in mount()'s own
 * comment on setMarginColor() as a real candidate for an actual
 * shell API (e.g. letting marginColor cover the card's own background
 * too) if another app ever wants this same effect.
 *
 * Sept 29 2026 update: that candidate shell API now exists --
 * ctx.chrome.setBackground() (AppShell.js) -- and app-live.js's
 * applyTableBackground() uses it for the new photo-texture options.
 * This rule is UNCHANGED and still load-bearing for the 'classic'
 * option (setBackground(null) leaves no override, so this transparent-
 * background rule is exactly what lets .wfl-chrome's own CSS-gradient
 * wood-table show through, same as before this feature existed) -- not
 * a redundant leftover. */
#appMainContent:has(> .wordflex-live) {
  background: transparent;
  border-color: transparent;
}
/* Sept 28 2026 addendum: this rule still matters now that mount() sets
 * setFullBleed(true) again (app-live.js) -- full-bleed drops the
 * card's margin/border but NOT its background (index.html's
 * `.app-main-content--full-bleed` rule never touches `background`), so
 * without this override a full-bleed #appMainContent would still show
 * its own white fill through its own (now much thinner, but nonzero)
 * padding, around this app's wood chrome. Kept exactly as-is; nothing
 * about it needed to change for full-bleed to come back. */

.wordflex-live {
  width: 100%;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 12px;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  color: var(--ink);
  /* Sept 24 2026 (Ron: the Setup/History sheets were taking over the
   * whole page via a `position: fixed` hack instead of the shell's
   * own chrome APIs): the positioned ancestor for those sheets, which
   * are now `position: absolute` (see .wfl-setup-sheet-wrap below)
   * instead of `fixed`. (History itself is no longer a sheet at all as
   * of Sept 26 2026 -- see .wfl-history-scrub further down -- but
   * Setup still is, and still needs this.) Scoped to this app's own
   * root rather than the viewport, so "full screen" is true
   * because mount() actually asked the shell for the whole screen
   * (setFullBleed/setTitleBarMode('hidden')/setToolbarHidden -- see
   * that call's own comment), not because the sheet paints over
   * wherever the viewport happens to be.
   *
   * Sept 25 2026 (bug fix, Ron: "every screen is wildly broken-
   * feeling, most screens have more content than will fit"): that
   * plan only works if this element's OWN box actually equals the
   * full screen. It never did -- #appMainContent (the shell's content
   * host, see AppShell.js) is a column flexbox and this was its only
   * child with no height of its own, so it sized to its CONTENT
   * (topbar + chrome card), not to the screen. Downstream, every
   * position:absolute;inset:0 sheet inherited that wrong box: too
   * short on some screens (a sheet that doesn't reach the bottom,
   * background visible past its edges), too tall on others (the CTA
   * button and everything below the fold pushed out of reach, only
   * reachable by scrolling the whole PAGE, not the sheet's own body).
   * `height: 100%` here is what makes "the shell gave us the whole
   * screen" and "this element's box IS the whole screen" the same
   * fact -- #appMainContent has a definite height to resolve that
   * percentage against (position:absolute;inset:0 against its own
   * sized ancestor), so this isn't a guess. min-height: 0 stops this
   * being a flex item that refuses to shrink below its content's
   * natural height, which is the other half of the same class of bug
   * (see .wfl-chrome's own min-height: 0 below, for the same reason
   * one level in). */
  height: 100%;
  min-height: 0;
  position: relative;
}
.wordflex-live select,
.wordflex-live input,
.wordflex-live textarea,
.wordflex-live button {
  font: inherit;
}

/* ---- Room & display (Sept 25 2026) ----
 * Moved here from a standalone top bar (see app-live.js's own comment
 * on the move) -- styled to match the rest of the Setup sheet's cream
 * section look (.wfl-setup-player-row/.wfl-setup-add-player, etc.)
 * rather than the app's plain default-shell button/label look, which
 * never matched the felt-and-wood redesign around it. */
/* Sept 28 2026 (Ron: "put 'connect' on the line below the room text
 * field, right-aligned. There's not enough room on my phone to have it
 * on the same line"): was `display: flex; gap: 8px; align-items:
 * flex-end;` (field + button side by side) -- now a column, field on its
 * own full-width line, Connect right-aligned on the line below it via
 * align-self on the button itself rather than align-items here (so a
 * future third child in this row wouldn't unexpectedly inherit
 * right-alignment too). */
.wfl-setup-room-row { display: flex; flex-direction: column; gap: 8px; }
.wfl-setup-room-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--wfl-paper-text-faint);
  min-width: 0;
}
.wfl-setup-room-field input,
.wfl-setup-room-field select {
  width: 100%;
}
.wfl-setup-room-connect {
  align-self: flex-end;
  padding: 7px 12px;
  border-radius: 8px;
  border: none;
  background: var(--wfl-paper-solid-btn-bg);
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: 700;
  flex-shrink: 0;
}
.wfl-setup-room-status {
  margin: 4px 0 0;
  font-size: 0.65rem;
  color: var(--wfl-paper-text-faintest);
}

/* Sept 25 2026 (bug fix, Ron: "every screen is wildly broken-feeling"):
 * this default plain-button look is meant as a FALLBACK for buttons that
 * don't have their own component styling (Connect, Send, Recall,
 * Shuffle, Release seat) -- every purpose-built component below (board
 * cells, rack tiles, chip buttons, mode/layout cards, setup/exit/dict
 * chrome buttons) is a `<button>` too and was silently LOSING its own
 * background/border/radius/padding to this rule, because `.wordflex-live
 * button` (a class + a type selector) is MORE specific than a single
 * class like `.wfl-cell` or `.wfl-tile` -- so this rule won regardless of
 * being declared first, no matter how specific-looking the later rule
 * was. `:where(button)` contributes zero specificity, dropping this back
 * to plain-class specificity so normal source order (this rule first,
 * every component override after it) decides again, as the rest of this
 * file already assumed it would. Do not remove the `:where` -- that's
 * what makes every `.wfl-*` button style below actually apply.
 */
.wordflex-live :where(button) {
  padding: 8px 12px;
  border-radius: 999px;
  border: 1.5px solid var(--brand);
  background: #fff;
  color: var(--brand);
  font-weight: 600;
  cursor: pointer;
}
.wordflex-live :where(button):disabled {
  opacity: 0.4;
  cursor: default;
}

/* Sept 24 2026: the old <details class="wfl-options"> panel is now a
 * hidden, empty legacy anchor (see app-live.js's own comment on it) --
 * the unified Setup sheet below replaces it entirely. */
.wfl-options { display: none; }
.wfl-inline { display: inline-flex; gap: 6px; align-items: center; }
.wfl-toggle { color: var(--slate); }
.wfl-buttons { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.wfl-categories { display: flex; flex-direction: column; gap: 4px; }

/* ---- chrome: the felt/wood card (Sept 24 2026, "core chrome first") ----
 * Wraps the players strip, board and rack tray in one card, matching
 * the Tabletop mockups -- see styles-live.css's theming block up top
 * for where --wfl-chrome-bg/--wfl-board-surface/--wfl-tray-bg come
 * from per theme. Everything inside still works exactly as before
 * (same data-role hooks, same handlers) -- this is a skin, not a
 * behavior change. */
.wfl-chrome {
  position: relative;
  border-radius: 16px;
  /* Sept 25 2026 (bug fix, same root cause as .wordflex-live's own
   * height:100% above): this card is where the History/Dictionary/exit
   * sheets actually live in the DOM (see the HTML -- only the Setup
   * sheet is a direct sibling of .wordflex-live; those three are
   * children of THIS element), so THEIR position:absolute;inset:0 was
   * sized to whatever this card's own content added up to, not the
   * screen -- same bug, one level in. flex:1 claims all the vertical
   * space .wordflex-live has left after the topbar above; min-height:0
   * is required on a flex item before a percentage/flex-based height
   * downstream (or this card's own overflow) can actually take effect,
   * instead of the item refusing to shrink below its content's natural
   * height (the default `min-height: auto` for a flex item). overflow-y
   * lets the card's OWN content (players/board/rack) scroll internally
   * on a short screen rather than pushing the card taller than the
   * space available -- overflow: hidden's job (clipping the card's
   * rounded corners/sheets to its own shape) still holds on the X axis. */
  overflow-x: hidden;
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
  background: var(--wfl-chrome-bg);
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 12px 14px;
}
.wfl-chrome-header {
  /* Sept 25 2026: this only ever held the room name, and Ron's plan
   * for inviting people is a shareable link, not telling someone a
   * room name to type in -- "no matter how I get it to Lisa, I won't
   * need to know the name, and I won't care about the name, and
   * neither will she... that means we can hide the name of the room
   * at the top of the game board." Hidden via CSS rather than deleted
   * from app-live.js/the template: the element and its data-role are
   * still wired up (see chromeTitleEl in mount()) for whenever a real
   * per-player-name display lands, likely via ctx.chrome.setTitleBarExtra()
   * per the comment already on the template markup -- this is a
   * one-line revert if that's wrong. */
  display: none;
}
.wfl-chrome-title {
  flex-grow: 1;
  text-align: center;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--wfl-chrome-fg);
  letter-spacing: 0.02em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wfl-setup-btn {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--wfl-chrome-fg) 35%, transparent);
  background: rgba(0, 0, 0, 0.12);
  color: var(--wfl-chrome-fg);
  font-size: var(--fs-xs);
  font-weight: 700;
  flex-shrink: 0;
}

/* ---- Setup sheet (unified New Game/Options, Sept 24 2026) ----
 * Matches NewGame.dc.html/OptionsMidGame.dc.html in spirit, but (Sept
 * 28 2026, Ron: "Now that we're using the entire shell as our
 * backdrop, let's maximize our real estate") no longer as a centered
 * card floating over a dimmed, still-visible game board -- Settings is
 * its own full top-level screen now. mount()'s enterSettingsMode()
 * calls ctx.chrome.setFullBleed(true), which drops the shell's own
 * card margin/border (index.html's `.app-main-content--full-bleed`),
 * so this wrap's `inset: 0` (position: absolute against
 * .wordflex-live's own position:relative) covers the ENTIRE reclaimed
 * content area edge to edge, not just the space inside that card.
 * Opaque background (no dimmed game board showing through, no gap
 * around the card) and no padding -- .wfl-setup-card below fills this
 * wrap completely instead of sitting centered inside it. Sept 24 2026
 * (Ron, on this covering the shell's OWN title bar/toolbar too): was
 * `position: fixed` against the viewport, which is what caused that --
 * this only needs to cover the app's own root, which IS the whole
 * reclaimed screen at that point, through a supported path rather than
 * an accidental one. The `[hidden]` override rule right below is
 * included from the start here, since author CSS beats the `hidden`
 * attribute's own UA-stylesheet default otherwise. */
.wfl-setup-sheet-wrap {
  position: absolute;
  inset: 0;
  z-index: 10;
  background: var(--wfl-chrome-bg);
  /* Sept 29 2026 (bug fix, Ron: "it seems to be shifted too high...
   * smashed up at the top of the content area with a large-ish gap at
   * the bottom"): this used to be `display: flex;` with NO
   * flex-direction (defaulting to `row`) plus `align-items: stretch;
   * justify-content: stretch;` -- `justify-content: stretch` isn't a
   * real flexbox value (only grid honors "stretch" there; on a flex
   * container it's invalid and simply ignored), and relying on the
   * CHILD's own `width/height: 100%` to fill a row-direction flex
   * parent -- rather than the flex algorithm itself sizing the child --
   * is exactly the kind of percentage-height dependency that can lose
   * to the browser's own height-resolution timing on this project's
   * dynamic-viewport (100dvh), toolbar-in-flex layout (see
   * .wordflex-live's own `height: 100%` comment for the general shape
   * of that class of bug). Explicit `flex-direction: column` plus
   * `.wfl-setup-card` sizing itself with `flex: 1 1 auto; min-height:
   * 0` (below) is the same "grow to fill, don't just assert a
   * percentage" idiom already used successfully for `.wordflex-live`/
   * .wfl-chrome elsewhere in this file, so this card no longer has any
   * way to end up shorter than the space it's actually been given. */
  display: flex;
  flex-direction: column;
}
.wfl-setup-sheet-wrap[hidden] { display: none; }
.wfl-setup-card {
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  max-width: none;
  max-height: none;
  /* Sept 28 2026: no more rounded corners or drop shadow -- those read
   * as "a card sitting on top of something else," which is exactly the
   * layered look this fix removes. This screen now fills its wrap
   * completely, so there's no surrounding surface for a shadow to cast
   * onto or a radius to reveal. */
  border-radius: 0;
  overflow: hidden;
  box-shadow: none;
  background: var(--wfl-chrome-bg);
  display: flex;
  flex-direction: column;
  /* Sept 27 2026 (Ron: "please make all fonts on this screen the 100%
   * size" -- and the horizontal-scroll bug report this screen had at
   * larger text sizes): every --fs-* token (see index.html's :root) is
   * `calc(--font-base * --font-scale * <ratio>)` -- --font-scale is the
   * ONE knob the shell's Text Size setting turns, normally inherited
   * from <html>'s own inline style. Overriding it back to 1 right here
   * makes every --fs-* reference anywhere inside this card resolve at
   * its @100% value regardless of the shell-wide setting, without
   * having to hunt down and hardcode every font-size rule in this
   * section individually. This is also the actual ROOT CAUSE fix for
   * the reported horizontal-scroll bug (the Add-player button being
   * shoved off-screen at larger text sizes) -- see
   * .wfl-setup-add-player's own flex-wrap just below for a second,
   * independent layer of defense against the same symptom. */
  --font-scale: 1;
}
.wfl-setup-body {
  flex-grow: 1;
  /* Sept 25 2026 (bug fix, Ron: "most screens have more content than
   * will fit"): a flex item's default `min-height: auto` resolves to
   * its CONTENT's own height for a scrolling container -- meaning
   * "flex-grow: 1" alone never actually let this shrink below
   * everything it contains, so the parent card's own `overflow:
   * hidden` (see .wfl-setup-card) just clipped whatever didn't fit,
   * silently, instead of this scrolling internally the way the
   * `overflow-y: auto` here always implied it would. This is what
   * made "Deal the Tiles" and the bottom of the board-layout picker
   * unreachable -- they were being rendered, then clipped off, not
   * scrolled to. Same fix as .wordflex-live/.wfl-chrome's own
   * min-height: 0 above, one level deeper. */
  min-height: 0;
  overflow-y: auto;
  margin: 0 10px 10px;
  border-radius: 12px;
  background: var(--wfl-paper-page-bg);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.wfl-setup-section h4 {
  margin: 0 0 8px;
  font-size: 0.6rem;
  font-weight: 700;
  color: var(--wfl-paper-text-dim);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.wfl-setup-divider { border: none; border-top: 1px solid var(--wfl-paper-border); margin: 0; }

/* Sept 27 2026 (Ron: "'players (n) - deal someone in or out at
 * anytime': make it just say 'Players'... make it the same
 * heading-ish font as the 'dictionary required' and 'no bad words'
 * entries"): matches .wfl-switch-row's own label look exactly (same
 * font-size/weight/color), NOT .wfl-setup-section h4's tiny uppercase
 * section-label style used for "Room & display".
 *
 * .wfl-setup-inline-heading (Sept 27 2026, Settings-screen feedback
 * round) is the same rule under a second, more generic name -- Ron
 * asked for the Board Layout/Word Placement disclosure titles to use
 * "the same inline title-ish font as dictionary/extra blanks," i.e.
 * this exact style, not a new one. Kept as one shared ruleset (not two
 * copies) rather than renaming .wfl-setup-players-heading outright,
 * since that class's own markup (the Players section) belongs to a
 * different pass. */
.wfl-setup-players-heading,
.wfl-setup-inline-heading {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--wfl-paper-text);
  margin: 0 0 8px;
}

.wfl-setup-players { display: flex; flex-direction: column; gap: 6px; }
/* Sept 27 2026 (Ron's Settings-screen feedback round -- player rows
 * redesigned): now a real <button>, one tappable table-row element per
 * player -- tapping it shows Rename/Remove as TOOLBAR tools (see
 * app-live.js's selectPlayerRow()), not inline text buttons living in
 * the row itself, so the reset rules below undo <button>'s native
 * appearance (full-width, left-aligned, no default border/font) rather
 * than the old plain wrapper div needing none of this. */
.wfl-setup-player-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  background: var(--wfl-paper-card-bg);
  border: 1px solid var(--wfl-paper-border);
  border-radius: 8px;
  padding: 8px 10px;
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.wfl-setup-player-row:active { background: var(--wfl-paper-card-active-bg); }
.wfl-setup-player-row.is-selected { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, #fff); }
.wfl-setup-player-name { flex-grow: 1; font-size: var(--fs-xs); font-weight: 700; color: var(--wfl-paper-text); }
/* Sept 27 2026 (Ron: "make the word 'you' the same font size as the
 * player name"): was 0.6rem, a fixed step smaller than the name next
 * to it -- now shares .wfl-setup-player-name's own var(--fs-xs). */
.wfl-setup-player-you { font-size: var(--fs-xs); color: var(--wfl-paper-text-faintest); }
/* Sept 27 2026: was two always-visible inline Rename/Remove text
 * buttons per row -- gone now that tapping the row itself surfaces
 * Rename/Remove as toolbar tools instead (see app-live.js's
 * selectPlayerRow(); the Rename/Remove icons themselves are drawn by
 * the shell's own toolbar-button styling, faIcon('pen-to-square')/
 * faIcon('trash')). */
/* Sept 28 2026 (bug fix, Ron: "make 'deal someone in' work the same
 * identical way [as player rename]: just tap it to reveal the system
 * edit dialog... right now it brings up the keyboard and attempts to
 * edit that field in-line, which fails miserably"): was a text input +
 * a separate "Add" button (the Sept 27 2026 flex-wrap/min-width rules
 * below existed only to stop that input+button pair from overflowing at
 * large font sizes -- see the removed comment this replaced); now a
 * single full-width tappable row, .wfl-setup-deal-in-btn, that opens
 * window.prompt() instead. The wrapping .wfl-setup-add-player div stays
 * (still the right "one section-level child, margin-top spacing"
 * container), it just has one child now instead of two. */
.wfl-setup-add-player {
  margin-top: 8px;
}
.wfl-setup-deal-in-btn {
  display: block;
  width: 100%;
  padding: 9px 12px;
  border-radius: 8px;
  border: none;
  background: var(--wfl-paper-solid-btn-bg);
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: 700;
  text-align: center;
}

.wfl-setup-toggles { display: flex; flex-direction: column; gap: 10px; }
.wfl-switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--wfl-paper-text);
}
/* Sept 30 2026: the two new standalone toggle rows (Board follows
 * Light/Dark, and the shared app-restoration toggle) each sit alone at
 * the bottom of their own disclosure section rather than inside a
 * .wfl-setup-toggles stack, so they need their own top spacing instead of
 * inheriting that container's `gap`. */
.wfl-setup-board-follows-row,
.wfl-setup-app-restoration-row {
  margin-top: 12px;
}
/* Sept 24 2026: a real checkbox underneath, styled as the mockup's
 * pill switch via appearance:none -- keeps native checkbox semantics
 * (keyboard, screen readers) instead of a fake button+state pair.
 *
 * Sept 27 2026 (Ron: "toggle buttons next to dictionary, angled
 * placement, etc: please make them about twice as large. big slider
 * toggle buttons the size of toolbar buttons"): every dimension below
 * is exactly doubled from the original 34x20/thumb-16/inset-2 -- the
 * resulting 40px height lands right next to the shell's own toolbar
 * button size (28px icon + 9px padding each side, see index.html's
 * .toolbar-tool-icon svg / .toolbar-tool-btn), matching "the size of
 * toolbar buttons" as asked. */
.wfl-switch-input {
  appearance: none;
  -webkit-appearance: none;
  width: 68px;
  height: 40px;
  border-radius: 999px;
  border: none;
  background: var(--wfl-paper-switch-track-bg);
  position: relative;
  flex-shrink: 0;
  cursor: pointer;
  transition: background 0.15s ease;
}
.wfl-switch-input::after {
  content: '';
  position: absolute;
  top: 4px;
  left: 4px;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  transition: left 0.15s ease;
}
.wfl-switch-input:checked { background: var(--brand); }
.wfl-switch-input:checked::after { left: 32px; }

.wfl-categories-details summary {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--wfl-paper-text-dim);
  cursor: pointer;
}
.wfl-categories-details .wfl-categories { margin-top: 8px; }

.wfl-stepper-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--wfl-paper-text);
}
.wfl-stepper { display: flex; align-items: center; gap: 8px; }
.wfl-stepper button {
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 999px;
  border: 1px solid var(--wfl-paper-stepper-border);
  background: var(--wfl-paper-card-bg);
  color: var(--wfl-paper-text-dim);
  font-weight: 700;
  font-size: 0.7rem;
  line-height: 1;
}
.wfl-stepper span { font-size: var(--fs-xs); font-weight: 700; color: var(--wfl-paper-text); width: 14px; text-align: center; }

/* Sept 30 2026 (item 10 -- dropdown replacing the +/- stepper above):
 * sized just wide enough for a single digit + the native select's own
 * disclosure triangle, so it reads as a compact control next to the
 * "Extra blanks" label rather than stretching to the row's full width
 * the way a bare <select> would by default. */
.wfl-extra-blanks-select {
  width: auto;
  min-width: 56px;
  flex-shrink: 0;
  font-size: var(--fs-xs);
  font-weight: 700;
  padding: 6px 8px;
}

/* Sept 27 2026 (Settings-screen feedback round -- disclosure widget,
 * shared by Word Placement and Board layout, ported (not imported) from
 * manage-repo-files' own <details class="repo-section"> pattern -- see
 * that app's styles.css ".repo-manage .repo-section"/"...summary"/
 * "...-body" rules, which this mirrors: border+radius+padding on the
 * <details>, a padded/pointer-cursor <summary>, a padded body. Plain
 * native <details>/<summary> there too, same reasoning ("free keyboard/
 * accessibility behavior, no JS state to maintain for open/closed") --
 * this app doesn't import that app's CSS (each app's styles.css is its
 * own business per start-here.md's "Three CSS tiers" section), so the
 * pattern is re-declared here under this app's own class names rather
 * than sharing a cross-app selector. */
.wfl-disclosure {
  border: 1px solid var(--wfl-paper-border);
  border-radius: 8px;
  padding: 0 12px;
  background: var(--wfl-paper-card-bg);
}
.wfl-disclosure > summary {
  padding: 10px 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.wfl-disclosure-body { padding-bottom: 12px; }

.wfl-layout-rows, .wfl-placement-rows { display: flex; flex-direction: column; gap: 8px; }
/* Sept 27 2026: was a horizontal-scrolling strip of small square cards
 * (.wfl-layout-card, 64px wide) -- now one full-width tappable row per
 * option (Ron: "put each item in its own row on the screen"), image on
 * the left and name/description on the right.
 *
 * Sept 28 2026 (feedback round #2, Ron: "please remove the literal radio
 * buttons... just make each item a selectable entry, just as you did
 * with the board-layout buttons" -- Word Placement -- and "as with word
 * placement... make just like Board Layout" -- Light/Dark mode): Word
 * Placement and Light/Dark mode's rows used to be a <label> wrapping a
 * real radio input (their own .wfl-placement-row-item rule, with a
 * :has(input:checked) active-state selector) -- both sections are now
 * plain <button>s reusing THIS rule directly, .wfl-layout-row-item, the
 * exact same element/class Board Layout and Themes already use, with the
 * same is-active class toggled from JS instead of a native checked
 * state. .wfl-placement-row-item and its radio-specific rules are gone
 * -- nothing in the markup uses that class any more, so keeping either
 * one around would just be dead CSS the next person has to figure out is
 * safe to delete. */
.wfl-layout-row-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  background: var(--wfl-paper-card-bg);
  border: 1px solid var(--wfl-paper-border);
  border-radius: 8px;
  padding: 10px;
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.wfl-layout-row-item:active { background: var(--wfl-paper-card-active-bg); }
.wfl-layout-row-item.is-active { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, var(--wfl-paper-card-bg)); }
.wfl-layout-row-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.wfl-layout-row-name { font-size: var(--fs-xs); font-weight: 700; color: var(--wfl-paper-text); }
.wfl-layout-row-desc { font-size: var(--fs-xs); font-weight: 500; color: var(--wfl-paper-text-faint); line-height: 1.3; }

.wfl-layout-mini {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 1px;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  /* Sept 30 2026: was a hardcoded #20362a (the DEFAULT theme's own
   * --wfl-board-grid-line, copied once and never revisited) -- these
   * "tiny board" swatches decorate the Settings screen with the real
   * board's own felt/cell colors, so they should track whichever THEME
   * (and, per the new "Board follows Light/Dark" toggle, mode) is
   * actually current, the same as the real board does, not always show
   * the default theme's grid line regardless of what's picked. */
  background: var(--wfl-board-grid-line);
  border-radius: 3px;
  overflow: hidden;
}
/* Sept 27 2026 (Ron: "make it about 4x the current size (2x in each
 * direction)"): 32px -> 64px is exactly that -- 4x the area, 2x each
 * dimension. A modifier, not a change to the base rule, since
 * .wfl-layout-mini's 32px size isn't used anywhere else in this file
 * right now but there's no reason to force every future consumer to
 * this screen's own "huge-ish" size. */
.wfl-layout-mini--lg { width: 64px; height: 64px; }
.wfl-layout-mini-cell { background: var(--wfl-cell-bg); }
.wfl-layout-mini-cell.wfl-p-TW { background: #c15c46; }
.wfl-layout-mini-cell.wfl-p-DW { background: #d99486; }
.wfl-layout-mini-cell.wfl-p-TL { background: #3f76b8; }
.wfl-layout-mini-cell.wfl-p-DL { background: #8fb6d9; }
/* Sept 29 2026 (Super Scrabble's real quadruple squares): darker/more
 * saturated variants of the TW/DL family colors just above, so a
 * quadruple square still reads as "more of the same kind" (word vs.
 * letter) rather than an unrelated new hue. */
.wfl-layout-mini-cell.wfl-p-QW { background: #8a3626; }
.wfl-layout-mini-cell.wfl-p-QL { background: #1f4d7a; }

/* Sept 29 2026 (Board Layout feature -- Standard/Custom sections).
 * Sept 29 2026 (refinements round, Ron: "remove the headings 'standard',
 * 'custom', and 'size'"): the subheading/heading rules that used to sit
 * here (.wfl-board-layout-subheading, .wfl-custom-size-heading) are gone
 * -- deleted outright, not just unused, since nothing in render()'s
 * markup prints those labels any more. */
.wfl-custom-size-row { display: flex; gap: 8px; }
.wfl-custom-size-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  background: var(--wfl-paper-card-bg);
  border: 1px solid var(--wfl-paper-border);
  border-radius: 8px;
  padding: 10px 6px;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.wfl-custom-size-btn:active { background: var(--wfl-paper-card-active-bg); }
.wfl-custom-size-btn.is-active { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, var(--wfl-paper-card-bg)); }
.wfl-custom-size-btn .wfl-layout-row-name { font-size: var(--fs-xs); font-weight: 700; color: var(--wfl-paper-text); }
/* Sept 29 2026: 3x3/5x5/7x7 mini grids evoking relative size (Ron) --
 * bigger box than the layout-row minis so three side by side still read
 * clearly at a glance. */
.wfl-custom-size-mini { width: 40px; height: 40px; }
/* Sept 29 2026 (Board Layout refinements, Ron: "symmetric, asymmetric,
 * and random three square buttons, just like the size buttons... just
 * give them icons"): the Random button reuses .wfl-custom-size-btn
 * directly (see render()'s markup, `data-role="custom-toolbar"`) with a
 * plain glyph in place of a mini-grid -- same 40px box as
 * .wfl-custom-size-mini just above so all three icons (two mini-grids +
 * one glyph) line up at an identical size. */
.wfl-custom-size-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; color: var(--wfl-paper-text-dim); }
.wfl-custom-size-icon svg { width: 24px; height: 24px; fill: currentColor; }

/* The live scaled preview of the actual chosen Custom size -- same felt/
 * grid-line look as the real board (.wfl-board), non-interactive, sized
 * to fit the Settings screen's own width rather than the viewport. */
.wfl-custom-preview-wrap {
  display: flex;
  justify-content: center;
  margin: 10px 0;
}
.wfl-custom-preview {
  display: grid;
  gap: 1px;
  width: min(100%, 260px);
  aspect-ratio: 1;
  /* Sept 30 2026: see .wfl-layout-mini's own updated comment -- same
   * "track the current theme, not always the default" fix. */
  background: var(--wfl-board-grid-line);
  border: 1.5px solid var(--wfl-board-grid-line);
  border-radius: 4px;
  overflow: hidden;
}
/* Sept 30 2026 (bug fix, second/more specific report -- Ron: "tap the
 * scrabble classic button... no colored squares show up in the preview
 * grid," confirmed for every standard AND custom option): this rule used
 * to re-declare the exact same `background: var(--wfl-cell-bg)` the base
 * `.wfl-layout-mini-cell` rule above already sets -- but as a TWO-CLASS
 * descendant selector (`.wfl-custom-preview .wfl-layout-mini-cell`), it
 * has the SAME specificity (0,2,0) as every `.wfl-layout-mini-cell.wfl-p-*`
 * premium-color rule just above it, and because it appears LATER in this
 * file, equal-specificity cascade order made it win every time -- so
 * every premium-colored cell rendered inside `.wfl-custom-preview`
 * (customPreviewHtml()'s output, which is what renderCustomPreview()
 * fills in for BOTH a Standard-layout pick via currentPreviewSizeAndPremium()
 * and a Custom one) was silently painted back to the plain empty-cell
 * color, regardless of its real `wfl-p-TW`/`wfl-p-DW`/etc. class. This is
 * exactly why NO option ever showed color here, standard or custom alike
 * -- this was the one shared preview element every pick renders into.
 * Confirmed this is the real, sufficient fix (not a guess): the rule was
 * fully redundant with the base `.wfl-layout-mini-cell` rule above (same
 * selector's-worth of actual visual intent, "default to the plain cell
 * color"), so deleting it changes nothing for an UNcolored cell and lets
 * the premium-color rules above finally win for a colored one, with no
 * compensating change needed anywhere else. */

/* Sept 29 2026 (Board Layout refinements): the labeled .wfl-custom-toolbar/
 * .wfl-custom-toolbar-btn row (Symmetric/Asymmetric/a lone icon button for
 * Random, plus a second such row for Save/Delete) is gone -- deleted
 * outright, not just unused. Symmetric/Asymmetric/Random are square
 * icon-only buttons now, reusing .wfl-custom-size-btn/.wfl-custom-size-icon
 * directly (see render()'s markup); Save/Delete moved out of this section
 * entirely, into the shell toolbar (see boardLayoutDetailsEl's 'toggle'
 * listener in mount()). */
.wfl-custom-saved-list { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }

/* Sept 27 2026 (Word Placement diagrams): a small board-tile grid built
 * from real .wfl-tile elements (see app-live.js's placementDiagramHtml())
 * at a small scale via the .wfl-tile--mini modifier -- reusing this
 * app's own tile visual language rather than inventing a second one, per
 * Ron's spec. Same dark board-frame background + cream empty-cell color
 * as .wfl-layout-mini/-cell just above, for the same "tiles sitting on a
 * board" read.
 * Sept 29 2026 (Dictionary/Word-Placement split, Ron: "two graphical
 * buttons: straight and angled. No explanatory text... about twice as
 * large as they are now"): both PLACEMENT_OPTIONS entries are 5x5 now
 * (the -7 variant -- used only by the removed "Backwards and Upwards"
 * option -- is gone, not just unused, since nothing renders it anymore).
 * Base size doubled from the previous 64px box to 128px, per Ron's "2x"
 * ask -- see .wfl-placement-btn just below for the two-across, no-text
 * layout these now sit inside instead of a Board-Layout-style row. */
.wfl-placement-diagram {
  display: grid;
  gap: 1px;
  flex-shrink: 0;
  /* Sept 30 2026: see .wfl-layout-mini's own updated comment. */
  background: var(--wfl-board-grid-line);
  border-radius: 4px;
  padding: 2px;
  box-sizing: border-box;
}
.wfl-placement-diagram-5 { width: 64px; height: 64px; grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(5, 1fr); }
.wfl-tile-mini-empty { background: var(--wfl-cell-bg); border-radius: 1px; }
/* Sept 29 2026 (Word Placement -- graphical-only buttons): two square,
 * icon-only buttons side by side, same visual family as
 * .wfl-layout-row-item (border/radius/is-active) but centered content and
 * no text row at all -- Ron: "No more graphics [elsewhere]... no
 * explanatory text. The illustration will explain." The diagram itself
 * doubles in size here (128px, from the 64px .wfl-placement-diagram-5
 * default) via the descendant selector below, which beats
 * .wfl-placement-diagram-5's own same-specificity-class rule regardless
 * of source order (two classes beat one).
 * Sept 30 2026 (bug fix, Ron's screenshot: "the right most tile bleeds
 * off the screen. Please put them on separate rows"): root cause -- this
 * was `flex-direction: row`, putting two 128px-diagram buttons (each
 * ~160px with their own padding/border) side by side in a disclosure
 * body that's only ~300-320px wide on a phone after its own
 * margin/padding (see .wfl-setup-body's own 10px margin + 16px padding
 * and .wfl-disclosure's 12px padding) -- roughly 320px of buttons plus a
 * 12px gap needing to fit in ~280px of actual room, which is exactly a
 * "rightmost tile bleeds off the screen" overflow, not a sizing choice
 * that happened to look bad. Column layout (one full-width row per
 * option, matching every OTHER disclosure section's row shape) is the
 * fix -- each 128px diagram now has the button's own full width to sit
 * in, comfortably clear of any phone-width edge. */
.wfl-placement-rows--graphical { flex-direction: column; gap: 12px; }
.wfl-placement-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--wfl-paper-card-bg);
  border: 1px solid var(--wfl-paper-border);
  border-radius: 8px;
  padding: 16px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.wfl-placement-btn:active { background: var(--wfl-paper-card-active-bg); }
.wfl-placement-btn.is-active { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, var(--wfl-paper-card-bg)); }
.wfl-placement-btn .wfl-placement-diagram { width: 128px; height: 128px; }
/* Sept 28 2026 (bug fix, Ron's screenshot: the mini tile grid rendering
 * huge and overlapping the row's own description text): root cause was
 * a CSS specificity/order collision, not a sizing choice at all --
 * .wfl-tile--mini and the base .wfl-tile rule (further down this file,
 * around the rack/board tile styles) are both single-class selectors of
 * EQUAL specificity, so whichever one is later in the file wins
 * regardless of which one "sounds" more specific. .wfl-tile sets a fixed
 * `width: 40px; height: 40px` (the real rack-tile size) and it comes
 * AFTER this block in source order, so every mini tile
 * (`class="wfl-tile wfl-tile--mini"`, see placementDiagramHtml() in
 * app-live.js) was silently rendering at full 40x40px rack-tile size --
 * seven of those per row blew way past this diagram's 56px box and
 * spilled straight over the label text next to it, exactly what the
 * screenshot showed. Fixed by writing this as the two-class selector
 * `.wfl-tile.wfl-tile--mini` (specificity (0,0,2,0), beats .wfl-tile's
 * (0,0,1,0) regardless of source order -- no reliance on rule ordering
 * surviving the next edit) and sizing each tile to 100%/100% of its own
 * grid cell (this diagram's grid-template-columns/-rows above already
 * gives each cell an equal, exact share of the 56px/45px box) instead of
 * `auto`, which depended on the very stretch behavior .wfl-tile's fixed
 * px size was overriding in the first place. This keeps Ron's real
 * preferred look (an actual mini tile-and-letter diagram, matching Board
 * Layout's own image-plus-text row technique) rather than falling back
 * to the monospace-letters alternative he said would also be
 * acceptable -- the diagram itself was never the problem, only this one
 * cascade collision was. */
.wfl-tile.wfl-tile--mini {
  width: 100%;
  height: 100%;
  min-width: 0;
  border-radius: 1px;
  box-sizing: border-box;
}
/* Sept 30 2026 (bug fix, Ron: "The text in the letters is way too small
 * to read. Please make the text in those tiles the same size as the text
 * on the rest of the Settings page. Or maybe a little bit smaller. But
 * if we can't read the text, we don't understand the illustration."):
 * was `clamp(5px, 1.6vw, 8px)` -- a leftover from when this diagram sat
 * inside a text-and-image ROW (Board Layout's own shape) at the original
 * 64px size; the Sept 29 2026 pass that doubled the diagram to 128px (see
 * .wfl-placement-btn .wfl-placement-diagram above) never revisited this
 * rule, so the glyphs stayed sized for a box half this one's area,
 * reading as illegibly tiny at the new scale. Fixed to `var(--fs-xs)`
 * (13px @100% text size -- the same size Settings-page body text like
 * .wfl-setup-player-name/.wfl-layout-row-name already uses), scaled down
 * very slightly (0.9x, per Ron's "or maybe a little bit smaller") since
 * these are the only tiles in this app sitting inside a fixed-size (not
 * viewport-relative) illustration -- a plain px-based rule, not clamp+vw,
 * is correct here for the same reason .wfl-setup-card already pins
 * --font-scale to 1 for this whole screen. */
.wfl-tile--mini .wfl-tile-letter { font-size: calc(var(--fs-xs) * 0.9); font-weight: 700; }

/* Sept 27 2026 (Settings-screen feedback round, new Light/Dark mode
 * section): the "big pretty icon" for each of the three
 * Light/Dark/System rows -- same 64px footprint as .wfl-layout-mini--lg
 * so all three disclosure sections (Board layout, Light/Dark mode,
 * Themes) read as one consistent row height, but a plain centered icon
 * tile rather than a mini board-tile grid (there's no board-square
 * metaphor to reuse for a light/dark toggle). Cream/board-frame colors
 * echo .wfl-layout-mini/.wfl-placement-diagram's own "tile sitting on a
 * board" look rather than inventing a third visual language. */
.wfl-scheme-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  background: var(--wfl-cell-bg);
  border-radius: 8px;
  color: var(--wfl-paper-text-dim);
}
/* Sept 28 2026: was `.wfl-placement-row-item:has(input:checked)` -- the
 * row is a plain button now (.wfl-layout-row-item.is-active), see that
 * class's own updated comment above. */
.wfl-layout-row-item.is-active .wfl-scheme-icon { color: var(--brand); }

/* Sept 27 2026 (Themes section preview swatches, Ron: "show a preview of
 * each other item"): a small two-chip swatch built from THEME_PREVIEW_
 * COLORS' own real per-family values (see app-live.js's themePreviewHtml())
 * -- same 64px footprint as .wfl-layout-mini--lg/.wfl-scheme-icon above,
 * so all three disclosure sections' rows line up at the same height.
 * 'Classic' (the renamed "app default") has no preview per Ron's spec
 * ("show a preview of each OTHER item") -- themePreviewHtml() returns ''
 * for it, so its row simply has no image at all. */
.wfl-theme-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: 8px;
}
.wfl-theme-preview-chip {
  width: 20px;
  height: 20px;
  border-radius: 999px;
  border: 2px solid rgba(255, 255, 255, 0.6);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}

/* Sept 29 2026 (Table Background feature -- real photo textures, see
 * app-live.js's TABLE_BACKGROUND_OPTIONS/tableBackgroundPreviewHtml()):
 * same 64px footprint as .wfl-theme-preview just above, so this
 * section's rows still line up with Board Layout/Themes. Unlike a theme
 * swatch (flat colors, known at render time), a texture's real preview
 * image isn't known until resolveTableBackgroundUrl() actually resolves
 * it -- background-image is set inline, from JS, once it does (see
 * applyTableBackgroundRows()); until then this is a plain neutral tile
 * (a hint that something's coming, not a broken image icon).
 * --wfl-texture-preview--failed (also set from JS, on a fetch error) is
 * a visibly different, dimmer/hatched state, distinguishing "still
 * loading" from "this one didn't load" without needing a second image
 * asset for an error icon. */
.wfl-texture-preview {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: 8px;
  background-color: var(--wfl-paper-texture-placeholder-bg);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--wfl-paper-border);
}
.wfl-texture-preview--classic {
  /* The original CSS-gradient wood-table look itself, at swatch scale --
   * reuses --wfl-chrome-bg verbatim (same "don't invent a second
   * palette" instinct as themePreviewHtml()) rather than a flat color,
   * so "Classic" previews the actual look it selects. */
  background: var(--wfl-chrome-bg);
  border: 1px solid rgba(0, 0, 0, 0.15);
}
.wfl-texture-preview--failed {
  background-image: none !important;
  background: repeating-linear-gradient(135deg, var(--wfl-paper-border), var(--wfl-paper-border) 4px, var(--wfl-paper-page-bg) 4px, var(--wfl-paper-page-bg) 8px);
}
.wfl-layout-row-item.is-active .wfl-texture-preview {
  border-color: var(--brand);
}

.wfl-setup-footer { padding: 0 10px 14px; flex-shrink: 0; }
.wfl-setup-cta {
  width: 100%;
  padding: 12px;
  border-radius: 999px;
  border: none;
  background: var(--brand);
  color: #fff;
  font-size: var(--fs-sm);
  font-weight: 800;
}

/* ---- History mode (scrub the live board itself, Sept 26 2026) ----
 * Superseding the Sept 24 2026 design below this comment used to
 * describe (a full-screen sheet with its own second, read-only board):
 * see README.md's "History becomes a toolbar mode, not a separate
 * sheet" and app-live.js's "History mode" section. This bar now lives
 * INSIDE .wfl-rack-area, right where .wfl-rack normally sits.
 *
 * Sept 27 2026 (Ron feedback round -- confirmed, not changed: "you
 * suggested you were going to hide my tile rack when you showed the
 * slider. instead, it pushes my tiles up. I think that's ok... Either
 * is fine"): the two are NOT actually mutually exclusive today, despite
 * the comment this replaces -- .wfl-rack (further down this file) has
 * no `[hidden]` counterpart rule, so setting `rackEl.hidden = true`
 * (app-live.js) doesn't hide it: an author-stylesheet class selector
 * (`.wfl-rack { display: flex }`) beats the UA's own `[hidden] {
 * display: none }` at equal specificity. That's what actually grows
 * this tray and pushes the tiles up when History opens, and per Ron's
 * "either is fine" it's being left as-is on purpose -- see the
 * `margin-top` below for the "more room between the two" he asked for
 * given that they do both stay on screen. */
.wfl-history-scrub {
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Sept 27 2026 (Ron: "please put more room between slider and tile
   * rack if you keep both on-screen"): on top of .wfl-rack-area's own
   * 8px inter-child gap, so the bar reads as clearly separate from the
   * tiles above it rather than crowding them. */
  margin-top: 14px;
}
.wfl-history-scrub[hidden] { display: none; }
/* Sept 27 2026 (Ron: "the animation stutters as it tries to slide
 * on-screen. Make it slide a little more slowly, and slide smoothly"):
 * entering History used to just flip [hidden] with no transition at
 * all -- a same-frame pop, not a slide. This animates only opacity and
 * transform (never height/max-height, which would force a layout
 * reflow on every animation frame and reintroduce the exact jank this
 * is fixing) -- see playHistoryScrubEnterAnimation() in app-live.js
 * for where this class gets (re)applied. 380ms/ease-out is
 * deliberately slower and gentler than this file's other UI
 * transitions (the rack-cover handoff slides are 220-260ms, tuned for
 * a quick pass-the-phone handoff, not a leisurely reveal). */
.wfl-history-scrub--enter {
  animation: wfl-history-scrub-slide-in 380ms cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform, opacity;
}
@keyframes wfl-history-scrub-slide-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
/* Sept 24 2026: the mockup's own "dug into the table" channel + tile
 * thumb, adapted to this app's CSS vars instead of hardcoded hex so it
 * still looks right across all six themes -- the track uses the same
 * --wfl-board-grid-line/--wfl-board-surface pair as the felt board
 * itself, and the thumb reuses --wfl-tile-bg/--wfl-tile-border, so a
 * dragged "tile" always matches whatever this theme's tiles look like
 * everywhere else. -webkit-appearance:none/-moz equivalents are
 * required for either browser to accept ANY custom track/thumb
 * styling at all -- without them the native default slider renders
 * and every rule below is simply ignored. */
.wfl-history-channel {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 40px;
  background: transparent;
  margin: 0;
}
.wfl-history-channel::-webkit-slider-runnable-track {
  height: 14px;
  border-radius: 7px;
  background: linear-gradient(180deg, var(--wfl-board-grid-line), var(--wfl-board-surface));
  box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.55), inset 0 -1px 0 rgba(255, 255, 255, 0.05);
}
.wfl-history-channel::-moz-range-track {
  height: 14px;
  border-radius: 7px;
  background: linear-gradient(180deg, var(--wfl-board-grid-line), var(--wfl-board-surface));
  box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.55);
}
/* Sept 27 2026 (Ron: "make the slider thumb as large as a regular
 * tile, and round -- evokes the same idea but a different control"):
 * 40x40 now matches .wfl-tile exactly (see that rule further down),
 * but round (border-radius: 50%) rather than .wfl-tile's rounded
 * square, on purpose -- Ron's own framing is that it should evoke a
 * tile without being mistaken for one. margin-top re-centers the
 * taller 40px thumb on the track's 14px height: -(40-14)/2. */
.wfl-history-channel::-webkit-slider-thumb {
  -webkit-appearance: none;
  margin-top: -13px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--wfl-tile-bg);
  border: 1px solid var(--wfl-tile-border);
  box-shadow: 0 3px 5px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.wfl-history-channel::-moz-range-thumb {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--wfl-tile-border);
  background: var(--wfl-tile-bg);
  box-shadow: 0 3px 5px rgba(0, 0, 0, 0.4);
}
@media (prefers-reduced-motion: reduce) {
  .wfl-history-scrub--enter { animation: none; }
}
.wfl-history-channel-labels {
  display: flex;
  justify-content: space-between;
  margin-top: 6px;
  font-size: 0.55rem;
  color: var(--wfl-chrome-fg-dim);
}

/* ---- Dictionary sheet (real search screen, Sept 24 2026) ----
 * Matches DictionaryLookup.dc.html originally; own search bar instead of
 * a board, a "board ribbon" strip that only shows up when opened by
 * tapping a placed word, and a scrollable results list.
 *
 * Sept 29 2026 (Dictionary redesign, Ron: "use the full screen real
 * estate, just like the board and settings" + "put the title in the
 * system title bar"): this used to be a centered, max-width-420px card
 * floating over a dimmed, still-visible board (`position: absolute;
 * inset: 0` on the WRAP, but the wrap itself only used flex centering +
 * padding to size the card, and the card had its own rounded corners/
 * shadow/max-width). Now a full top-level screen, same recipe as
 * .wfl-setup-sheet-wrap/.wfl-setup-card just above (opaque background,
 * no padding, `flex-direction: column` + the child sizing itself with
 * `flex: 1 1 auto; min-height: 0` rather than a percentage height --
 * see .wfl-setup-sheet-wrap's own comment on why that particular fix
 * matters on this project's dynamic-viewport toolbar-in-flex layout,
 * ported verbatim rather than re-derived). No more own header row
 * either -- mount()'s enterDictionaryMode()/exitDictionaryMode() now put
 * "Dictionary"/"WordFlex" in the system title bar via
 * ctx.chrome.setTitle(), and the Done button is gone (the mode-tool
 * breadcrumb's own onExit closes this now, same as Settings). */
.wfl-dict-sheet-wrap {
  position: absolute;
  inset: 0;
  z-index: 10;
  background: var(--wfl-chrome-bg);
  display: flex;
  flex-direction: column;
}
.wfl-dict-sheet-wrap[hidden] { display: none; }
.wfl-dict-card {
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  max-width: none;
  max-height: none;
  border-radius: 0;
  overflow: hidden;
  box-shadow: none;
  background: var(--wfl-chrome-bg);
  display: flex;
  flex-direction: column;
  --font-scale: 1; /* Sept 29 2026: same Text-Size-setting override as .wfl-setup-card, same reasoning */
}
.wfl-dict-board-ribbon {
  margin: 0 14px 8px;
  padding: 6px 10px;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.15);
  color: var(--wfl-chrome-fg-dim);
  font-size: var(--fs-xs);
  flex-shrink: 0;
}
.wfl-dict-board-ribbon[hidden] { display: none; }
.wfl-dict-board-ribbon b { color: var(--wfl-chrome-fg); }
.wfl-dict-body {
  flex-grow: 1;
  min-height: 0; /* Sept 25 2026 -- same fix as .wfl-setup-body above */
  overflow-y: auto;
  margin: 0 10px 10px;
  border-radius: 10px;
  background: var(--wfl-board-surface);
  padding: 14px;
  display: flex;
  flex-direction: column;
  box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.35);
  /* Sept 30 2026 (bug fix, Ron's screenshot -- "it stays light text on a
   * light background, which I can't read"): the Dictionary sheet reuses
   * the Setup sheet's own .wfl-disclosure/.wfl-switch-row/etc. widgets
   * (see the "Fill from my rack"/"Peek at bad words"/Any order/Sounds
   * like section in the HTML) -- but this sheet is ALWAYS felt-and-wood
   * styled (this element's own --wfl-board-surface background above, and
   * --wfl-chrome-fg text elsewhere on it), regardless of Light/Dark mode,
   * exactly like the real game board is. The one earlier attempt at this
   * (".wfl-dict-toggles .wfl-switch-row { color: var(--wfl-chrome-fg) }"
   * a few rules down) only re-colored the switch-row TEXT to match that
   * felt look -- it never touched the disclosure CARD sitting behind it,
   * which was still the cream Setup-sheet's hardcoded white
   * (--wfl-paper-card-bg's own light value). Light cream text on a white
   * card is exactly "stuck halfway" and exactly unreadable -- and since
   * neither half of that pairing was ever actually keyed off
   * [data-wfl-mode], toggling Light/Dark mode never touched it either
   * ("changing the setting doesn't fix the screen"). Fixed at the root:
   * override the SAME --wfl-paper-* tokens every .wfl-setup-*/
   * .wfl-disclosure rule already reads (see the base theming block's own
   * comment), scoped to just this sheet, to the same translucent-dark
   * treatment .wfl-dict-board-ribbon/.wfl-dict-chip already use elsewhere
   * on this screen -- so the reused widget looks like it belongs on the
   * felt, in every mode, the same way it always should have. */
  --wfl-paper-card-bg: rgba(0, 0, 0, 0.18);
  --wfl-paper-card-active-bg: rgba(0, 0, 0, 0.28);
  --wfl-paper-border: color-mix(in srgb, var(--wfl-chrome-fg) 25%, transparent);
  --wfl-paper-text: var(--wfl-chrome-fg);
  --wfl-paper-text-dim: var(--wfl-chrome-fg-dim);
  --wfl-paper-text-faint: var(--wfl-chrome-fg-dim);
  --wfl-paper-text-faintest: var(--wfl-chrome-fg-dim);
  --wfl-paper-switch-track-bg: rgba(0, 0, 0, 0.3);
  --wfl-paper-stepper-border: color-mix(in srgb, var(--wfl-chrome-fg) 40%, transparent);
  --wfl-paper-texture-placeholder-bg: rgba(0, 0, 0, 0.22);
}
.wfl-dict-searchbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 10px;
  background: var(--wfl-tray-bg);
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.35);
  color: var(--wfl-chrome-fg-dim);
}
.wfl-dict-query {
  flex: 1 1 auto;
  min-width: 0;
  border: none;
  background: transparent;
  font-size: var(--fs-sm);
  color: var(--wfl-chrome-fg);
  outline: none;
}
.wfl-dict-query::placeholder { color: var(--wfl-chrome-fg-dim); }
/* Sept 29 2026 (Dictionary redesign): the search-TYPE <select>
 * (.wfl-dict-mode) that used to sit in the searchbar is gone -- see
 * app-live.js's runDictSearch()/searchRanked() comments. "Fill from my
 * rack"/"Peek at bad words" now live in their own disclosure section
 * (.wfl-setup-section/.wfl-disclosure, the same shared widget Board
 * Layout/Word Placement/Themes use on the Setup screen -- see that
 * section's own header comment; not styled again here). */
/* Sept 29 2026 (Ron: "please put some vertical white space between the
 * search bar, the disclosure area, and the 'no matches' message"): the
 * search options section and the status line each get real breathing
 * room above them now -- previously 0 and 12px respectively, which read
 * as "options glued to the search bar, status barely separated from
 * them." */
.wfl-dict-body > .wfl-setup-section { margin: 16px 0; }
.wfl-dict-chips {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.wfl-dict-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-radius: 999px;
  padding: 5px 12px;
  font-size: var(--fs-xs);
  font-weight: 700;
  /* Sept 30 2026 (bug fix, second/more specific report -- Ron's
   * screenshot: "stays light text on a light background, which I can't
   * read," specifically the "Fill from my rack"/"Peek at bad words"
   * chips): THIS is the gap the Sept 30 `.wfl-dict-body` --wfl-paper-*
   * override (see that rule's own long comment) never actually closed.
   * That pass explicitly audited ".wfl-disclosure/.wfl-switch-row/etc."
   * -- this chip is a THIRD, separate widget rendered inside the same
   * disclosure body, and it was never migrated off its own older,
   * independent "always dark felt chrome" assumption: `color:
   * var(--wfl-chrome-fg)` (the theme's cream text, correct against a
   * genuinely dark backdrop) paired with a border/background computed
   * directly from --wfl-chrome-fg/a flat rgba(0,0,0,0.1) darkening
   * overlay, neither of which is part of the --wfl-paper-* system the
   * rest of this disclosure now reads. So this chip alone kept using
   * the theme's cream text color while sitting on whatever background
   * the card actually has -- unreadable whenever that card isn't dark.
   * Fixed by switching this chip onto the SAME --wfl-paper-* tokens
   * `.wfl-disclosure`/`.wfl-switch-row` already use (which `.wfl-dict-
   * body` already overrides to its own dark-felt-translucent values --
   * see that rule) instead of a second, independent color source that
   * can drift out of sync with it. */
  border: 1px solid var(--wfl-paper-border);
  background: var(--wfl-paper-card-bg);
  color: var(--wfl-paper-text);
}
.wfl-dict-chip.is-active {
  background: var(--brand);
  color: #fff;
  border-color: transparent;
}
/* Sept 29 2026 (Ron: "disable that button until the search matches some
 * bad words"): a real, native `disabled` button (see app-live.js's
 * renderDictResults(), which is what actually flips this based on the
 * room's own blocked-word answer) -- same "native disabled beats a
 * silent no-op" idiom the toolbar's own Send/History buttons already
 * use, just applied to an in-sheet chip instead of a toolbar tool. */
.wfl-dict-chip:disabled {
  opacity: 0.45;
  cursor: default;
}
/* Sept 29 2026 (peek button's warning-triangle icon -- see this
 * button's own template comment in app-live.js for why a slash is
 * drawn here instead of a second FA glyph): the slash is a plain
 * diagonal line drawn across the icon's own box with ::after, not a
 * separate image -- rotate(-45deg) on a square box always crosses
 * corner-to-corner regardless of the triangle glyph's own aspect
 * ratio. */
.wfl-dict-chip-icon {
  position: relative;
  display: inline-flex;
  width: 14px;
  height: 14px;
}
.wfl-dict-chip-icon--slashed::after {
  content: '';
  position: absolute;
  inset: 0;
  border-top: 2px solid currentColor;
  transform: translateY(-1px) rotate(-45deg);
  transform-origin: center;
}
.wfl-setup-toggles.wfl-dict-toggles {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid color-mix(in srgb, var(--wfl-chrome-fg) 20%, transparent);
}
/* Sept 29 2026: the Dictionary sheet's own dark wood/felt chrome doesn't
 * have the Settings sheet's cream-card background .wfl-switch-row/
 * .wfl-switch-input were originally styled against.
 * Sept 30 2026 (superseded, not removed as dead weight -- see
 * .wfl-dict-body's own updated comment): this rule alone only ever
 * re-colored the switch-row TEXT, leaving the disclosure CARD behind it
 * hardcoded white -- the actual "light text on a light background" bug.
 * .wfl-switch-row now reads var(--wfl-paper-text) directly (see that
 * rule further down), and .wfl-dict-body overrides --wfl-paper-text (and
 * every other --wfl-paper-* token) for its whole subtree, so this
 * selector is redundant -- kept, commented out, as a pointer for anyone
 * searching for how this used to be patched, not live CSS.
 * color: var(--wfl-chrome-fg) was equal to var(--wfl-paper-text)'s own
 * new --wfl-dict-body value here, so removing it changes nothing. */
.wfl-dict-status {
  margin: 16px 0 6px;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--wfl-chrome-fg-dim);
}
.wfl-dict-results {
  display: flex;
  flex-direction: column;
  gap: 6px;
  overflow-y: auto;
}
/* Sept 29 2026 (bug fix, Ron: "certain search results make the page
 * start scrolling left to right... when words are long enough, or if
 * they're followed by the 'blocked by room settings' marker"): the same
 * flex min-width:auto trap documented in apps/notes, apps/manage-repo-
 * files, and apps/api's own styles.css -- a flex ITEM defaults to
 * min-width:auto (its own unbroken content's width), not 0, so a long
 * word (or a word plus its blocked-word tag) refused to shrink and
 * pushed .wfl-dict-results, then the whole sheet, wider than the
 * screen. Fixed the same way those apps were: min-width:0 on the row
 * AND on the text span, overflow-wrap so a genuinely long single word
 * can still break rather than overflow.
 *
 * Sept 29 2026 (real <button>, not a <div>): each result row is now a
 * native, full-width, left-aligned, tappable button -- selecting one
 * surfaces the "Report bad word" tool (see app-live.js's
 * selectDictWord()); .is-selected mirrors .wfl-setup-player-row's own
 * selected-row treatment (border + tinted background), same visual
 * language as everywhere else in this app a row can be selected. */
.wfl-dict-word {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  min-width: 0;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid transparent;
  background: var(--wfl-tray-bg);
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.wfl-dict-word.is-selected {
  border-color: var(--brand);
  background: color-mix(in srgb, var(--brand) 22%, var(--wfl-tray-bg));
}
.wfl-dict-word-text {
  min-width: 0;
  overflow-wrap: anywhere;
  font-weight: 800;
  letter-spacing: 0.03em;
  color: var(--wfl-chrome-fg);
}
/* Sept 29 2026 (Dictionary redesign -- combined ranked search): the
 * matched portion of each result word (app-live.js's
 * highlightedWordHtml(), from searchRanked()'s own per-word `matches`
 * indices) -- a plain semantic <mark>, styled here rather than left to
 * the UA default yellow (which doesn't read against this wood/felt
 * theme's own dark chrome background). */
.wfl-dict-word-text mark {
  background: color-mix(in srgb, var(--brand) 55%, transparent);
  color: #fff;
  border-radius: 3px;
  padding: 0 1px;
}
/* Sept 29 2026 (restoring "sounds like" phonetic search, Ron: "let's
 * highlight the phonetic match in a different color"): a rank-
 * RANK_PHONETIC hit has no per-character `matches` to wrap in <mark>
 * (Soundex says "sounds alike," not "these letters correspond" -- see
 * searchRanked()'s own comment in word-check/main.js), so the WHOLE
 * word gets this treatment instead -- a distinct amber/gold tone, kept
 * clearly different from the brand-blue <mark> exact/prefix/substring/
 * subsequence/any-order hits use above, so "why did this show up" reads
 * at a glance even without re-reading which rank tier it was. */
.wfl-dict-word--phonetic .wfl-dict-word-text {
  color: #e0a95c;
}
.wfl-dict-word--blocked { box-shadow: inset 0 0 0 1.5px #c0392b; }
/* Sept 29 2026 (Ron: "a visual way of indicating that something is a
 * bad word. Maybe a warning triangle? That will be easier to read than
 * the tiny text explanation"): replaces the old .wfl-dict-word-tag text
 * label -- a small triangle-exclamation icon, same glyph the peek
 * button and the report-bad-word toolbar tool use, so "this is a
 * flagged word" reads as one consistent visual language across the
 * whole Dictionary sheet rather than a one-off text string. `title`
 * (set in app-live.js) still carries the same explanation for anyone
 * who taps/hovers it -- the information isn't gone, just not
 * permanently printed in tiny caps. */
.wfl-dict-word-flag {
  display: inline-flex;
  flex-shrink: 0;
  color: #e6a4a4;
}

/* Sept 25 2026 (Ron: "please remove the error message at the top. If
 * I'm not allowed to send, disable the send button"): same treatment
 * as the score-preview line in the previous round -- the element and
 * showError()'s text-setting logic both stay in app-live.js (a couple
 * of genuine problems -- room disconnected, dropped a tile on an
 * occupied cell -- still don't have anywhere else to surface yet), but
 * nothing writes it to the screen any more. Send specifically no
 * longer goes through showError() at all now that the toolbar can
 * mark it disabled for real (see renderToolbar()/contextualToolbarTools()). */
.wfl-chrome > .wfl-error { display: none; }
.wfl-chrome > .wfl-summary { color: var(--wfl-chrome-fg-dim); margin: 0; }
.wfl-score-preview { color: var(--wfl-chrome-fg-dim); font-weight: 600; margin: 0 0 4px; }

/* ---- players strip ---- */
/* Sept 25 2026 (Ron: "the screens have basically random buttons all
 * over... a formal scrabble table that happened to have elegantly
 * designed controls"): this used to be a stack of full-width bordered
 * cards, each permanently showing Act-as/Claim/Remove buttons -- real
 * functionality, but shown ALL the time whether or not it was relevant
 * to what you were doing, which is exactly the drift the mockups (esp.
 * Main.dc.html's small floating corner chips: "▶ Ron · 42" / "Lisa · 38
 * · ⊘") were designed against. Now: a thin row of small pill chips --
 * name, score, a ▶ on whoever's turn it is -- and tapping a chip IS
 * "act as this player" (no separate button for the thing a tap on the
 * chip already obviously does). Sept 27 2026 (remote-first redesign):
 * the old Claim/Release links are gone entirely -- who may act for a
 * player is identity/loan based now (see the toolbar's "Pass phone"
 * tool). Removing a player from the game entirely already lives in
 * Setup's own player roster (Rename/Remove there) -- no reason to
 * duplicate that as an always-visible ✕ next to every name here too. */
.wfl-players { display: flex; gap: 6px; flex-wrap: wrap; }
.wfl-player-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 3px 9px;
  cursor: pointer;
  background: rgba(0, 0, 0, 0.15);
  font-size: var(--fs-xs);
  /* Sept 25 2026 (bug fix, Ron: "the name of the players are too
   * low-contrast for me to read -- black on a dark background"): never
   * had its own color, so it fell through to .wordflex-live's
   * `color: var(--ink)` -- the shell's normal dark-on-light body text
   * color, wrong here since this chip row sits on the wood chrome
   * background, not the page background. --wfl-chrome-fg is the light
   * color already used for text elsewhere on that same background
   * (.wfl-chrome-title, .wfl-setup-hint); .wfl-chip-score already
   * inherits correctly once this is set. */
  color: var(--wfl-chrome-fg);
}
.wfl-player-chip.is-turn { border-color: #d97757; }
.wfl-player-chip.is-acting { background: color-mix(in srgb, var(--brand) 30%, rgba(0, 0, 0, 0.15)); }
.wfl-chip-name { font-weight: 600; }
.wfl-chip-score { color: var(--wfl-chrome-fg-dim); }
/* Sept 29 2026 (Ron: "add a pill for 'bag,' which instead of a short
 * score shows the number of tiles left in the bag"): same chip shape as
 * a player (see .wfl-player-chip above), never active/turn-highlighted
 * (it's not a player), just a slightly dimmer background so it still
 * reads as "part of this strip" without being mistaken for someone at
 * the table. Also playBagFlyInAnimation()'s own flight ORIGIN --
 * app-live.js locates it live via `[data-role="bag-chip"]`, not a fixed
 * offset, so this can move (a name wrapping to a second line, a themed
 * font-size change) without the animation needing to know why. */
.wfl-bag-chip { background: rgba(0, 0, 0, 0.22); cursor: default; }

/* ---- board ---- */
/* Sept 25 2026 (bug fix -- Ron: "board does not yet have pinch zoom as
 * in the original... look at how it's done in the original game").
 * This whole block is ported from app.js/styles.css's own
 * .scrabble-board-wrap/.scrabble-board -- that implementation is the
 * reference, not a fresh design: same container-query sizing fix (a
 * fixed `min(100%, 480px)` cap sized the board off the WINDOW's width,
 * which is wrong wherever the wrap's own available box isn't just
 * "the full-width column" -- e.g. once this wrap actually claims its
 * fair share of a short screen instead of being sized by the board's
 * own aspect-ratio, per below), same MIN/MAX_ZOOM, same pinch-anchor
 * math (see mount()'s "board pinch-zoom + pan" section), same
 * will-change-only-during-the-gesture trick for crisp text at rest.
 *
 * Sept 24 2026 (still true): the felt -- a darker panel behind the
 * grid, same idea as the Tabletop mockups' green felt, themed via
 * --wfl-board-surface (see the theming block up top). */
.wfl-board-wrap {
  /* Sept 29 2026 (placement-guide feature, bug 8): the containing block
   * for .wfl-drag-guide-h/-v below -- they're positioned in plain CSS
   * pixels relative to THIS box (not .wfl-board, which pinch-zooms/pans
   * via its own transform), so this needs a real position context. */
  position: relative;
  width: 100%;
  /* Sept 25 2026: this used to just be "as tall as the centered board
   * wants to be" -- flex:1 1 auto + min-height:0 is what lets it
   * actually CLAIM whatever's left of .wfl-chrome's height after the
   * header/players/rack (same min-height:0 flex-shrink story as
   * .wfl-chrome itself, see that rule's own comment), instead of the
   * board's own intrinsic aspect-ratio size deciding how tall this
   * box is. container-type:size turns this into a query container so
   * .wfl-board below can size itself in cqw/cqh -- this wrap's own
   * actual box, on any screen shape -- instead of guessing at the
   * window's dimensions (which is what made the mockups' fixed
   * 390x940 canvas the only aspect ratio this ever actually fit).
   * touch-action:none hands pinch/pan entirely to the JS below rather
   * than letting the browser's own page-zoom/scroll gestures fight it
   * for the same touches. */
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  touch-action: none;
  container-type: size;
  background: var(--wfl-board-surface);
  border-radius: 10px;
  padding: 12px;
  box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.3);
}
.wfl-board {
  display: grid;
  grid-template-columns: repeat(15, minmax(0, 1fr));
  grid-template-rows: repeat(15, minmax(0, 1fr));
  /* min(100cqw, 100cqh, 640px) -- the largest square that fits inside
   * board-wrap's own actual box (see container-type above), capped at
   * 640px so it doesn't get silly on a tablet. Replaces the old
   * `min(100%, 480px)`, which only ever accounted for width. */
  width: min(100cqw, 100cqh, 640px);
  aspect-ratio: 1 / 1;
  gap: 1px;
  background: var(--wfl-board-grid-line);
  border: 1.5px solid var(--wfl-board-grid-line);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
  transform-origin: center center;
  /* Sept 29 2026 (bug 3 belt-and-suspenders): -webkit-user-select/
   * -webkit-touch-callout already sit on .wfl-cell/.wfl-tile themselves
   * (see those rules' own comments -- that's the actual fix, confirmed
   * as the root cause of the flicker/ghost-box bugs too); repeated here
   * on the whole board so nothing inside it (premium labels, tile
   * letters) is selectable even if a future cell/tile-face rule ever
   * stops inheriting it. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* will-change toggles on/off with the actual pinch/pan gesture (see
 * mount()'s pointerdown/endPinchPointer) rather than sitting on
 * permanently -- promoting the board to its own composited layer at
 * ALL times means it can settle back to a resting scale without ever
 * getting cleanly re-rasterized on some browsers, so text stays soft
 * even after the gesture ends. Only asking for the layer while a
 * gesture is actually live keeps the board crisp once it stops. */
.wfl-board--gesture { will-change: transform; }
.wfl-cell {
  position: relative;
  padding: 0;
  border: none;
  border-radius: 0;
  background: var(--wfl-cell-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Sept 25 2026 (bug fix -- Ron: "board tiles have vastly oversized
   * text which doesn't fit the squares"): this was a flat 7px before,
   * which sounds safe but a cell's actual on-screen size is driven by
   * viewport width (see .wfl-board above), not by this font-size.
   *
   * Sept 25 2026 (bug fix, ROUND TWO -- Ron: "the premium squares are
   * using a font that makes no sense -- the letters in the squares are
   * twice as big as the squares in both directions"): the real fix now
   * lives in app-live.js's mount(), not here -- see
   * applyCellFontVar()'s own long comment there for the full story
   * (short version: this board's width comes off a CSS container
   * query, container query LENGTH UNITS can't be used inside
   * font-size at all, and neither can a plain OR an @property-
   * registered custom property that changes later -- all three
   * confirmed directly by testing, not guessed at; a JS-computed,
   * plain-px stylesheet rule rewritten into its own <style> tag is the
   * one mechanism that actually works). This clamp is just the safe
   * FALLBACK for the moment before that JS runs (or if it somehow
   * doesn't) -- ordinary vw, same idea as the original app.js/
   * styles.css version, deliberately overridable: the JS-written rule
   * targets this exact selector with only one extra attribute
   * selector's worth of specificity, so it always wins once it exists. */
  font-size: clamp(7px, 2.4vw, 13px);
  /* Sept 30 2026 (bug fix, Ron: "[the bolder/higher-contrast text] only
   * seems to take effect on the tiles I'm dragging. Please also add to
   * the tiles on the rack, tiles in the board, and premium squares."):
   * this button-level rule is what the premium-square label text
   * (`<span class="wfl-cell-premium">`, in render()'s markup -- it has
   * no dedicated CSS rule of its own, so it always inherits straight
   * from .wfl-cell) actually renders at -- it was never touched by
   * the Sept 29 2026 boldface pass, which only bumped `.wfl-tile-letter`
   * (the LETTER tiles' own text, a different element entirely). Bumped
   * from 700 to 900 here too, for the same "consistent bold treatment
   * everywhere" ask, and the base text opacity raised from 55% to 75%
   * for real added contrast (not just weight) on every premium square
   * that doesn't already set its own fully-opaque text color below
   * (DW/DL/QW/QL warm/cool premium squares don't set --wfl-*-text, only
   * TW/TL do) -- see index.html's own updated comment for the SEPARATE,
   * real root cause of why `.wfl-tile-letter`'s own 900 wasn't visibly
   * bolder anywhere except the actively-dragged tile (a missing Inter
   * font-weight face, not a CSS specificity bug in this file at all).
   *
   * Sept 30 2026 (item 1 -- new Font section): reads the same
   * --wfl-tile-font-weight custom property the board/rack letter rules
   * below do (app-live.js's applyFontSetting() sets it on the root
   * element), falling back to this same 900 default if it's never been
   * set at all (e.g. the very first paint, before mount() runs). This
   * is the premium-square label's own font-weight -- .wfl-cell-premium
   * has no dedicated rule of its own (see this comment's own header
   * above), so it always inherits whatever this rule resolves to.
   * font-variation-settings is set alongside font-weight for the same
   * reason app-live.js's applyCellFontVar() writes real inline values
   * elsewhere in this file: some browsers only honor a variable font's
   * exact intermediate weights (anything not a multiple of 100) through
   * this property, not font-weight, even though both should do the
   * same thing per spec. font-family falls back to `inherit` (i.e.
   * .wordflex-live's own --font-sans, Inter) so an unset property here
   * changes nothing from before this feature existed. */
  font-weight: var(--wfl-tile-font-weight, 900);
  font-variation-settings: 'wght' var(--wfl-tile-font-weight, 900);
  font-family: var(--wfl-tile-font-family, inherit);
  color: color-mix(in srgb, var(--ink) 75%, transparent);
  cursor: pointer;
  /* Sept 29 2026 (bug fixes -- Ron: "when I tap a tile on the board, it
   * flickers" / "I leave a translucent gray box where the tile used to
   * be as I drag" / "a little translucent ghost of a square comes with
   * me" for an already-placed tile): all three turned out to be the SAME
   * thing, confirmed by code trace -- .wfl-cell is a real <button>, and
   * this rule never turned off WebKit's default tap-highlight rectangle
   * (every other tappable element in this stylesheet already does, see
   * e.g. .wfl-layout-row-item below) or the OS's own text-selection
   * long-press. A tap or a drag-start on a cell -- empty, pending, OR
   * committed (committed tiles aren't draggable at all, see
   * boardTileHtml()'s own comment, so touching one falls through to this
   * same button) -- showed the platform's own translucent gray
   * pressed-state box, which is exactly the "flicker"/"ghost square that
   * follows my finger and vanishes on release" Ron reported in three
   * separate bullets, not three separate bugs. -webkit-user-select/
   * -webkit-touch-callout also fixes the "I keep accidentally getting
   * into text-selection mode on the board" report (bug 3) -- same
   * root cause, WebKit's default long-press behavior, on this same
   * class of element. */
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.wfl-cell.wfl-center { background: var(--wfl-center-bg); }
.wfl-cell.wfl-p-TW { background: var(--wfl-tw-bg); color: var(--wfl-tw-text); }
.wfl-cell.wfl-p-DW { background: var(--wfl-dw-bg); }
.wfl-cell.wfl-p-TL { background: var(--wfl-tl-bg); color: var(--wfl-tl-text); }
.wfl-cell.wfl-p-DL { background: var(--wfl-dl-bg); }
/* Sept 29 2026 (Super Scrabble's real quadruple squares, engine.js's
 * QW/QL codes): darkened variants of the existing TW/TL theme colors
 * via color-mix, so every theme automatically gets a QW/QL color
 * without needing its own --wfl-qw-bg/--wfl-ql-bg custom properties
 * added to all six palettes. */
.wfl-cell.wfl-p-QW { background: color-mix(in srgb, var(--wfl-tw-bg) 70%, #000); color: var(--wfl-tw-text); }
.wfl-cell.wfl-p-QL { background: color-mix(in srgb, var(--wfl-tl-bg) 70%, #000); color: var(--wfl-tl-text); }
.wfl-cell.wfl-has-tile { cursor: default; }
/* Sept 30 2026 (new Crosshairs section, "Tracks" style, Ron: "add a pale
 * themed highlight to the rows and columns centering on my drag
 * location. Meaning, the actual grid squares will light up, or dim, or
 * highlight in some theme-appropriate way, so the grid itself provides
 * the crosshairs instead of an overlaid pair of thin lines. Default to
 * tracks."): applied to every real `.wfl-cell` sharing the current drag
 * hotspot's row or column (see updateCrosshairTracks() in app-live.js's
 * mount()). A large-spread INSET box-shadow, not `background`, so it
 * reads as a pale tint layered on top of whatever's already there --
 * the plain cream cell color, OR a premium square's own TW/DW/TL/DL
 * background (`.wfl-cell.wfl-p-*` below) -- rather than needing to fight
 * those same-specificity rules for the `background` property itself.
 * Declared BEFORE `.wfl-cell.wfl-drop-target` just below on purpose: the
 * exact drop-target cell is always also on its own row/column, so both
 * classes land on that one cell together -- drop-target's own (later,
 * stronger) box-shadow/background pair is meant to win there, and does,
 * by plain source order, leaving the pale track tint to read on the
 * REST of that row/column, exactly the "grid squares light up" effect
 * asked for. */
.wfl-cell.wfl-crosshair-track {
  box-shadow: inset 0 0 0 999px color-mix(in srgb, var(--brand) 14%, transparent);
}

/* Sept 25 2026 (Ron: "I never wanted tap-to-place... it wasn't supposed
 * to stick around" -- placement is now drag, ported from app.js's own
 * drag/ghost/drop-highlight system, see mount()'s "drag & drop: tiles"
 * section). Highlights whichever cell is currently under the dragged
 * ghost, tracked as a single element (setDragHoverCell()) rather than
 * scanning the board on every pointermove. */
.wfl-cell.wfl-drop-target {
  box-shadow: inset 0 0 0 3px var(--brand);
  background: color-mix(in srgb, var(--brand) 16%, var(--wfl-cell-bg));
  z-index: 2;
}

/* Sept 29 2026 (new feature, bug 8 -- Ron: "show some sort of
 * crosshairs, like guidelines in PowerPoint... which come in from the
 * edges of the screen (or board or table) and meet at the hotspot of
 * the tile you are dragging"): two thin lines spanning .wfl-board-wrap
 * (declared in app-live.js's render(), siblings of .wfl-board so they
 * never rotate/scale with the board's own pinch-zoom transform),
 * updateDragGuides() in mount() positions them to cross exactly at
 * whichever cell is the CURRENT valid drop target and toggles their
 * [hidden] attribute -- shown only while a drag is hovering a real,
 * unoccupied cell, hidden the instant the drag ends or moves off the
 * board/onto an occupied cell. pointer-events:none so they never
 * intercept the drag itself.
 * Sept 30 2026 (Crosshairs section): this is now the "Lines" style,
 * one of two -- the new "Tracks" style (`.wfl-cell.wfl-crosshair-track`
 * above) is the default; updateDragGuides() only ever drives ONE of the
 * two at a time, per whichever is picked in the new Settings section. */
.wfl-drag-guide {
  position: absolute;
  pointer-events: none;
  z-index: 3;
  background: color-mix(in srgb, var(--brand) 70%, transparent);
}
.wfl-drag-guide-h { left: 0; right: 0; height: 2px; }
.wfl-drag-guide-v { top: 0; bottom: 0; width: 2px; }

/* Sept 30 2026 (Crosshairs section -- Settings preview swatches for the
 * two styles, see app-live.js's crosshairPreviewHtml()): "Tracks" reuses
 * Board Layout's own .wfl-layout-mini/-cell grid, tinting the center row
 * and column with a slightly stronger version of the real
 * `.wfl-crosshair-track` tint (a flat color-mix, since this preview
 * isn't itself a real `.wfl-cell` with a premium background to layer
 * over) so the swatch reads clearly at 64px. "Lines" is a small custom
 * crosshair glyph -- two thin brand-colored bars crossing at the center
 * of a .wfl-scheme-icon-sized box, matching the REAL guide lines'
 * own color (`.wfl-drag-guide`'s `color-mix(...) 70%`) at preview scale. */
.wfl-crosshair-preview-track { background: color-mix(in srgb, var(--brand) 45%, var(--wfl-cell-bg)); }
.wfl-crosshair-preview-lines { position: relative; }
.wfl-crosshair-preview-line {
  position: absolute;
  background: color-mix(in srgb, var(--brand) 70%, transparent);
}
.wfl-crosshair-preview-line--h { left: 8px; right: 8px; top: 50%; height: 2px; transform: translateY(-50%); }
.wfl-crosshair-preview-line--v { top: 8px; bottom: 8px; left: 50%; width: 2px; transform: translateX(-50%); }

/* Sept 30 2026 (new Font section, item 1 -- Ron: "a live preview, a
 * sample letter tile and a sample premium square, above a weight
 * slider"): the preview row's two boxes are standalone (not real board/
 * rack elements, so they don't need a grid or rack flex context to
 * size correctly) but each wraps the REAL .wfl-tile-letter/.wfl-cell-
 * premium span -- see app-live.js's render()-time comment on this
 * section for why that matters (the preview can never drift out of
 * sync with a real tile, since both read the same two custom
 * properties). Sized/bordered to read as an actual tile/premium square
 * at a glance, not just floating text. */
.wfl-font-preview-row {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 14px;
}
.wfl-font-preview-tile {
  position: relative;
  width: 48px;
  height: 48px;
  border-radius: 6px;
  background: var(--wfl-tile-bg);
  border: 1px solid var(--wfl-tile-border);
  color: var(--wfl-tile-text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* .wfl-cell is reused verbatim (same class as every real board square)
 * so this preview reads the exact same --wfl-tile-font-weight/-family
 * properties through the exact same selector real gameplay does --
 * only width/height/border-radius are added here, since a real
 * .wfl-cell normally gets its size from the board's own CSS grid, which
 * this standalone preview box isn't part of. */
.wfl-cell.wfl-font-preview-premium {
  width: 48px;
  height: 48px;
  border-radius: 6px;
  flex-shrink: 0;
}
.wfl-font-weight-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 6px;
}
.wfl-font-weight-readout {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--brand);
  min-width: 3ch;
  text-align: right;
}
/* Sept 30 2026 (item 1, Ron: "a weight slider the same way the Text
 * Size app's own slider works"): ported the SHAPE of apps/text-size's
 * own slider (large custom thumb via appearance:none, a thin themed
 * track) under this app's own class names and color tokens -- not a
 * shared stylesheet or copy-pasted selector, per start-here.md's "each
 * app owns its own styles.css" rule, since the two sliders control
 * unrelated things and would otherwise be coupled by nothing more than
 * an accidental shared class name. Smaller than text-size's 64px thumb
 * (this one sits inside a Settings disclosure body, not a full-screen
 * panel) but keeps the same "much bigger than the native default"
 * reasoning -- a precise drag target for a value that's read closely,
 * right next to its own live numeric readout above. */
.wfl-font-weight-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 44px;
  margin: 0 0 16px;
  background: transparent;
  cursor: pointer;
}
.wfl-font-weight-slider::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: var(--wfl-paper-border);
}
.wfl-font-weight-slider::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--wfl-paper-border);
}
.wfl-font-weight-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 36px;
  height: 36px;
  margin-top: -15px;
  border-radius: 50%;
  background: var(--brand);
  border: 3px solid #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.wfl-font-weight-slider::-moz-range-thumb {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--brand);
  border: 3px solid #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
/* Sept 30 2026 (item 1): the family list's own per-row preview swatch --
 * same 64px .wfl-scheme-icon footprint as Crosshairs/Light-Dark mode's
 * own rows (see that class's shared header comment), rendering the
 * literal text "Aa" in each option's own real cssFamily (set inline
 * from app-live.js's FONT_FAMILY_OPTIONS, per-row, since each row shows
 * a DIFFERENT family -- a single shared custom property can't do that).
 * Bold and full-opacity (unlike .wfl-scheme-icon's own dimmed default)
 * since the letterforms themselves, not an icon's presence/absence, are
 * the entire point of this particular swatch. */
/* Carries BOTH .wfl-scheme-icon (the shared 64px footprint/active-state
 * color rule, line ~1255 above -- `.wfl-layout-row-item.is-active
 * .wfl-scheme-icon { color: var(--brand) }` already covers this swatch
 * too, nothing extra needed here for that) and this modifier, which
 * only overrides the dimmed default color/weight/size for the literal
 * "Aa" text these two letters actually are. */
.wfl-font-family-swatch {
  font-size: 22px;
  font-weight: 600;
  color: var(--wfl-paper-text);
}

.wfl-board-tile {
  position: relative;
  width: 92%;
  height: 92%;
  border-radius: 3px;
  background: var(--wfl-tile-bg);
  border: 1px solid var(--wfl-tile-border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--wfl-tile-text);
  /* Sept 29 2026 (bugs 1/3, belt-and-suspenders): set directly here, not
   * just inherited from .wfl-cell/.wfl-board -- startTileDrag() (see
   * app-live.js) reparents a dragged tile's real element to
   * document.body while it's lifted, which takes it OUT from under
   * those ancestors' inherited user-select/tap-highlight, so a
   * mid-drag long-press on the lifted tile itself needs its own rule. */
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.wfl-board-tile.wfl-pending-tile { background: var(--wfl-pending-bg); border-color: var(--wfl-pending-border); }
/* Sept 25-26 2026: draggable tiles (rack tiles + your own not-yet-sent
 * board tiles), ported from app.js's .scrabble-tile-draggable/
 * -dragging-source/-ghost lineage. As of Sept 26 2026 the dragged
 * element is the REAL tile itself (see app-live.js's startTileDrag()),
 * detached from its rack/board slot and reparented to document.body --
 * not a cloned "ghost" the way it briefly was Sept 23-25 (see
 * wordflex/README.md's "Notes for future sessions" on why that
 * distinction matters and isn't just a rename). Sized to the tile's
 * NATURAL zoom=1 dimensions and scaled via its own transform to match --
 * see onTilePointerDown()'s header comment on why that avoids the "big
 * box, tiny mis-scaled letter" bug the naive version has. */
.wfl-tile-draggable { cursor: grab; touch-action: none; }
/* Sept 26 2026: the tile's OLD slot while it's lifted -- rendered by
 * renderRack()/renderBoard() as a normal tile plus this class, so the
 * layout footprint (rack button width, board cell) stays exactly the
 * same and nothing around it reflows. Invisible rather than dimmed:
 * there's nothing left showing there to dim -- the one real tile is
 * off flying with the finger, not duplicated. */
.wfl-tile-drag-placeholder { visibility: hidden; }
.wfl-tile-lifted {
  position: fixed;
  z-index: 500;
  pointer-events: none;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
  /* Sept 30 2026 (item 3 -- drag-size scaling, Ron: "as it nears the
   * board it should smoothly grow... reverse when dragged back toward
   * the rack"): bumped from 0.08s -- `transform` on this element is
   * ONLY ever a `scale(...)` (position is `left`/`top`, set directly,
   * never part of this transition), so lengthening it only affects how
   * the SIZE change reads, not how closely the tile tracks the finger.
   * app-live.js's onDragMove() now writes a genuinely different
   * liftScale as the drag crosses in and out of isNearBoard()'s zone
   * (previously it was computed once at pickup and never changed, so
   * this transition had nothing to actually ease between) -- long
   * enough to read as a deliberate resize, short enough to keep up with
   * a drag that's crossing the near-board boundary repeatedly. */
  transition: transform 0.18s ease-out;
  /* Sept 25 2026 (bug fix, Ron: "I can't see the tile when I drag...
   * please show a transparent tile as i drag it"): this used to be
   * fully opaque AND sized/positioned to exactly match the tile's real
   * on-screen size, which put it directly under the fingertip --
   * invisible until moving fast enough for lag to peek it out from
   * under you. app-live.js's startTileDrag()/onDragMove() now also
   * float it a fixed number of pixels above the actual touch point and
   * floor its size so it reads at a glance.
   *
   * Follow-up same day, twice over: first pass used element-level
   * `opacity: 0.86`, which Ron read as glassy -- opacity fades the
   * background, border, AND letter/value text uniformly, which is
   * exactly what makes something look like a pane of glass rather than
   * a solid, slightly-faded object. Dropping the number to 0.5 (his
   * next ask) made that worse, not better -- Ron: "it's currently
   * basically invisible... I think it may not be getting a fill color
   * at all." No CSS rule here ever stopped setting a background --
   * .wfl-tile's own `background: var(--wfl-tile-bg)` still applies --
   * but at 0.5 element-level opacity, THAT fill is faded by half on top
   * of already being a light color, on top of whatever the board/rack
   * surface behind it looks like; entirely plausible that reads as "no
   * fill" on some themes/screens even though a fill genuinely is there.
   * Real fix: no element-level opacity at all any more -- full-strength
   * border, box-shadow, and letter/value text, with ONLY the background
   * itself made translucent (so it still reads as a solid object
   * sitting on the board, just intentionally faded, not "glass"). The
   * actual color is set inline from startTileDrag() (see its own
   * comment) rather than as a CSS declaration here, for the same reason
   * position had to move inline earlier this session: a plain
   * class-based override of `background` would be fighting the exact
   * same .wfl-tile-vs-.wfl-tile-lifted cascade/specificity trap that
   * caused the position bug, and this bug fix shouldn't quietly reopen
   * that door. */
}
/* Sept 25 2026 (same bug fix as .wfl-cell above, same source: app.js/
 * styles.css) -- clamp+vw for now, same safe-fallback-until-JS-runs
 * story as .wfl-cell's own font-size (see its comment -- the real,
 * board-size-aware value comes from app-live.js's applyCellFontVar(),
 * which targets these same two classes by a more specific selector),
 * and the -8% nudge (ported verbatim) leaves the bottom-right corner
 * clear for the point value instead of the two overlapping
 * dead-center. */
/* Sept 29 2026 (Ron: "for all the letters on the board, let's make them
 * much more boldface and/or high contrast -- my wife said so" -- a
 * screenshot of the NYT Crossplay app was the reference: heavy,
 * near-black glyph weight and stark white-on-navy tile faces). Reading
 * that reference against this app's own six palettes: most of them
 * already have strong raw luminance contrast between --wfl-tile-text
 * and --wfl-tile-bg (the default/Espresso-Light pairing alone works out
 * to roughly 12.5:1, Charcoal-Light's white-tile/near-black-text is
 * higher still) -- what Crossplay actually reads as "bolder" is
 * primarily the glyph WEIGHT (a true black/900 face, not this app's
 * previous 700), so that's the change applied everywhere, board and
 * rack alike (see .wfl-tile-letter below). Contrast itself only got a
 * few themes' worth of real color tuning -- Espresso/Classic Dark and
 * Midnight Dark measured out closer to 7-8:1 (see this rule's own theme
 * blocks up top, and that file's own updated comments) rather than the
 * 12+:1 everything else already had, so those two got their
 * --wfl-tile-bg/--wfl-tile-text values nudged further apart; the other
 * four palettes' tile colors are untouched (already high-contrast, and
 * changing a color Ron already picked without a specific complaint
 * about it felt like the wrong call here). */
/* Sept 30 2026 (item 1 -- new Font section): font-weight/font-family
 * now read from the same --wfl-tile-font-weight/--wfl-tile-font-family
 * custom properties applyFontSetting() sets on the root element (see
 * .wfl-tile-letter's own matching comment below, and .wfl-cell's own
 * just above, for the full reasoning -- all three rules read the same
 * two properties so a font change applies everywhere at once). Falls
 * back to 900/inherit (Inter) if never set, matching this rule's
 * existing hardcoded value exactly -- no visual change for anyone who
 * never opens the new Font section. */
.wfl-board .wfl-tile-letter { font-weight: var(--wfl-tile-font-weight, 900); font-variation-settings: 'wght' var(--wfl-tile-font-weight, 900); font-family: var(--wfl-tile-font-family, inherit); font-size: clamp(11px, 4vw, 20px); line-height: 1; transform: translate(-8%, -8%); }
.wfl-board .wfl-tile-value { position: absolute; right: 1px; bottom: 0px; font-size: clamp(6px, 1.8vw, 11px); }

/* ---- rack ---- */
/* Sept 24 2026: the wood tray, same idea as the felt board above --
 * themed via --wfl-tray-bg. Action buttons inside it get a lighter,
 * outlined treatment (readable on wood) rather than the app's default
 * white pill, which would look like a paper scrap glued onto wood. */
.wfl-rack-area {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex-shrink: 0;
  background: var(--wfl-tray-bg);
  border-radius: 10px;
  padding: 10px 10px 8px;
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.15);
  /* Sept 27 2026 ("pass the phone" cover, below): .wfl-rack-cover is
   * position:absolute against THIS box, not the whole screen -- Ron's
   * spec is "cover the current device-holder's own tile rack," not a
   * full-screen sheet like Setup/Dictionary/History. */
  position: relative;
}
.wfl-rack-area > .wfl-score-preview { color: var(--wfl-chrome-fg); }
/* Sept 25 2026 (Ron: "the screens have basically random buttons all
 * over... it was a formal scrabble table that happened to have
 * elegantly designed controls"): this tray used to show five buttons
 * at once, always (Send/Recall/Shuffle/Select-tiles-to-swap/Swap
 * selected), most of them irrelevant to whatever you were actually
 * doing. Matching Tabletop.dc.html's own tray -- exactly ONE contextual
 * pair, meaning changes with what's true right now instead of more
 * controls appearing alongside: pending tiles on the board -> Send +
 * Recall; nothing pending -> Shuffle + Swap; mid tile-swap-selection ->
 * Swap N tiles + Cancel. See renderActionRow() in mount(). */
.wfl-rack-area .wfl-buttons { flex-wrap: nowrap; }
.wfl-rack-area .wfl-buttons button {
  border-color: color-mix(in srgb, var(--wfl-chrome-fg) 45%, transparent);
  background: rgba(0, 0, 0, 0.15);
  color: var(--wfl-chrome-fg);
}
.wfl-rack-area .wfl-buttons button.wfl-btn-primary {
  flex: 1;
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
  font-weight: 700;
}
.wfl-rack-area .wfl-buttons button:disabled { opacity: 0.45; }
/* Sept 29 2026 (bug 3 belt-and-suspenders, same reasoning as .wfl-board
 * above): the rack's own tile letters/values, covered too. */
.wfl-rack {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
  border-radius: 8px;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.wfl-rack.wfl-drop-target { box-shadow: inset 0 0 0 2px var(--brand); }
/* Sept 24 2026: tile text now comes from --wfl-tile-text (see the
 * theming block up top), not var(--ink) -- var(--ink) goes near-white
 * in the shell's own dark mode, which used to go illegible against a
 * light tan tile face (styles-test.css has the same note). Each theme
 * sets --wfl-tile-text to whatever actually reads against ITS tile
 * color, light or dark. */
.wfl-tile {
  color: var(--wfl-tile-text);
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 6px;
  background: var(--wfl-tile-bg);
  border: 1px solid var(--wfl-tile-border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  /* Sept 29 2026 (same bug fixes as .wfl-cell above -- rack tiles are
   * buttons too): the rack side of bugs 1/3's own root cause. */
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.wfl-tile.is-blank { background: var(--wfl-pending-bg); border: 2px solid var(--wfl-pending-border); }
.wfl-tile.is-selected { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Sept 29 2026 (Swap-mode clarity, Ron: "what does the '<->' icon do?
 * ... I can't tell why [the tools changed]"): see renderRack()'s own
 * comment in app-live.js for the reasoning -- reuses the shell's own
 * "selectable, not yet chosen" convention (index.html's
 * `.toolbar-tool-btn--mode:not(.is-active)`), just on the tiles instead
 * of a toolbar button. `:not(.is-selected)` so a tile the player HAS
 * tapped shows the existing solid `.is-selected` outline above instead
 * of fighting with this one.
 * Sept 29 2026 (Ron: "I don't like that dashed-outline convention...
 * I had hoped it would be something we could easily turn off later"):
 * the outline style now follows the shell's own
 * `--selectable-affordance-style` variable (index.html), defaulting to
 * `solid` -- flip that one variable to `dashed` to restore the
 * original look everywhere at once, here included. The background tint
 * is what actually carries the "you can tap this" signal at the
 * default (solid-outline) setting, matching the tint the shell's own
 * mode buttons use for the same purpose. */
.wfl-rack--swap-select .wfl-tile:not(.is-selected) {
  outline-style: var(--selectable-affordance-style, solid);
  outline-width: 2px;
  outline-color: var(--brand);
  outline-offset: 2px;
  background: rgba(59, 111, 214, 0.08);
}
/* Sept 29 2026 (rack recentering bug fix -- see app-live.js's
 * "rendering: rack (fixed slots)" section header comment for the full
 * story): a played/swapped-away tile's slot renders one of these
 * instead of just vanishing -- same footprint as a real .wfl-tile (see
 * .wfl-tile's own width/height above), invisible, so every OTHER tile's
 * flex position in .wfl-rack never shifts. Plain `display: inline-block`
 * (not `visibility: hidden` on a real tile, which .wfl-tile-drag-
 * placeholder already uses for a different case) since there's no
 * underlying tile markup here to hide -- this genuinely is nothing. */
.wfl-tile-slot-empty { display: inline-block; width: 40px; height: 40px; }
/* Sept 29 2026 (Ron: "when I press the shuffle button, please animate
 * the shuffle"): a quick, staggered wiggle (see playShuffleAnimation())
 * rather than a real physics simulation -- flourish, not a literal
 * depiction of tiles swapping. `--wfl-shuffle-delay` is set inline, per
 * tile, right before this class is added. Skips under prefers-reduced-
 * motion (media query at the bottom of this rule) same as every other
 * animation in this file. */
.wfl-tile--shuffle {
  animation: wfl-tile-shuffle 380ms ease-in-out;
  animation-delay: var(--wfl-shuffle-delay, 0ms);
}
@keyframes wfl-tile-shuffle {
  0% { transform: translateY(0) rotate(0deg); }
  25% { transform: translateY(-9px) rotate(-9deg); }
  55% { transform: translateY(5px) rotate(7deg); }
  80% { transform: translateY(-3px) rotate(-3deg); }
  100% { transform: translateY(0) rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .wfl-tile--shuffle { animation: none; }
}
/* Sept 25 2026: same class of bug as the board's own tile text above --
 * `em` here was relative to .wordflex-live's inherited font-size
 * (var(--fs-sm)), which moves with the shell's Text Size setting while
 * the 40px tile box these sit inside does not. clamp+vw decouples it,
 * same recipe as the board (rack tiles are read close-up, so this
 * clamp's own ceiling is deliberately bigger than the board's). */
/* Sept 29 2026 (same boldface/contrast pass as .wfl-board .wfl-tile-letter
 * above -- Ron: "consider whether the same treatment should extend to
 * rack tiles for consistency": yes -- a rack tile is the same physical
 * "letter tile" object as a board tile, just held in your hand rather
 * than played, so a different weight between the two would read as an
 * inconsistency once the board's own tiles got noticeably bolder). */
/* Sept 30 2026 (item 1 -- new Font section): same --wfl-tile-font-
 * weight/--wfl-tile-font-family custom properties as .wfl-board
 * .wfl-tile-letter/.wfl-cell above -- this is the rack tile's own
 * (and, via the ancestor-scoped ...[data-role="board"]/.wfl-board
 * rules above, the fallback for a lifted/reparented tile's) letter
 * rule, so a font change applies to a tile in your hand exactly the
 * same way it applies to one on the board. */
.wfl-tile-letter { font-weight: var(--wfl-tile-font-weight, 900); font-variation-settings: 'wght' var(--wfl-tile-font-weight, 900); font-family: var(--wfl-tile-font-family, inherit); font-size: clamp(16px, 6vw, 24px); }
.wfl-tile-value { position: absolute; right: 3px; bottom: 1px; font-size: clamp(7px, 2vw, 10px); }

/* ---- "pass the phone" rack cover (Sept 27 2026) ----
 * Reuses the tray's own wood-embossed language rather than a
 * mismatched one-off dialog (Ron's explicit standing note on this
 * project) -- same --wfl-tray-bg the rack itself sits on, an inset
 * shadow for the "embossed into the wood" carved look, and the same
 * --brand/--wfl-chrome-fg pair every other sheet's primary button and
 * body text already use. Positioned absolute against .wfl-rack-area
 * (see that rule's own comment) -- covers ONLY the rack tray, not the
 * board or the rest of the screen, matching the spec's "cover the
 * current device-holder's own tile rack." */
.wfl-rack-cover {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px;
  border-radius: 10px;
  background: var(--wfl-tray-bg);
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.45), inset 0 -1px 0 rgba(255, 255, 255, 0.06);
}
.wfl-rack-cover[hidden] { display: none; }
.wfl-rack-cover-inner {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
}
/* Sept 29 2026 (bug fix, Ron: "the cover-plate text with my name on it
 * isn't designed well -- it's not readable (a dark blue against a dark
 * brown background), and it's not a consistent font"): root-caused to a
 * specificity collision, not a wrong color value -- this rule already
 * said `color: var(--wfl-chrome-fg)` (the correct light cream), but
 * index.html's global `.app-shell p { color: var(--slate); font-size:
 * var(--fs-xs); margin: 0; }` (one class + one type = higher
 * specificity than this rule's single class) was winning regardless of
 * source order, since a `<p>` tag is what this text actually is. That's
 * `--slate` (#5b6478, a grey-blue) against the wood tray -- exactly
 * Ron's "dark blue" -- and `--fs-xs` instead of this rule's own
 * `--fs-sm`, which is what read as "not a consistent font" (a visibly
 * different size, even though the family itself was always inherited
 * correctly from .wordflex-live's own --font-sans). Fixed by qualifying
 * the selector with its own `.wfl-rack-cover` ancestor (two classes),
 * which outranks `.app-shell p` the same way `.wfl-rack-area .wfl-buttons
 * button` elsewhere in this file already beats a stray tag-level rule --
 * not `!important`, and not touching the shared `.app-shell p` rule
 * itself, which plenty of other apps' empty-state copy still wants.
 * `font-family` is now also stated explicitly (belt-and-suspenders) so a
 * future tag-level rule can't quietly reintroduce the same class of bug. */
.wfl-rack-cover .wfl-rack-cover-text {
  margin: 0;
  color: var(--wfl-chrome-fg);
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: var(--fs-sm);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
.wfl-rack-cover-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.wfl-rack-cover-btn {
  padding: 8px 16px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--wfl-chrome-fg) 45%, transparent);
  background: rgba(0, 0, 0, 0.18);
  color: var(--wfl-chrome-fg);
  font-size: var(--fs-xs);
  font-weight: 700;
}
.wfl-rack-cover-btn--primary { background: var(--brand); border-color: var(--brand); color: #fff; }

/* Sept 27 2026 (rebuilt per Ron's exact spec: "my tile rack physically
 * slides off-screen... a covered tile rack with Lisa's name on it
 * appears in the opposite visual path. It uncovers"): the slide now
 * plays on the COVER itself (.wfl-rack-cover), not the bare rack --
 * runHandoffCoverAnimation() in app-live.js slides the outgoing
 * holder's still-covered cover off, swaps its text to the incoming
 * holder's name and the rack CONTENTS underneath it (still hidden)
 * while it's off-screen, slides the relabeled cover back on, then
 * uncovers (hides the cover, reveals the real rack) after a short
 * beat. prefers-reduced-motion gets an instant swap, no slide -- this
 * is a table-manners flourish, not information the animation itself
 * carries. */
.wfl-rack-cover--slide-out { animation: wfl-rack-cover-slide-out 220ms ease-in forwards; }
.wfl-rack-cover--slide-in { animation: wfl-rack-cover-slide-in 260ms ease-out; }
@keyframes wfl-rack-cover-slide-out {
  to { transform: translateX(-115%); opacity: 0; }
}
@keyframes wfl-rack-cover-slide-in {
  from { transform: translateX(115%); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

/* Sept 28 2026 (bug fix, Ron: "things are happening too fast to
 * track... I have no clear sense of whose tiles are whose" -- the
 * cover used to just flip `hidden` in the same tick as the blocking
 * window.confirm() call right after it, so the browser never got a
 * chance to paint the cover before rendering froze for the native
 * dialog; see openPassThePhone()'s own comment in app-live.js for the
 * full root-cause writeup). `--appear`/`--disappear` are a NEW pair,
 * separate from `--slide-out`/`--slide-in` above -- those two are the
 * off-screen handoff slide (a different device's cover swapping in),
 * this pair is "the cover materializes/dematerializes in place over
 * MY OWN rack," used for (1) applying the cover before the confirm()
 * dialog fires and (2) reversing it on Cancel. app-live.js waits for
 * this animation's own duration (see RACK_COVER_APPEAR_MS) before
 * calling window.confirm() or hiding the cover, so it's guaranteed to
 * actually paint first.
 *
 * Sept 29 2026 (bug fix, Ron: "it fades in. Can you make it (a) slide
 * down, and (b) finish the sliding animation before the dialog
 * appears?"): (b) was already true -- app-live.js's RACK_COVER_APPEAR_MS
 * gate above is exactly that wait -- so only the motion itself changes
 * here, from a scale/opacity cross-fade to a real slide: the cover now
 * travels down from directly above the tray (translateY(-100%), fully
 * off its own top edge) to resting in place, like a physical cover
 * being lowered over the rack. Reversed for `--disappear` (retracts back
 * up). Same 260ms duration, so RACK_COVER_APPEAR_MS's own "slightly
 * longer than the CSS animation" gate (300ms) still comfortably covers
 * it -- nothing to change on the JS side for this part. */
.wfl-rack-cover--appear { animation: wfl-rack-cover-appear 260ms ease-out; }
.wfl-rack-cover--disappear { animation: wfl-rack-cover-disappear 260ms ease-in forwards; }
@keyframes wfl-rack-cover-appear {
  from { transform: translateY(-100%); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
@keyframes wfl-rack-cover-disappear {
  from { transform: translateY(0); opacity: 1; }
  to { transform: translateY(-100%); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .wfl-rack-cover--slide-out, .wfl-rack-cover--slide-in,
  .wfl-rack-cover--appear, .wfl-rack-cover--disappear { animation: none; }
}

/* ---- shell toolbar theme ----
 * Sept 25 2026 (Ron, toolbar-consolidation pivot: "I would... be OK
 * with passing a style to the shell for the toolbar itself. That would
 * let us put our own buttons on there, in the style of the app, and
 * make the toolbar fit us"): applied via AppShell.js's new
 * ctx.chrome.setToolbarClass('wfl-toolbar-theme') (see app-live.js's
 * mount()). #appBottomToolbar lives OUTSIDE .wordflex-live's own DOM
 * subtree entirely -- it's the shell's own toolbar element, not this
 * app's content -- so it can't read this file's --wfl-* custom
 * properties above (those are scoped to .wordflex-live and its
 * descendants, see the "theming" section up top); this hardcodes the
 * same wood colors as .wordflex-live's own DEFAULT (Espresso Light)
 * palette instead. #appBottomToolbar (its real id in index.html) is
 * used instead of relying on .wfl-toolbar-theme's class specificity
 * alone, so this reliably wins over the shell's own plain
 * `.app-toolbar { background: var(--card) }` rule.
 *
 * Known gap, flagged rather than silently faked: the toolbar doesn't
 * yet follow the in-app theme picker (Midnight/Charcoal, dark modes) --
 * every theme gets this one wood look regardless. Making it track the
 * live theme would need a second hook (an inline style or CSS-variable
 * passthrough, not just a class name) -- worth doing once Ron's tried
 * this v1, not blocking on it now. */
#appBottomToolbar.wfl-toolbar-theme {
  background: linear-gradient(160deg, #6b4a2e, #5a3c24);
  border-top-color: rgba(240, 230, 210, 0.35);
}
#appBottomToolbar.wfl-toolbar-theme .toolbar-tool-btn {
  border-color: rgba(240, 230, 210, 0.55);
  background: rgba(0, 0, 0, 0.12);
  color: #f0e6d2;
}
#appBottomToolbar.wfl-toolbar-theme .toolbar-tool-btn:active {
  background: rgba(240, 230, 210, 0.18);
}
#appBottomToolbar.wfl-toolbar-theme .toolbar-tool-btn.is-active {
  background: #d97757;
  border-color: #d97757;
  color: #fff;
}
/* Sept 25 2026 (Ron: "if I'm not allowed to send, disable the send
 * button"): the shell's own generic disabled look (index.html's
 * .toolbar-tool-btn:disabled, opacity: 0.4) reads fine against a
 * plain system toolbar, but washes out to near-invisible against this
 * dark wood background at that opacity -- overriding with a flatter,
 * still-obviously-inert look instead of just dimming further. */
#appBottomToolbar.wfl-toolbar-theme .toolbar-tool-btn:disabled {
  opacity: 1;
  background: rgba(0, 0, 0, 0.28);
  border-color: rgba(240, 230, 210, 0.22);
  color: rgba(240, 230, 210, 0.45);
}
