/* Japanese Word Match game — styles specific to game.html.
   The site header/nav/login-bar stays in the portfolio's light burgundy theme (style.css
   tokens, untouched here). Only the game's own content — the level-select screen and the
   board screen, both wrapped in <main class="game-main">, plus the two game-specific modals
   (#start-modal, #result-modal) — switches to a dark "digital hanafuda" theme: deep lacquer
   ground, washi-paper tiles, gold foil accents, four hanafuda-suit accent hues.
   All dark tokens live under .game-main / #start-modal / #result-modal, not :root, so nothing
   here leaks into the shared header, footer, or the auth/account modals (which stay light). */

/* Makes the level-select screen's dark theme full-bleed instead of a boxed dark column with
   the shared light :root --bg-page peeking through past .container's 1200px cap on wide
   viewports (the "playing" full-screen mode above already handles this differently via the
   100vw breakout). Only needs the single deepest gradient stop, not the whole token block,
   since it's just painting a seamless edge color, not reproducing the radial gradient itself.
   :has() here only ever matches game.html, since .game-main only exists there. */
body:has(.game-main) {
    --ground-deep: #0d0c12;
    background: var(--ground-deep);
}

.game-main {
    --ground: #15131c;
    --ground-deep: #0d0c12;
    --panel: #1e1a2a;
    --panel-edge: #2c2640;
    --paper: #f2e8d5;
    --paper-edge: #d9c9a8;
    --ink: #2a2117;
    --ink-soft: #5a4f3d;
    /* --ink-soft is a muted BROWN, tuned for use on the light --paper tile background (e.g.
       .tile-reading) -- it's ~2.2:1 against this theme's dark grounds, well under the WCAG AA
       text minimum of 4.5:1, so it must never be used as text color directly on --ground/
       --ground-deep/the example-panel gradient or similar dark surfaces. --paper-muted is the
       equivalent "secondary/muted text" role for those dark surfaces: the midpoint between
       --paper (#f2e8d5) and --ink-soft (#5a4f3d), i.e. a muted warm tan rather than a muted
       brown. Verified via WCAG relative-luminance contrast math against the example-panel's
       gradient (#1c1828 to --ground-deep #0d0c12, checked at both stops since text can sit
       anywhere along it): 6.39:1 at minimum (vs. --ink-soft's 2.16:1 in the same spot) --
       comfortably clears 4.5:1 with room to spare, while still reading as visually dimmer than
       full-strength --paper (14.27:1) so muted text keeps a real visual hierarchy. */
    --paper-muted: #a69c89;
    --gold: #d4a64b;
    --gold-bright: #f4ce7a;
    --crimson: #c0435a;
    --pine: #3d7a5c;
    --wisteria: #5b57a6;
    --persimmon: #d97a3f;
    --lightning: #7fe0ff;

    --font-jp: "Noto Serif JP", "Hiragino Mincho ProN", "Yu Mincho", "MS Mincho", serif;
    --ease-pop: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

    position: relative;
    isolation: isolate;
    /* 64px one-row masthead + the ~25px clearance (was 140px for the old 115px two-row bar). */
    padding: 89px 40px 60px;
    min-height: 100vh;
    background: radial-gradient(ellipse 120% 60% at 50% -10%, #241f34 0%, var(--ground) 45%, var(--ground-deep) 100%);
    color: var(--paper);
    overflow: hidden;
}

/* Decorative per-level background photo (castle/mountain, see BOARD_BG_IMAGES in game.js).
   Lives on .game-main, not .board-wrap: the honeycomb covers ~95% of the board panel's own
   area, so a photo layered there was empirically imperceptible even at high opacity (tried
   0.13 and 0.32, confirmed via screenshot -- not just assumed) -- .game-main has the actual
   open space (margins, the area around the toolbar and side panel) for it to read as a real
   background rather than a few pixels in the hex gaps. isolation: isolate above scopes
   z-index here to just this element's descendants, so z-index: 0 (this pseudo-element paints
   right after .game-main's own gradient, before any normal-flow or explicitly-stacked child,
   since it's the first thing in DOM/paint order at that stacking level) is enough to sit
   behind the ambient/fx canvases and every .game-section without needing a negative value.
   --board-bg-image is unset outside an active level (only set in startLevel()), so this stays
   invisible on the level-select screen via the `none` fallback. */
.game-main::before {
    content: '';
    position: absolute;
    inset: 0;
    background-image: var(--board-bg-image, none);
    background-size: cover;
    background-position: center;
    opacity: 0.16;
    z-index: 0;
    pointer-events: none;
}

/* Full-screen "playing" mode (like slither.io — the play area fills the whole browser
   window, no site chrome around it), active only while a board is actually up. Toggled by
   game.js adding/removing .game-playing on <body> in startLevel()/backToLevels(). The site
   header/login-bar hide entirely; .game-main breaks out of .container's max-width:1200px
   centering to span the true viewport width instead of sitting in a boxed page column.
   body already has overflow-x:hidden (style.css) so the 100vw breakout can't cause a
   horizontal scrollbar even where a vertical scrollbar is present. */
body.game-playing .site-header-wrap {
    display: none;
}

body.game-playing .game-main {
    position: relative;
    left: 50%;
    width: 100vw;
    transform: translateX(-50%);
    padding: 20px 24px 40px;
}

.game-fx-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 0;
}

#fx-canvas { z-index: 5; }

/* Only the actual content sections (and the float-text layer) need lifting above the fx
   canvases -- NOT the canvases themselves. A blanket ".game-main > *" rule here would tie in
   specificity with .game-fx-canvas's own "position: absolute" and (as the later rule) win,
   silently flipping the canvases back to static/relative flow -- which then feeds their own
   height back into .game-main's auto height via the JS resize() below, a runaway loop. */
.game-main > .game-section,
.game-main > .fx-text-layer {
    position: relative;
    z-index: 1;
}
.game-main .fx-text-layer { z-index: 6; }

@media (prefers-reduced-motion: reduce) {
    .game-main *, .game-main *::before, .game-main *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* .nav-current / .back-link now live in style.css (shared header styling, not game-specific). */

/* ---------------------------------------------------------------------
   Page shell / HUD row
   --------------------------------------------------------------------- */
.game-hud-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 20px;
}

.game-title {
    font-family: var(--font-jp);
    font-size: 2.3rem;
    font-weight: 700;
    margin: 0 0 12px;
    color: var(--paper);
    line-height: 1.15;
}

