/* Own palette, deliberately not matched to any existing app in the genre. */
.block-game {
    /* Channel triples, so the ghost preview can reuse a piece's own colour at reduced alpha
       without restating the colour a second time. */
    --block-game-color-1-channels: 47, 111, 237;
    --block-game-color-2-channels: 18, 163, 107;
    --block-game-color-3-channels: 224, 161, 27;
    --block-game-color-4-channels: 214, 69, 93;
    --block-game-color-5-channels: 123, 86, 214;
    --block-game-color-6-channels: 21, 142, 168;

    --block-game-color-1: rgb(var(--block-game-color-1-channels));
    --block-game-color-2: rgb(var(--block-game-color-2-channels));
    --block-game-color-3: rgb(var(--block-game-color-3-channels));
    --block-game-color-4: rgb(var(--block-game-color-4-channels));
    --block-game-color-5: rgb(var(--block-game-color-5-channels));
    --block-game-color-6: rgb(var(--block-game-color-6-channels));

    --block-game-cell-gap: 2px;
    --block-game-empty: rgba(148, 163, 184, 0.18);
    /* The keyboard cursor is drawn as a dark, light, dark sandwich rather than in one colour, and
       these are the two. One colour cannot serve here: the cursor has to be visible on an empty cell
       and on all six piece colours, in a light and a dark theme, and the pair always puts a strong
       edge next to whatever is underneath because the two are 17:1 against each other. */
    --block-game-cursor: #ffffff;
    --block-game-cursor-edge: #0b1220;
    /* High enough that the preview reads as a piece against both a white and a near-black page,
       low enough that it never passes for an already placed piece. */
    --block-game-ghost-alpha: 0.62;
    --block-game-blocked-scrim: rgba(15, 23, 42, 0.45);

    /* Shared with renderer.js, which reads this property and times the deferred line clear on
       it. Editing it here changes both sides at once. */
    --block-game-clear-duration-ms: 180;

    /* The board and tray surfaces. Darker than the overlay behind them, so the two read as
       objects lying on it rather than as holes in it. */
    --block-game-board-surface: rgba(0, 0, 0, 0.22);
    --block-game-tray-surface: rgba(0, 0, 0, 0.14);

    display: flex;
    flex-direction: column;
    align-items: center;
    /* Pushed to the bottom rather than centred. On a phone the board is limited by width, not
       height, so a square board always leaves slack: centring splits it above and below, and the
       half below puts the tray out of comfortable thumb reach. All of it belongs at the top. */
    justify-content: flex-end;
    gap: 0.75rem;
    /* The reward layer positions itself against this box, so it has to be the containing block.
       The drag layer is position: fixed and does not care either way. */
    position: relative;
    /* Fills the host it is given rather than the whole viewport. The safe area is paid by the
       overlay chrome around this box, so paying it again here would pay it twice. */
    height: 100%;
    min-height: 0;
    padding: 0 0.75rem 0.75rem;
    overflow: hidden;
    user-select: none;
    -webkit-touch-callout: none;
}

/* The layout above adds percentages, gaps and padding up to the host's height, which only holds if
   padding sits inside the boxes. The app gets that from Bootstrap; the dev harness has no Bootstrap,
   so without this the harness measured a different game from the one children play. */
.block-game,
.block-game *,
.block-game *::before,
.block-game *::after {
    box-sizing: border-box;
}

/* Air under the tray, so board and tray sit higher than the bottom edge. The browsers put their
   fullscreen notice there for the first seconds of a game, over the pieces the child reaches for.
   Takes only slack: on a phone the board is width-bound and the room is spare, and where height is
   short the air shrinks to nothing before anything else gives. */
.block-game::after {
    content: '';
    flex: 0 1 15cqh;
    min-height: 0;
}