.game-title-sub {
    display: block;
    font-family: 'Fraunces', Georgia, serif;
    font-style: italic;
    font-size: 1rem;
    color: var(--gold);
    font-weight: 600;
    margin-top: 6px;
    letter-spacing: 0.02em;
}

.game-subtitle {
    color: var(--ink-soft);
    max-width: 680px;
    line-height: 1.6;
    margin: 0 0 8px;
}

.game-guest-hint {
    color: var(--gold-bright);
    background: rgba(212, 166, 75, 0.1);
    border: 1px solid rgba(212, 166, 75, 0.3);
    display: inline-block;
    padding: 8px 16px;
    font-size: 0.9rem;
    margin: 8px 0 30px;
}

.game-section.hidden {
    display: none;
}

/* Sound toggle -- shared look for the level-select and in-board copies */
.sound-toggle {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    border: 1px solid rgba(212, 166, 75, 0.4);
    background: rgba(212, 166, 75, 0.08);
    color: var(--gold-bright);
    font-size: 1.05rem;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background 0.2s ease, transform 0.2s var(--ease-pop), box-shadow 0.2s ease;
}

.sound-toggle:hover { background: rgba(212, 166, 75, 0.18); transform: translateY(-1px); }
.sound-toggle:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: 2px; }
.sound-toggle.on { box-shadow: 0 0 0 2px rgba(244, 206, 122, 0.28); }

/* ---------------------------------------------------------------------
   Level select — a JLPT tier tab strip (reuses the shared --jlpt-n5..n1 hues from
   style.css, same palette the header nav dropdown and Phonetics Family already use for the
   same five values) above a denser grid, since each tier now holds 10 levels instead of one
   card per tier holding a random-set-of-several.
   --------------------------------------------------------------------- */
.jlpt-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 22px;
}

.jlpt-tab {
    background: var(--panel);
    border: 1px solid var(--panel-edge);
    color: var(--paper-muted);
    font-family: inherit;
    font-weight: 700;
    font-size: 0.9rem;
    letter-spacing: 0.02em;
    padding: 9px 20px;
    cursor: pointer;
    border-bottom: 3px solid transparent;
    transition: all 0.2s ease;
}

.jlpt-tab:hover { color: var(--paper); border-color: var(--gold); }

.jlpt-tab[data-level="N5"].active { border-bottom-color: var(--jlpt-n5); }
.jlpt-tab[data-level="N4"].active { border-bottom-color: var(--jlpt-n4); }
.jlpt-tab[data-level="N3"].active { border-bottom-color: var(--jlpt-n3); }
.jlpt-tab[data-level="N2"].active { border-bottom-color: var(--jlpt-n2); }
.jlpt-tab[data-level="N1"].active { border-bottom-color: var(--jlpt-n1); }

.jlpt-tab.active {
    background: linear-gradient(180deg, var(--panel), #191527);
    color: var(--paper);
    box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.6);
}

.level-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 14px;
    margin-top: 18px;
}

.level-card {
    text-align: left;
    background: linear-gradient(180deg, var(--panel), #191527);
    border: 1px solid var(--panel-edge);
    box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.6);
    padding: 16px 16px;
    cursor: pointer;
    transition: all 0.25s ease;
    font-family: inherit;
    color: var(--paper);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.level-card:hover {
    border-color: var(--gold);
    box-shadow: 0 16px 40px -14px rgba(0, 0, 0, 0.75), 0 0 0 1px rgba(212, 166, 75, 0.15);
    transform: translateY(-3px);
}

.level-badge {
    display: inline-block;
    align-self: flex-start;
    background: rgba(212, 166, 75, 0.12);
    color: var(--gold-bright);
    font-weight: 700;
    font-size: 0.75rem;
    letter-spacing: 0.03em;
    padding: 3px 9px;
    border: 1px solid rgba(212, 166, 75, 0.3);
}

.level-card h3 {
    margin: 0;
    font-size: 1.05rem;
    font-family: 'Fraunces', Georgia, serif;
}

.level-meta {
    font-size: 0.78rem;
    /* was --ink-soft (~2.2:1 on this dark card face) -- see the contrast note near the top of
       this file; --paper-muted is the verified-6.39:1 replacement used throughout. */
    color: var(--paper-muted);
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.level-meta .completed {
    color: var(--pine);
    font-weight: 700;
}

/* ---------------------------------------------------------------------
   Board toolbar
   --------------------------------------------------------------------- */
.board-toolbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 16px;
    margin-bottom: 26px;
}

.board-level-label {
    font-weight: 700;
    font-size: 1.1rem;
    font-family: 'Fraunces', Georgia, serif;
    color: var(--paper);
}

.board-stats {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-left: auto;
    font-size: 0.85rem;
    /* was --ink-soft (~2.2:1 on this dark toolbar ground) -- --paper-muted is the verified
       6.39:1 replacement already used throughout this file. */
    color: var(--paper-muted);
    font-weight: 600;
}

#board-timer.low-time {
    color: #e2705c;
    font-weight: 700;
    animation: lowTimePulse 1s ease-in-out infinite;
}

@keyframes lowTimePulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
}

.board-stats span:not(.hud-stat) {
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--panel-edge);
    padding: 6px 12px;
}

.hud-stat {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 3px;
}

.hud-label {
    font-size: 0.6rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.hud-value {
    font-family: 'Fraunces', Georgia, serif;
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--gold-bright);
    font-variant-numeric: tabular-nums;
    text-shadow: 0 0 14px rgba(244, 206, 122, 0.35);
    line-height: 1;
}

.streak-stat { width: 96px; }

.streak-meter {
    position: relative;
    display: block;
    width: 100%;
    height: 7px;
    border-radius: 5px;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(212, 166, 75, 0.25);
    overflow: hidden;
}

.streak-fill {
    position: absolute;
    inset: 0;
    display: block;
    width: 0%;
    background: linear-gradient(90deg, var(--gold), var(--gold-bright));
    transition: width 0.4s var(--ease-out);
}

.streak-fill.tier-flash { animation: tierFlash 0.5s var(--ease-out); }

@keyframes tierFlash {
    0% { filter: brightness(1); }
    35% { filter: brightness(2.2); }
    100% { filter: brightness(1); }
}