.block-game-board {
    flex-shrink: 0;
    display: grid;
    gap: var(--block-game-cell-gap);
    /* Square whichever way the host is shaped: bounded by the available width, and by the host's
       own height through cqh, which leaves room for the tray below. The anchor lookup derives
       row and column from one cell size, so a board that stopped being square would misplace
       every drop.

       The dvh line is the fallback, not dead weight: an engine without container query units
       drops the cqh declaration at parse time, and with no earlier width the grid would fall to
       width:auto, which for nine empty 1fr tracks is the gaps alone and renders a 16px board. */
    width: min(100%, 66dvh);
    /* The second bound is what is left of the height once the tray's 20%, the two gaps around it and
       the bottom padding are paid. 76cqh alone overran it on any host under about 900px tall, and the
       overrun came off the top of the board. */
    width: min(100%, 76cqh, calc(80cqh - 2.25rem));
    aspect-ratio: 1;
    touch-action: none;
    contain: layout paint;

    /* A board, not a grid floating in the page. The inset shadow sinks it slightly, which is what
       makes the pieces read as lying on top of something rather than next to nothing. */
    padding: var(--block-game-cell-gap);
    border-radius: 1rem;
    background: var(--block-game-board-surface);
    box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.45),
                0 1px 0 rgba(255, 255, 255, 0.04);
}

/* Play is blocked while the budget is spent or the game is over. The tray keeps its pieces so the
   preserved game state stays visible across the question gate, so the dimming is what tells the
   player the pieces are not draggable right now. */
.block-game.is-play-blocked .block-game-board,
.block-game.is-play-blocked .block-game-tray {
    opacity: 0.42;
}

.block-game.is-play-blocked .block-game-tray-piece {
    cursor: default;
}

.block-game-cell {
    background: var(--block-game-empty);
    border-radius: 18%;
}

/* A filled cell is a piece lying on the board, not a painted square. The top highlight and the
   bottom shadow are inset so they cost nothing in layout, and both are white and black at low
   alpha rather than per-colour values, so one rule works for all six colours and any added later.
   data-color is what the renderer sets, so this catches exactly the filled cells. */
.block-game-cell[data-color] {
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.32),
                inset 0 -3px 0 rgba(0, 0, 0, 0.28),
                0 1px 2px rgba(0, 0, 0, 0.35);
}

.block-game-cell[data-color="1"] { background: var(--block-game-color-1); }

.block-game-cell[data-color="2"] { background: var(--block-game-color-2); }

.block-game-cell[data-color="3"] { background: var(--block-game-color-3); }

.block-game-cell[data-color="4"] { background: var(--block-game-color-4); }

.block-game-cell[data-color="5"] { background: var(--block-game-color-5); }

.block-game-cell[data-color="6"] { background: var(--block-game-color-6); }

/* The legal preview paints the piece's own colour so the player sees where it lands. Declared
   after the placed colours, which it deliberately overrides on the cells it covers. */
.block-game-cell[data-ghost="1"] { background: rgba(var(--block-game-color-1-channels), var(--block-game-ghost-alpha)); }

.block-game-cell[data-ghost="2"] { background: rgba(var(--block-game-color-2-channels), var(--block-game-ghost-alpha)); }

.block-game-cell[data-ghost="3"] { background: rgba(var(--block-game-color-3-channels), var(--block-game-ghost-alpha)); }

.block-game-cell[data-ghost="4"] { background: rgba(var(--block-game-color-4-channels), var(--block-game-ghost-alpha)); }

.block-game-cell[data-ghost="5"] { background: rgba(var(--block-game-color-5-channels), var(--block-game-ghost-alpha)); }

.block-game-cell[data-ghost="6"] { background: rgba(var(--block-game-color-6-channels), var(--block-game-ghost-alpha)); }

/* Illegal never paints the piece's colour. It keeps whatever the cell already shows, darkens it
   and rings it, so a blocked drop stays distinguishable from a legal preview even when the cells
   underneath are already filled in the dragged piece's own colour. The scrim is a background
   image, which composites over the cell's own background colour rather than replacing it, and
   outlines take no part in layout. */
.block-game-cell[data-ghost="illegal"] {
    background-image: linear-gradient(var(--block-game-blocked-scrim), var(--block-game-blocked-scrim));
    outline: 3px solid var(--block-game-color-4);
    outline-offset: -3px;
}

/* Where the arrow keys are, which is the anchor cell of the piece the ghost is previewing. Inset
   shadows rather than an outline, because the illegal ghost above already owns the cell's outline and
   because a shadow takes no part in layout either. Static, so it costs one paint and never a frame:
   the cursor moves on a key press, not on a timer. */