.ghost-btn {
    background: rgba(212, 166, 75, 0.06);
    border: 1px solid rgba(212, 166, 75, 0.35);
    color: var(--gold-bright);
    padding: 9px 16px;
    font-family: inherit;
    font-weight: 600;
    font-size: 0.85rem;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: all 0.2s ease;
}

.ghost-btn:hover {
    border-color: var(--gold);
    background: rgba(212, 166, 75, 0.16);
    transform: translateY(-1px);
}

/* Powerup toolbar buttons (see maybeGrantPowerup/updatePowerupUI in game.js). Charges are
   earned automatically but always spent by an explicit click -- disabled/muted with no charge,
   a soft pulsing gold ring once at least one is banked, so "you have something to use" reads at
   a glance without stealing focus from the board. */
.powerup-controls {
    display: flex;
    gap: 8px;
}

.powerup-btn {
    display: flex;
    align-items: center;
    gap: 6px;
    background: rgba(212, 166, 75, 0.06);
    border: 1px solid rgba(212, 166, 75, 0.35);
    color: var(--gold-bright);
    padding: 7px 12px;
    font-family: inherit;
    font-weight: 600;
    font-size: 0.8rem;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: all 0.2s ease;
}

.powerup-icon { font-size: 1rem; line-height: 1; }

.powerup-charge-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.35);
    font-size: 0.68rem;
    font-variant-numeric: tabular-nums;
    color: var(--paper-muted);
}

.powerup-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.powerup-btn:not(:disabled):hover {
    border-color: var(--gold);
    background: rgba(212, 166, 75, 0.16);
    transform: translateY(-1px);
}

.powerup-btn.ready {
    border-color: var(--gold-bright);
    background: rgba(244, 206, 122, 0.12);
    animation: powerupReadyPulse 1.8s ease-in-out infinite;
}

.powerup-btn.ready .powerup-charge-badge {
    background: var(--gold);
    color: var(--ground-deep);
}

@keyframes powerupReadyPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(244, 206, 122, 0.35); }
    50% { box-shadow: 0 0 0 5px rgba(244, 206, 122, 0); }
}

.powerup-btn:not(:disabled):active {
    transform: translateY(0) scale(0.94);
}

/* ---------------------------------------------------------------------
   Board + honeycomb tile grid
   --------------------------------------------------------------------- */
/* Wide layout (default): board and the example/progress side panel sit side by side.
   max-width caps the combined row (1180 board + 20 gap + 260 panel = 1460) and centers it,
   the same role .board-wrap's own max-width/margin:auto used to play alone before the panel
   became a flex sibling instead of a fixed-position overlay. Reverted to plain block flow
   under 968px -- see the media query at the bottom of this file -- where .board-wrap goes
   back to being the sole (self-centered) block-level child and #example-panel returns to a
   fixed full-width bottom bar, pixel-for-pixel the pre-redesign layout. */
.board-layout {
    display: flex;
    align-items: flex-start;
    gap: 20px;
    max-width: 1460px;
    margin: 0 auto;
}

.board-wrap {
    position: relative;
    /* flex: 1 1 0% (not "auto") so the item's hypothetical size starts at zero and grows/
       shrinks to fill whatever the row has left after the panel's fixed flex-basis -- doesn't
       depend on interactions between a % width and max-width during flex-basis resolution.
       min-width: 0 overrides flexbox's default min-width:auto, which otherwise refuses to
       shrink a flex item below its content's intrinsic width (here, the hex grid's natural
       size) -- without it this item can't shrink past that floor and overflows its row on
       narrower "wide-layout" widths (e.g. 969-1100px) exactly like the old vw-only formula did
       on phones. max-width keeps the same 1180px cap the un-paneled board always had; at the
       widest supported viewport this is what the flex math actually resolves to anyway (1460
       row max-width - 260 panel - 20 gap = 1180), so it's a belt-and-suspenders duplicate of
       .board-layout's own cap, not a competing source of truth. container-type: inline-size
       turns this element into a query container so .tile-grid's --hex-w (below) can be sized
       off this element's own resolved width via cqw units instead of a hand-maintained vw
       formula that has no way to know a side panel exists -- see the big comment on .tile-grid. */
    flex: 1 1 0%;
    min-width: 0;
    max-width: 1180px;
    container-type: inline-size;
    overflow-x: auto;
    padding: 30px 12px 10px;
    transition: transform 0.05s linear;
}

.board-wrap.board-shake { animation: boardShake 0.34s var(--ease-out); }

/* A 3+ pair lightning chain flashes the whole board, not just the chained tiles -- the same
   "this touched everything" structural cue .board-shake already gives a mismatch and the
   penalty dust gives a penalty, so a big combo reads as a board-wide event too, not merely a
   bigger version of an ordinary match. Sits under #fx-canvas/.flyer's z-index:5 so the bolt
   particles still render on top of it. */
.board-wrap.lightning-flash::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 4;
    background: radial-gradient(ellipse at center, rgba(127, 224, 255, 0.32), transparent 68%);
    animation: lightningFlash 0.4s ease-out;
}

@keyframes lightningFlash {
    0% { opacity: 0; }
    18% { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes boardShake {
    0% { transform: translateX(0); }
    20% { transform: translateX(-8px) rotate(-0.3deg); }
    40% { transform: translateX(7px) rotate(0.3deg); }
    60% { transform: translateX(-5px); }
    80% { transform: translateX(3px); }
    100% { transform: translateX(0); }
}

/* Honeycomb layout: rows of pointy-top hexagon tiles, alternate rows offset by half a tile
   width and pulled up to interlock into the row above. Rows sit edge-to-edge with ZERO gap
   here -- hexagons only interlock without a seam when adjacent-row tiles share an exact
   vertex, which requires the offset to be precisely half the tile width (not half the tile
   width *plus* a gap -- that nudges every other row just far enough off-vertex to open a
   visible seam). The visible ~8% breathing room between tiles instead comes from uniformly
   insetting each tile within its slot (the --tile-inset scale below), identical on all six
   sides. perRow is fixed at 8 in JS (40 tiles / 8 = 5 clean rows, no partial last row); this
   math doesn't depend on row length, only on --hex-w/--hex-h and the 0.25 corner fraction, so
   it holds at any tile count per row.
   --hex-w is derived, not guessed. .tile-grid's align-items:center centers each plain row
   (width 8 x hex-w) first, leaving margin M = (available - 8 x hex-w)/2 on both sides; the
   offset row then shifts right by another hex-w/2 on top of that same centered start, so its
   right edge lands at M + 8.5 x hex-w, not just 8.5 x hex-w from the container edge. Solving
   M + 8.5 x hex-w <= available for the largest non-overflowing hex-w gives available / 9, not
   available / 8.5 (an earlier pass here divided by 8.5 and left a residual 8-30px overflow at
   every width -- caught by re-measuring actual tile edges with getBoundingClientRect after the
   first fix, not by trusting the formula).
   "available" is .board-wrap's own content-box width, sized via container query units (cqw)
   off .board-wrap itself (container-type: inline-size, set above) rather than 100vw. This
   replaced an earlier vw-based formula -- available / 9 with "available" hand-computed as
   100vw minus every ancestor's combined horizontal padding -- that worked fine as long as
   .board-wrap was always the full content width of the page, but had no way to know when a
   side panel (see .board-layout above) started eating part of that width on wide screens; a
   100vw-based number can only ever describe the viewport, never "my parent flex item's actual
   resolved width after its sibling took some of the row." cqw solves that structurally: 100cqw
   always equals .board-wrap's own resolved content-box width in whichever layout is active
   (paneled or not), so this one formula is correct in both without tracking what the panel
   "costs".
   Note there's no "- Npx" term subtracting .board-wrap's own padding the way the old vw formula
   had to subtract 72px of ancestor padding -- verified empirically (isolated container/cqw
   test, not assumed) that cqw is already relative to the container's content box, i.e. padding
   and border already excluded, the same convention as %. Subtracting .board-wrap's padding
   again on top of that would double-count it and under-fill the row by 24px/9 (~2.7px) of
   hex-w for no reason. If .board-wrap's own padding (currently 30px 12px 10px, i.e. 12px x2
   horizontal) ever changes, no formula edit is needed here -- that's the entire point of
   sizing off the container's own resolved box instead of a hand-tracked constant.
   The 32px floor is chosen so the formula stays unclamped (i.e. mathematically zero-overflow)
   down to 360px viewport width in the unpaneled (mobile) layout, comfortably below any phone in
   real use; only below that does .board-wrap's overflow-x:auto become the fallback rather than
   a formality.
   Divisor is perRow+1 (not perRow+0.5): align-items:center centers a plain row first, and the
   offset row's rightward shift lands on top of that already-centered position, so the true
   footprint that has to fit is one full extra half-tile on EACH side, not just the shift
   amount -- see the game.js PER_ROW comment for the fuller derivation. PER_ROW changed from 8
   to 5 when levels shrank from 20 to 10 pairs, so the divisor changes with it: 6, not 9. */
.tile-grid {
    --hex-w: clamp(38px, calc(100cqw / 6), 148px);
    --hex-h: calc(var(--hex-w) * 1.017);
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Deliberately no explicit width: a flex container in normal block flow already fills
       its parent's width by default, which is what leaves room on both sides for the
       offset rows' rightward translateX shift (see .hex-row.offset below) without clipping. */
}

.hex-row {
    display: flex;
    margin-top: calc(var(--hex-h) * -0.25);
}

.hex-row:first-child { margin-top: 0; }

.hex-row.offset {
    transform: translateX(calc(var(--hex-w) / 2));
}

.tile {
    --tile-shadow: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.35));
    --tile-inset: 0.92;
    position: relative;
    width: var(--hex-w);
    height: var(--hex-h);
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    border: none;
    background:
        radial-gradient(140% 120% at 15% 10%, rgba(255, 255, 255, 0.5), transparent 55%),
        linear-gradient(165deg, var(--paper), var(--paper-edge) 130%);
    cursor: pointer;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    padding: 4px 6px;
    text-align: center;
    font-family: inherit;
    color: var(--ink);
    transition: transform 0.32s var(--ease-pop), filter 0.32s ease;
    transform: translateY(0) scale(var(--tile-inset));
    filter: var(--tile-shadow);
}

/* JP-side vs. meaning-side tiles get their own resting shadow tint so the two halves of a pair
   read as visually distinct families at a glance, not just by which edge of the board they're
   on. Overriding --tile-shadow here (rather than each state's own filter rule) propagates
   automatically into hover/selected/match-pop/etc., since they all compose var(--tile-shadow)
   into their own filter -- one override point, no risk of the two drifting out of sync. Colors
   reuse the existing indigo/amber suit hues (SUIT_COLORS in game.js) rather than introducing new
   ones. The compound selector (.tile.tile-jp, not bare .tile-jp) matters: .tile-jp is reused as
   the *inner* reading span's class too (see dealPairs' btn.innerHTML) -- .tile.tile-jp only ever
   matches the outer button. */
.tile.tile-jp { --tile-shadow: drop-shadow(0 3px 4px rgba(91, 87, 166, 0.45)); }
.tile.tile-en { --tile-shadow: drop-shadow(0 3px 4px rgba(217, 122, 63, 0.45)); }

.tile:focus-visible {
    outline: 2px solid var(--gold-bright);
    outline-offset: 3px;
}

.tile-dot {
    position: absolute;
    top: 14%;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--suit-color, var(--gold));
    opacity: 0.6;
}

/* .tile-jp/.tile-reading/.tile-en-text share a max-width:100% + ellipsis clamp: game-words.js's
   `en` field ranges from single words up to full disambiguating phrases like "bright (in
   reference to personality or weather)" (50 chars) -- no font-size floor can fit that on a
   38-148px hex tile, and unconstrained text was overflowing the tile's clip-path edge and
   overlapping neighboring tiles (confirmed by measuring rendered text width against tile width
   on real game-words.js entries, not just visually). These are flex column children, so
   max-width:100% resolves against .tile's own content-box width (padding already excluded) and
   actually constrains them despite align-items:center's default shrink-to-fit sizing. */