.block-game-cell[data-cursor] {
    box-shadow:
        inset 0 0 0 1px var(--block-game-cursor-edge),
        inset 0 0 0 3px var(--block-game-cursor),
        inset 0 0 0 4px var(--block-game-cursor-edge);
}

.block-game-cell.is-clearing {
    animation: block-game-clear calc(var(--block-game-clear-duration-ms) * 1ms) ease-out forwards;
}

/* Flash, then go. Fading alone reads as the piece being removed rather than as something the
   player earned, and this is the only reward the loop has. */
@keyframes block-game-clear {
    0%   { filter: brightness(2.2); transform: scale(1.12); }
    35%  { filter: brightness(2.2); transform: scale(1.12); }
    100% { opacity: 0; transform: scale(0.4); filter: brightness(1); }
}

/* The floating score. Its own layer so a label can sit over the board without joining the grid,
   and pointer-events none so it never swallows a tap meant for a cell. */
.block-game-reward-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 3;
}

.block-game-reward {
    position: absolute;
    transform: translate(-50%, -50%);
    font-size: 1.6rem;
    font-weight: 800;
    color: #fff;
    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.75);
    white-space: nowrap;
    text-align: center;
    animation: block-game-reward-rise 1100ms cubic-bezier(0.17, 0.89, 0.32, 1.27) forwards;
}

/* Two lines at once is worth four times one, and nothing else in the game says so. */
.block-game-reward.is-big {
    font-size: 2.4rem;
    color: #fde047;
    text-shadow: 0 0 18px rgba(253, 224, 71, 0.6), 0 2px 10px rgba(0, 0, 0, 0.8);
}

/* A combo is the rarest thing the board does, so it gets its own colour as well as its size. */
.block-game-reward.is-combo {
    color: #fb923c;
    text-shadow: 0 0 22px rgba(251, 146, 60, 0.7), 0 2px 10px rgba(0, 0, 0, 0.8);
}

.block-game-reward-lines {
    display: block;
    font-size: 0.9rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.9;
}

@keyframes block-game-reward-rise {
    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
    18%  { opacity: 1; transform: translate(-50%, -60%) scale(1.15); }
    30%  { transform: translate(-50%, -62%) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -160%) scale(1); }
}

/* Two or more lines at once: the board takes the hit. Short and small on purpose, because a long
   or wide shake on a grid the player is aiming at reads as a fault rather than as impact. */
.block-game-board.is-shaking {
    animation: block-game-shake 260ms cubic-bezier(0.36, 0.07, 0.19, 0.97);
}

@keyframes block-game-shake {
    0%, 100% { transform: translate(0, 0); }
    15%      { transform: translate(-3px, 2px); }
    30%      { transform: translate(3px, -2px); }
    45%      { transform: translate(-2px, -2px); }
    60%      { transform: translate(2px, 2px); }
    80%      { transform: translate(-1px, 1px); }
}

/* Asked for less motion: the label still says what was earned, it just does not travel. */
.block-game-reward.is-still {
    animation: block-game-reward-hold 900ms ease-out forwards;
}

@keyframes block-game-reward-hold {
    0%   { opacity: 0; }
    15%  { opacity: 1; }
    70%  { opacity: 1; }
    100% { opacity: 0; }
}

.block-game-tray {
    display: flex;
    /* A definite share of the host, so the tray's own pixel-sized contents can never feed back
       into how much room the board gets. */
    flex: 0 0 20%;
    /* Never taller than its share, however tall its contents: a flex item otherwise grows to fit
       them, and the board above is what gave way. The renderer sizes the pieces to fit instead. */
    min-height: 0;
    justify-content: center;
    align-items: center;
    gap: 1rem;
    touch-action: none;

    /* A shelf the pieces rest on. Without it they float between the board and the bottom edge,
       and it is not obvious they are a set to choose from rather than part of the board. */
    width: min(100%, 76cqh, calc(80cqh - 2.25rem));
    border-radius: 1rem;
    background: var(--block-game-tray-surface);
    padding: 0.5rem 0.75rem;
}

.block-game-tray-piece {
    display: grid;
    gap: var(--block-game-cell-gap);
    cursor: grab;
}