.tile-jp {
    font-family: var(--font-jp);
    font-size: clamp(1rem, 2.6cqw, 1.6rem);
    font-weight: 700;
    line-height: 1.15;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tile-reading {
    font-size: clamp(0.7rem, 1.4cqw, 0.9rem);
    /* was --ink-soft (~2.2:1 on this dark tile face, see the contrast note further up this
       file) -- --paper-muted is the same verified-6.39:1 replacement used in the side panel. */
    color: var(--paper-muted);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tile-en-text {
    font-family: 'IBM Plex Sans', sans-serif;
    /* lower floor than .tile-jp's 1rem -- English glosses run far longer than Japanese headwords
       (up to 50 chars vs. a handful of kana/kanji), so they need more shrink room before ellipsis
       has to take over at the smallest tile sizes. */
    font-size: clamp(0.62rem, 2cqw, 1.15rem);
    font-weight: 600;
    line-height: 1.15;
    text-transform: lowercase;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tile:not(.cleared):hover {
    transform: translateY(-5px) scale(calc(var(--tile-inset) * 1.035));
    filter: var(--tile-shadow) drop-shadow(0 0 9px rgba(212, 166, 75, 0.55));
}

.tile.selected {
    transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06));
    animation: hexSelectPulse 0.5s var(--ease-out);
}

@keyframes hexSelectPulse {
    0% { filter: var(--tile-shadow) drop-shadow(0 0 2px var(--gold-bright)); }
    45% { filter: var(--tile-shadow) drop-shadow(0 0 15px var(--gold-bright)); }
    100% { filter: var(--tile-shadow) drop-shadow(0 0 9px var(--gold-bright)); }
}

.tile.match-pop { animation: matchPop 0.56s var(--ease-pop); }

@keyframes matchPop {
    0% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)); filter: var(--tile-shadow) drop-shadow(0 0 9px var(--gold-bright)); }
    35% { transform: translateY(-14px) scale(calc(var(--tile-inset) * 1.22)); filter: var(--tile-shadow) drop-shadow(0 0 16px var(--gold-bright)) brightness(1.35) saturate(1.2); }
    65% { transform: translateY(-10px) scale(calc(var(--tile-inset) * 0.94)); filter: var(--tile-shadow) drop-shadow(0 0 9px var(--gold-bright)); }
    100% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)); filter: var(--tile-shadow) drop-shadow(0 0 9px var(--gold-bright)); }
}

/* Phonetic-chain "lightning connect" -- a brighter, cyan-tinted pop distinct from a normal
   gold match, for every tile caught in the chain */
.tile.lightning-pop { animation: lightningPop 0.62s var(--ease-pop); }

@keyframes lightningPop {
    0% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)); filter: var(--tile-shadow) drop-shadow(0 0 10px var(--lightning)); }
    30% { transform: translateY(-18px) scale(calc(var(--tile-inset) * 1.3)); filter: var(--tile-shadow) drop-shadow(0 0 22px var(--lightning)) brightness(1.5) saturate(1.3); }
    60% { transform: translateY(-10px) scale(calc(var(--tile-inset) * 0.92)); filter: var(--tile-shadow) drop-shadow(0 0 14px var(--lightning)); }
    100% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)); filter: var(--tile-shadow) drop-shadow(0 0 9px var(--lightning)); }
}

/* Wakan blast (winged-tile bonus): the dropped-on pair plus its blast-radius neighbours --
   violet-tinted (--wisteria, otherwise only ever a per-pair suit-dot accent) so it's visually
   distinct from a gold ordinary match and a cyan lightning chain, but built from the exact same
   pop shape as both. */
.tile.wakan-pop { animation: wakanPop 0.62s var(--ease-pop); }

@keyframes wakanPop {
    0% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)); filter: var(--tile-shadow) drop-shadow(0 0 10px var(--wisteria)); }
    30% { transform: translateY(-18px) scale(calc(var(--tile-inset) * 1.3)); filter: var(--tile-shadow) drop-shadow(0 0 22px var(--wisteria)) brightness(1.5) saturate(1.3); }
    60% { transform: translateY(-10px) scale(calc(var(--tile-inset) * 0.92)); filter: var(--tile-shadow) drop-shadow(0 0 14px var(--wisteria)); }
    100% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)); filter: var(--tile-shadow) drop-shadow(0 0 9px var(--wisteria)); }
}

/* Streak-4 powerups (maybeGrantPowerup in game.js). Free-clear: a full spin-in-place while it
   pops, distinct MOTION (not just color) from match/lightning/wakan's shared vertical
   lift-bounce -- the star-sparkle burst (see spawnSparkle in game.js) doing the rest of the
   "magic" reading. Swap: a real 3D card-flip -- the outgoing tile rotates away face-first
   (perspective() is set per-keyframe, so no ancestor needs its own perspective/preserve-3d),
   its replacement flips in from the opposite face (.tile.swap-in, passed as dealPairs'
   entranceClass in grantSwapPowerup) -- reads literally as "this card got swapped", not a
   generic entrance/exit. */
.tile.powerup-pop { animation: powerupPop 0.6s var(--ease-pop); }

@keyframes powerupPop {
    0% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)) rotate(0deg); filter: var(--tile-shadow) drop-shadow(0 0 10px rgb(110, 224, 165)); }
    50% { transform: translateY(-16px) scale(calc(var(--tile-inset) * 1.26)) rotate(180deg); filter: var(--tile-shadow) drop-shadow(0 0 22px rgb(110, 224, 165)) brightness(1.5) saturate(1.3); }
    100% { transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)) rotate(360deg); filter: var(--tile-shadow) drop-shadow(0 0 9px rgb(110, 224, 165)); }
}

.tile.powerup-swap-out {
    animation: powerupSwapOut 0.4s var(--ease-pop) forwards;
    pointer-events: none;
}

@keyframes powerupSwapOut {
    0% { opacity: 1; transform: perspective(700px) rotateY(0deg) scale(var(--tile-inset)); filter: var(--tile-shadow) drop-shadow(0 0 10px rgb(224, 110, 190)); }
    55% { opacity: 1; transform: perspective(700px) rotateY(78deg) scale(calc(var(--tile-inset) * 0.94)); filter: var(--tile-shadow) drop-shadow(0 0 18px rgb(224, 110, 190)) brightness(1.3); }
    100% { opacity: 0; transform: perspective(700px) rotateY(112deg) scale(calc(var(--tile-inset) * 0.82)); filter: var(--tile-shadow) drop-shadow(0 0 4px rgb(224, 110, 190)); }
}

.tile.swap-in { animation: swapIn 0.42s var(--ease-pop); }

@keyframes swapIn {
    0% { opacity: 0; transform: perspective(700px) rotateY(-112deg) scale(calc(var(--tile-inset) * 0.82)); filter: var(--tile-shadow) drop-shadow(0 0 4px rgb(110, 224, 165)); }
    45% { opacity: 1; transform: perspective(700px) rotateY(-70deg) scale(calc(var(--tile-inset) * 0.94)); filter: var(--tile-shadow) drop-shadow(0 0 18px rgb(110, 224, 165)) brightness(1.3); }
    100% { opacity: 1; transform: perspective(700px) rotateY(0deg) scale(var(--tile-inset)); filter: var(--tile-shadow); }
}

.tile.mismatch { animation: mismatchShake 0.42s var(--ease-out); }

@keyframes mismatchShake {
    0%, 100% { transform: translateY(0) translateX(0) scale(var(--tile-inset)); }
    20% { transform: translateX(-6px) rotate(-2deg) scale(var(--tile-inset)); background-image: linear-gradient(165deg, #e8c9c9, #d1a3a3 130%); }
    40% { transform: translateX(6px) rotate(2deg) scale(var(--tile-inset)); }
    60% { transform: translateX(-4px) rotate(-1deg) scale(var(--tile-inset)); }
    80% { transform: translateX(3px) scale(var(--tile-inset)); }
}

/* A refill batch arriving (see dealPairs() in game.js) -- a small pop-in so new cards visibly
   arrive rather than the board silently changing shape between clicks. */
.tile.dealt-in { animation: tileDealIn 0.34s var(--ease-pop); }

@keyframes tileDealIn {
    0% { opacity: 0; transform: translateY(0) scale(calc(var(--tile-inset) * 0.35)) rotate(-6deg); }
    100% { opacity: 1; transform: translateY(0) scale(var(--tile-inset)) rotate(0deg); }
}

/* Declared after every entrance animation (.swap-in, .dealt-in) despite matching specificity, so
   a clear always wins the cascade even when a tile is matched right after being dealt/swapped
   in -- before this rule moved last, a freshly dealt tile that got matched immediately kept its
   .dealt-in class, and .dealt-in's later-declared `animation` shorthand overrode .cleared's,
   leaving the tile frozen at full opacity (pointer-events: none still applied, so it read as a
   stuck, unclickable "ghost" tile) forever instead of fading out. */
.tile.cleared {
    animation: tileClear 0.5s var(--ease-pop) forwards;
    pointer-events: none;
}

@keyframes tileClear {
    0% { opacity: 1; transform: translateY(-7px) scale(calc(var(--tile-inset) * 1.06)); }
    100% { opacity: 0; transform: translateY(-7px) scale(calc(var(--tile-inset) * 0.4)) rotate(8deg); }
}

/* ---------------------------------------------------------------------
   Floating combo/score text + lightning bolts (lightning bolts render on
   the fx-canvas via JS; this layer is just the text)
   --------------------------------------------------------------------- */
.fx-text-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.float-text {
    position: absolute;
    transform: translate(-50%, -50%);
    font-family: 'Fraunces', Georgia, serif;
    font-weight: 700;
    color: var(--gold-bright);
    text-shadow: 0 0 20px rgba(244, 206, 122, 0.6), 0 2px 6px rgba(0, 0, 0, 0.5);
    white-space: nowrap;
    animation: floatUp 1.1s var(--ease-out) forwards;
}

.float-text.streak-text {
    font-style: italic;
    letter-spacing: 0.02em;
}

.float-text.lightning-text {
    color: var(--lightning);
    text-shadow: 0 0 20px rgba(127, 224, 255, 0.65), 0 2px 6px rgba(0, 0, 0, 0.5);
}

/* Time bonus (+20s per pair) -- a cool teal rather than gold/cyan so it reads as its own
   thing next to the score's gold "+N" and the lightning chain's brighter cyan, even though
   both float-texts appear at once on an ordinary match. */
.float-text.time-text {
    color: #6fd9c4;
    text-shadow: 0 0 16px rgba(111, 217, 196, 0.55), 0 2px 6px rgba(0, 0, 0, 0.5);
    font-size: 0.85em;
}

.float-text.wakan-text {
    color: var(--wisteria);
    text-shadow: 0 0 20px rgba(91, 87, 166, 0.65), 0 2px 6px rgba(0, 0, 0, 0.5);
}

/* Penalty return: a cleared pair reappearing mid-round. Distinct from .match-pop (gold, tiles
   leaving) and .cleared (fading out) -- this is a tile arriving with a warning-red flash, the
   visual inverse of clearing. */
.tile.penalty-return {
    animation: penaltyReturn 0.7s var(--ease-pop);
}

@keyframes penaltyReturn {
    0% { filter: drop-shadow(0 0 0 rgba(226, 112, 92, 0)) var(--tile-shadow); transform: translateY(0) scale(calc(var(--tile-inset) * 0.4)) rotate(-8deg); opacity: 0; }
    40% { filter: drop-shadow(0 0 14px rgba(226, 112, 92, 0.85)) var(--tile-shadow); opacity: 1; }
    60% { transform: translateY(-6px) scale(calc(var(--tile-inset) * 1.05)) rotate(0deg); }
    100% { filter: var(--tile-shadow); transform: translateY(0) scale(var(--tile-inset)) rotate(0deg); }
}

@keyframes floatUp {
    0% { opacity: 0; transform: translate(-50%, -40%) scale(0.7); }
    18% { opacity: 1; transform: translate(-50%, -70%) scale(1.08); }
    30% { transform: translate(-50%, -78%) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -140%) scale(1); }
}

/* ---------------------------------------------------------------------
   Wakan winged-tile bonus event (see the JS section of the same name in game.js).
   The flyer is a sibling of #tile-grid, not a child of it -- #tile-grid's innerHTML gets wiped
   and rebuilt on every ordinary relayout (a match, a refill, a shuffle), which would silently
   delete the flyer out from under its own JS state if it lived inside that subtree. Its own
   --flyer-w/-h duplicate .tile-grid's --hex-w/-h clamp formula rather than inheriting it (an
   element can only inherit a custom property from its own ancestors, and .tile-grid is a
   sibling here, not one) -- .board-wrap already being a container (container-type: inline-size,
   set for that same --hex-w formula) means 100cqw resolves the same way regardless of which of
   its descendants declares the variable, so the two stay visually in sync without depending on
   DOM position.
   --------------------------------------------------------------------- */
.flyer {
    --flyer-w: clamp(38px, calc(100cqw / 6), 148px);
    --flyer-h: calc(var(--flyer-w) * 1.017);
    position: absolute;
    top: 40%;
    left: -140px;
    width: var(--flyer-w);
    height: var(--flyer-h);
    z-index: 5;
    cursor: pointer;
    filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.5));
}