/* is-used is a spent slot, is-lifted is a slot whose piece is currently in the player's hand and
   which comes back if the drag is abandoned. Both keep their footprint so the tray never reflows. */
.block-game-tray-piece.is-used,
.block-game-tray-piece.is-lifted { visibility: hidden; }

/* The slot the number keys picked up. Same two colours as the board cursor and for the same reason,
   here as a light ring inside a dark halo: the tray sits straight on the overlay background, which is
   white in one theme and near-black in the other, so a single colour would vanish in one of them.
   Neither an outline nor a shadow moves anything, so this costs no layout.

   What it needs is room, because .block-game clips at overflow: hidden. Both rings are measured from
   the same border box rather than from each other, so the painted halo reaches 6px out, not 4 plus 6:
   the outline occupies 2px to 4px and the shadow 0 to 6px. Sideways that is always clear, from the
   tray's own 1rem gap and this container's 0.75rem side padding. Downwards it is whatever the tray band
   leaves under the tallest piece, which is the one that can run short. Measured on the dev harness with
   a three-row piece selected: 95px on a 390x844 phone, 11.3px on a short 390x568 viewport, and 8.4px
   while playing at a 844x390 landscape one. The single state under 6px is that landscape viewport with
   a panel on screen (3.5px, so about 2.5px of the halo's bottom edge is cut), because the panel takes
   height off the board host. Left alone deliberately: the board is frozen in every state that draws a
   panel, the layout is portrait by design and asks the device for a portrait lock, and buying those
   2.5px means changing the height of a box the board sizes itself from. */
.block-game-tray-piece.is-selected {
    outline: 2px solid var(--block-game-cursor);
    outline-offset: 2px;
    box-shadow: 0 0 0 6px var(--block-game-cursor-edge);
}

.block-game-drag-layer {
    position: fixed;
    top: 0;
    left: 0;
    pointer-events: none;
    z-index: 20;
}

.block-game-drag-layer.is-hidden { display: none; }

@media (prefers-reduced-motion: reduce) {
    .block-game-score,
    .block-game-board.is-shaking,
    .block-game-over,
    .block-game-over-score,
    .block-game-over-record {
        animation: none;
    }

    .block-game-reward,
    .block-game-reward.is-still {
        animation: block-game-reward-hold 900ms ease-out forwards;
    }

    .block-game-cell.is-clearing {
        animation: none;
    }
}

.block-game-overlay {
    position: fixed;
    inset: 0;
    z-index: 1050;
    display: flex;
    flex-direction: column;
    background: var(--bg-primary, #0f172a);
    /* Set here rather than inherited: the body's colour was computed from the page's theme, and the
       overlay carries the dark one (game-dark-surface). */
    color: var(--text-primary);
    padding-bottom: env(safe-area-inset-bottom);
}

/* Suppresses pull-to-refresh while the game is open. The declaration has to sit on the document
   scroller to have any effect, and keying it off the overlay's presence means no teardown path
   can leave the rest of the app without it. */
html:has(.block-game-overlay) {
    overscroll-behavior: none;
}

.block-game-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem 1rem;
    padding-top: calc(0.75rem + env(safe-area-inset-top));
    font-variant-numeric: tabular-nums;
}

/* The score is what the loop pays out in, and it was the same weight as the budget beside it.
   The key on the element in the markup makes Blazor replace the node when the number changes,
   which is what restarts the animation: re-adding a class to a live node does not. */
.block-game-score {
    font-size: 1.35rem;
    font-weight: 800;
    color: #fde047;
    /* Grows to the right out of its own left edge, so the bump never reaches into the budget
       beside it. A centred origin made the two overlap at the top of the scale. */
    transform-origin: left center;
    display: inline-block;
    animation: block-game-score-bump 420ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes block-game-score-bump {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.2); text-shadow: 0 0 14px rgba(253, 224, 71, 0.7); }
    100% { transform: scale(1); }
}

/* Pushed to the right so the score's bump has the whole left side to itself. Crowding them
   together made the two overlap at the top of the scale whatever the gap was. */
.block-game-budget {
    margin-left: auto;
    opacity: 0.85;
}