.flyer.held {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 60;
    pointer-events: none; /* lets the click that lands the drop pass straight through to the board tile underneath */
}

.flyer-body {
    width: 100%;
    height: 100%;
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    background:
        radial-gradient(140% 120% at 15% 10%, rgba(255, 255, 255, 0.5), transparent 55%),
        linear-gradient(165deg, var(--paper), var(--paper-edge) 130%);
    color: var(--ink);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    padding: 4px 6px;
    text-align: center;
}

.flyer.held .flyer-body {
    animation: flyerShake var(--shake-dur, 0.44s) linear infinite;
}

@keyframes flyerShake {
    0%, 100% { transform: translate(0, 0) rotate(0deg); }
    20% { transform: translate(calc(var(--shake-amp, 1.5px) * -1), 0) rotate(calc(var(--shake-rot, 0.6deg) * -1)); }
    45% { transform: translate(var(--shake-amp, 1.5px), 0) rotate(var(--shake-rot, 0.6deg)); }
    70% { transform: translate(calc(var(--shake-amp, 1.5px) * -0.6), 0) rotate(calc(var(--shake-rot, 0.6deg) * -0.6)); }
}

.flyer-reading {
    font-size: clamp(0.7rem, 1.4cqw, 0.9rem);
    color: var(--ink-soft);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.flyer-text {
    font-family: var(--font-jp);
    font-size: clamp(1rem, 2.6cqw, 1.6rem);
    font-weight: 700;
    line-height: 1.15;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.flyer-wing {
    position: absolute;
    top: 24%;
    width: 44%;
    height: 52%;
    background: var(--gold-bright);
    clip-path: polygon(100% 0, 0 38%, 28% 52%, 0 74%, 100% 100%);
    opacity: 0.92;
}

.flyer-wing.l { left: -38%; animation: flyerWingL 0.34s ease-in-out infinite alternate; }
.flyer-wing.r { right: -38%; transform: scaleX(-1); animation: flyerWingR 0.34s ease-in-out infinite alternate; }

.flyer.held .flyer-wing { opacity: 0.32; transform: scale(0.72); animation: none; }
.flyer.held .flyer-wing.r { transform: scaleX(-1) scale(0.72); }

@keyframes flyerWingL {
    from { transform: rotate(-14deg) scaleY(0.82); }
    to { transform: rotate(12deg) scaleY(1.06); }
}
@keyframes flyerWingR {
    from { transform: scaleX(-1) rotate(-14deg) scaleY(0.82); }
    to { transform: scaleX(-1) rotate(12deg) scaleY(1.06); }
}

.flyer-clock {
    position: absolute;
    left: 50%;
    top: -22px;
    transform: translateX(-50%);
    font-size: 0.76rem;
    font-weight: 700;
    color: var(--gold-bright);
    font-variant-numeric: tabular-nums;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
    opacity: 0;
}

.flyer.held .flyer-clock { opacity: 1; }

/* While carrying: hide the system cursor over the board so the tile itself reads as the
   pointer (see .flyer.held above having pointer-events: none -- the cursor still needs to be
   suppressed separately since the underlying board tiles remain clickable/hoverable). */
.board-wrap.carrying-flyer { cursor: none; }
.board-wrap.carrying-flyer .tile { cursor: none; }

/* Shatter: the caught tile breaking apart on a miss or a wrong drop (see spawnShards() in
   game.js). Fixed-positioned like .flyer.held, since shatter only ever happens to a tile that
   was already being carried. Six wedges of the same hexagon clip-path split apart, distinct
   from .tile.cleared's plain fade-and-shrink -- this is meant to read as breaking, not clearing. */
.flyer-shard {
    position: fixed;
    background:
        radial-gradient(140% 120% at 15% 10%, rgba(255, 255, 255, 0.5), transparent 55%),
        linear-gradient(165deg, var(--paper), var(--paper-edge) 130%);
    pointer-events: none;
    z-index: 61;
    animation: flyerShardFly 0.7s var(--ease-out) forwards;
}

@keyframes flyerShardFly {
    0% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
    100% { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
}

/* ---------------------------------------------------------------------
   Side panel — example sentence + (wide layout only) progress/families-found blocks.
   Wide layout (default): a card sitting beside the board, sized by .board-layout's flex-basis
   (see above). Reverts to the original fixed full-width bottom bar under 968px -- see the
   media query at the bottom of this file, which is the only place position/left/right/bottom/
   z-index get set now (previously those applied at every width, since there was only ever the
   one bottom-bar layout).
   --------------------------------------------------------------------- */
.example-panel {
    flex: 0 0 290px;
    max-width: 290px;
    /* Caps how tall the panel can grow before it scrolls internally, so a long example
       sentence + several family chips can never stretch the card past a sane height (the
       board's own height is unaffected either way -- .board-layout's align-items: flex-start
       means each flex child is sized off its own content, not the tallest sibling -- this is
       purely so the panel itself doesn't run off screen on a short viewport). Not tied to the
       exact toolbar height above it; a round vh-based budget is enough for a safety cap. */
    max-height: calc(100vh - 200px);
    overflow-y: auto;
    background: linear-gradient(180deg, #1c1828, var(--ground-deep));
    border: 1px solid rgba(212, 166, 75, 0.2);
    box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(212, 166, 75, 0.05) inset;
    padding: 20px 18px;
    color: var(--paper);
}

.example-panel-inner {
    max-width: 1200px;
    margin: 0 auto;
}

.example-panel h3 {
    margin: 0 0 6px;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--gold);
}

/* Shared vertical rhythm for the panel's stacked blocks (progress / families found / last
   match) -- a thin divider between each, none after the last. */
.panel-block + .panel-block {
    margin-top: 18px;
    padding-top: 18px;
    border-top: 1px solid rgba(212, 166, 75, 0.15);
}

.panel-progress-pairs {
    margin: 0 0 10px;
    font-family: 'Fraunces', Georgia, serif;
    font-weight: 700;
    font-size: 1rem;
    color: var(--gold-bright);
}

.panel-progress-label {
    display: block;
    margin: 0 0 4px;
    font-size: 0.65rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--paper-muted);
}

.families-empty {
    margin: 0;
    font-size: 0.8rem;
    line-height: 1.5;
    color: var(--paper-muted);
}

.families-found-list {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

/* Gold overlay at low alpha, composited onto the panel's gradient background -- alpha chosen
   (0.1, matching .game-guest-hint's existing convention elsewhere in this file) so both chip
   text colors below still clear 4.5:1 with real margin: --gold-bright measures >=9.7:1 and
   --paper-muted >=5.4:1 against the composited chip background at both gradient stops. */
.family-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    background: rgba(212, 166, 75, 0.1);
    border: 1px solid rgba(212, 166, 75, 0.3);
    padding: 4px 9px;
    font-size: 0.78rem;
    line-height: 1.3;
}

.family-chip-kanji {
    font-family: var(--font-jp);
    font-weight: 700;
    color: var(--gold-bright);
}

.family-chip-reading,
.family-chip-label {
    font-size: 0.85em;
    color: var(--paper-muted);
}

.family-chip-label {
    text-transform: lowercase;
}

.example-empty {
    margin: 0;
    font-size: 1rem;
    color: var(--paper-muted);
}

.example-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 6px;
}

.example-jp {
    font-family: var(--font-jp);
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--paper);
}