.block-game-sound-button {
    background: none;
    border: none;
    color: inherit;
    font-size: 1.25rem;
    min-width: 2.75rem;
    min-height: 2.75rem;
    opacity: 0.75;
}

.block-game-sound-button[aria-pressed="true"] {
    opacity: 1;
    color: #fde047;
}

.block-game-header .block-game-close-button {
    /* The budget already takes the auto margin, so this one only needs its own spacing: a second
       auto would split the free space between them and pull the budget back towards the score. */
    margin-left: 0;
    background: none;
    border: none;
    color: inherit;
    min-width: 2.75rem;
    min-height: 2.75rem;
}

/* The close button is disabled only while the question gate is open, and the gate covers the header
   completely, so this styling is never expected to be seen. It exists so that a stylesheet or
   viewport that does leave the header visible shows a button that reads as unpressable rather than
   as broken: the author colour above would otherwise beat the browser's own disabled colour. */
.block-game-header .block-game-close-button:disabled {
    opacity: 0.5;
    cursor: default;
}

.block-game-host {
    flex: 1;
    min-height: 0;
    /* Makes the host the cqh reference for the board, so the board fits the room it is actually
       given instead of measuring itself against the viewport it does not own. */
    container-type: size;
}

/* The board is a focus target, because a key only reaches a Blazor handler by bubbling from the
   element that holds focus, and a closing gate hands focus to it so the overlay can answer Escape. It
   fills its box, where the global focus ring's positive outline-offset would be drawn outside it and
   clipped, hence the inset ring. The gate's own ring is in game-answer-screen.css.

   The [data-focus-visible] half is not decoration: app.css pairs the attribute with the pseudo-class
   everywhere for engines without native :focus-visible, and on those engines this rule would not match
   at all, leaving the ring to app.css's positive outline-offset. */
.block-game-host:focus-visible,
.block-game-host[data-focus-visible] {
    outline: 3px solid var(--accent-primary);
    outline-offset: -3px;
}

.block-game-panel {
    padding: 1rem;
    text-align: center;
}

/* Over the board, dimming it rather than replacing it: the shape the child built is the context
   the number belongs to, and hiding it makes the end of a round feel like leaving the game. */
.block-game-over {
    position: absolute;
    inset: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background: rgba(15, 23, 42, 0.82);
    backdrop-filter: blur(2px);
    animation: block-game-over-fade 260ms ease-out;
}

@keyframes block-game-over-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}

.block-game-over-card {
    text-align: center;
    max-width: 22rem;
    width: 100%;
}

.block-game-over-title {
    margin: 0 0 0.25rem;
    font-size: 1.1rem;
    opacity: 0.85;
}

/* Lands after the number has, so the child reads the score first and then what it means. */
.block-game-over-record {
    display: inline-block;
    margin: 0 0 0.5rem;
    padding: 0.2rem 0.8rem;
    border-radius: 999px;
    background: rgba(253, 224, 71, 0.18);
    color: #fde047;
    font-weight: 800;
    letter-spacing: 0.02em;
    animation: block-game-pop 450ms 600ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes block-game-pop {
    from {
        opacity: 0;
        transform: scale(0.6);
    }

    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* The number is the point of the screen, so it is the size of one. It travels from roughly where
   the running score sat in the header and grows on the way, which is what ties the figure the child
   watched all game to the one they ended on. */
.block-game-over-score {
    margin: 0;
    font-size: 5rem;
    line-height: 1;
    font-weight: 900;
    color: #fde047;
    text-shadow: 0 0 30px rgba(253, 224, 71, 0.35);
    font-variant-numeric: tabular-nums;
    animation: block-game-over-score 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes block-game-over-score {
    0% {
        /* Roughly the header's left edge, which is where the live score sat. */
        transform: translate(-32vw, -34vh) scale(0.26);
        opacity: 0;
    }
    30%  { opacity: 1; }
    100% { transform: translate(0, 0) scale(1); opacity: 1; }
}

.block-game-over-label {
    margin: 0.25rem 0 1.75rem;
    font-size: 1rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    opacity: 0.7;
}

.block-game-over-actions {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.block-game-over-actions .btn {
    padding: 0.9rem 1rem;
    font-size: 1.05rem;
    font-weight: 700;
    border-radius: 0.75rem;
}