.example-reading {
    color: var(--paper-muted);
    font-size: 1rem;
}

.example-meanings {
    color: var(--gold-bright);
    font-weight: 700;
    font-size: 1.05rem;
}

.example-sentence-block {
    border-top: 1px solid rgba(212, 166, 75, 0.15);
    padding-top: 6px;
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 4px 10px;
}

.example-sentence-jp {
    margin: 0;
    font-family: var(--font-jp);
    font-size: 1.15rem;
    color: var(--paper);
    line-height: 2.2;
}

.example-sentence-jp rt {
    font-size: 0.55em;
    font-weight: 400;
    color: var(--paper-muted);
    user-select: none;
}

.example-sentence-en {
    margin: 0;
    color: var(--paper-muted);
    font-size: 1rem;
}

.example-none {
    margin: 0;
    color: var(--paper-muted);
    font-size: 1rem;
    font-style: italic;
}

/* ---------------------------------------------------------------------
   Start / Result modals -- game-specific, dark themed. Scoped by ID so the
   shared .modal-overlay/.modal-card rules (also used by the light-themed
   auth/account modals elsewhere on this page) are untouched.
   --------------------------------------------------------------------- */
#start-modal .modal-card,
#result-modal .modal-card {
    background: linear-gradient(180deg, #201c2d, #171320);
    border: 1px solid var(--gold, #d4a64b);
    box-shadow: 0 0 60px rgba(212, 166, 75, 0.2), 0 30px 70px -20px rgba(0, 0, 0, 0.8);
    color: #f2e8d5;
}

#start-modal .modal-card h3,
#result-modal .modal-card h3 {
    font-family: 'Fraunces', Georgia, serif;
    color: #f4ce7a;
    text-shadow: 0 0 20px rgba(244, 206, 122, 0.35);
}

#start-modal .modal-card p,
#result-modal .modal-card p {
    color: #b6a996;
}

#result-modal .modal-close {
    color: #b6a996;
}

#result-modal .result-stat {
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid #2c2640;
}

#result-modal .result-value {
    color: #f4ce7a;
}

#result-modal .result-label {
    color: #b6a996;
}

#result-modal .result-best {
    color: #b6a996;
}

#result-modal .result-save-status {
    color: #7fbf9e;
}

#start-modal .auth-btn,
#result-modal .auth-btn {
    background: linear-gradient(135deg, #d4a64b, #b5842f);
    border: 1px solid #d4a64b;
    color: #171320;
}

#start-modal .auth-btn:hover,
#result-modal .auth-btn:hover {
    background: linear-gradient(135deg, #f4ce7a, #d4a64b);
}

#start-modal .auth-btn-outline,
#result-modal .auth-btn-outline {
    background: transparent;
    color: #f4ce7a;
    border-color: #d4a64b;
}

#start-modal .auth-btn-outline:hover,
#result-modal .auth-btn-outline:hover {
    background: rgba(212, 166, 75, 0.15);
    color: #f4ce7a;
}

/* ---------------------------------------------------------------------
   Result modal shared layout (unchanged from the pre-redesign structure)
   --------------------------------------------------------------------- */
.result-stats {
    display: flex;
    gap: 20px;
    margin: 10px 0 20px;
}

.result-stat {
    flex: 1;
    text-align: center;
    padding: 16px;
}

.result-value {
    display: block;
    font-size: 1.8rem;
    font-weight: 700;
}

.result-label {
    display: block;
    font-size: 0.85rem;
    margin-top: 4px;
}

.result-best {
    font-size: 0.9rem;
    margin: 0 0 16px;
}

.result-save-status {
    font-size: 0.9rem;
    margin: 0 0 16px;
}

.result-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* ---------------------------------------------------------------------
   Responsive
   --------------------------------------------------------------------- */
@media (max-width: 968px) {
    .game-main {
        /* 56px mobile masthead + the same ~25px clearance (was 130px for the 105px bar). */
        padding: 81px 20px 160px;
    }

    .game-hud-row {
        flex-direction: column;
    }

    .board-stats {
        margin-left: 0;
        width: 100%;
        justify-content: space-between;
    }

    /* Reverts the wide side-by-side layout back to the original stacked one: .board-layout
       drops its flex/max-width/centering (back to plain block flow), .board-wrap regains the
       standalone centering it used to do itself, and #example-panel goes back to being a fixed
       full-width bottom bar -- pixel-for-pixel the pre-redesign layout, not just "close to it".
       Every property the wide-layout .example-panel rule (above) sets is either restored to its
       old value here or explicitly neutralized (border/max-width/max-height/overflow-y) so
       nothing about being a flex child at wider widths leaks into this state; background and
       color are identical in both layouts so they're left to inherit rather than repeated. */
    .board-layout {
        display: block;
        max-width: none;
        margin: 0;
    }

    .board-wrap {
        max-width: 1180px;
        margin: 0 auto;
    }

    .example-panel {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 100;
        flex: none;
        max-width: none;
        max-height: none;
        overflow-y: visible;
        border: none;
        border-top: 1px solid rgba(212, 166, 75, 0.2);
        box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
        padding: 10px 20px;
    }

    /* Progress mirror + families-found chips only make sense as extra use of the side panel's
       spare width; the mobile bottom bar has no spare width to give them, so they simply don't
       render there rather than being crammed in. */
    .panel-wide-only {
        display: none;
    }
}
