/* Daily Cryptic — /puzzle
 *
 * Self-contained: this page does not load the site's style.css. The rest of
 * dakotacodelab.com is Inter on pale blue with hard black borders and offset
 * shadows; the puzzle is Fredoka and Nunito Sans on lilac. Two visual
 * languages under one roof is deliberate — the puzzle is a place you go and
 * stay, not another card in the carousel — but it means the tokens below must
 * not leak, so everything is scoped under .pz-body.
 */

.pz-body {
    /* §2 palette */
    --canvas: #F3F0FF;
    --surface: #FFFFFF;
    --grape: #2E2555;
    --grape-soft: #6B60A8;
    --sun: #FFCE3D;
    --sky: #BFE4FF;
    --bubble: #FF7EB0;   /* guest-clue accent — reserved, unused on the website */
    --leaf: #3FC17E;
    --coral: #FF6B5A;

    /* Not a §2 token. A lilac a shade below the canvas, used only to sink
       disabled controls into the page. */
    --muted: #E3DEF4;

    /* The site's brand green, taken from style.css's own token rather than the
       §2 palette: the "Dakota" in the wordmark is green everywhere else on
       dakotacodelab.com, and the puzzle page should not be the exception. The
       literal is a fallback for the case where style.css fails to load. */
    --brand-green: var(--green, #22c55e);

    --rule: 2px;
    --radius: 14px;

    --display: 'Fredoka', ui-rounded, system-ui, sans-serif;
    --ui: 'Nunito Sans', ui-rounded, system-ui, sans-serif;

    margin: 0;
    background: var(--canvas);
    color: var(--grape);
    font-family: var(--ui);
    font-weight: 400;
    line-height: 1.55;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    -webkit-text-size-adjust: 100%;
}

.pz-body *,
.pz-body *::before,
.pz-body *::after {
    box-sizing: border-box;
}

/* :where() so the reset contributes no specificity beyond .pz-body. Written as
   `.pz-body p` it scores (0,1,1) and silently outranks every single-class
   component rule that follows — `.pz-date { margin-top }` and friends were
   being dropped on the floor. */
.pz-body :where(h1, h2, h3, p, ol, ul, figure) {
    margin: 0;
    padding: 0;
}

.pz-body :where(ol, ul) {
    list-style: none;
}

.pz-body button {
    font: inherit;
    color: inherit;
}

/* Focus rings have to survive on white cells, on grape blocks and on the sun
   and sky highlights, so they are drawn outside the element rather than as a
   colour change on it. */
.pz-body :focus-visible {
    outline: 3px solid var(--grape);
    outline-offset: 2px;
    border-radius: 4px;
}

.pz-skip {
    position: absolute;
    left: -9999px;
    top: 0;
    background: var(--surface);
    color: var(--grape);
    padding: 0.75rem 1rem;
    z-index: 10;
    font-weight: 700;
}

.pz-skip:focus {
    left: 0;
}

/* Header / footer
 *
 * The navigation bar is the site's own — markup, styles and behaviour all come
 * from style.css and script.js, so it stays in step with the other pages
 * without a second copy to maintain. Nothing here restyles it. The one thing
 * this page has to do is clear it: .header is position:fixed, so the content
 * needs room to start underneath. */

.pz-footer {
    margin-top: auto;
    padding: 2.5rem 1rem 1.5rem;
    text-align: center;
    color: var(--grape-soft);
    font-size: 0.8rem;
    font-weight: 300;
}

.pz-main {
    width: 100%;
    max-width: 1100px;
    margin: 0 auto;
    /* Clears the fixed site header: 60px of logo plus its 1rem padding on
       each side, and 80px above the 768px breakpoint where the logo grows. */
    padding: calc(60px + 2rem + 1rem) 1rem 0;
}

@media (min-width: 768px) {
    .pz-main {
        padding-top: calc(80px + 2rem + 1rem);
    }
}

.pz-main:focus {
    outline: none;
}

/* Landing */

.pz-landing {
    max-width: 34rem;
    margin: 1rem auto 0;
    text-align: center;
}

.pz-eyebrow {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--grape);
}

/* Green "Dakota", as on the homepage header and hero. */
.pz-eyebrow span {
    color: var(--brand-green);
}

.pz-title {
    font-family: var(--display);
    font-weight: 600;
    font-size: clamp(2.75rem, 12vw, 4.5rem);
    line-height: 1;
    margin: 0.35rem 0 0;
}

.pz-title span {
    color: var(--grape-soft);
}

/* The second half of the name, set as its own line under the wordmark: at the
   title's size "- Mini" on the same line either wraps to an orphan or shrinks
   the whole thing to fit a phone. */
.pz-title-mini {
    display: block;
    font-size: 0.3em;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    margin-top: 0.35em;
}

.pz-date {
    /* Held off the title: the two were colliding as one block. */
    margin-top: 1.1rem;
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--grape-soft);
}

.pz-play {
    font-family: var(--display);
    font-weight: 600;
    font-size: 1.35rem;
    margin: 1.75rem 0 0.5rem;
    padding: 0.7rem 3rem;
    border: var(--rule) solid var(--grape);
    border-radius: 999px;
    background: var(--sun);
    cursor: pointer;
}

.pz-play[disabled] {
    background: var(--surface);
    color: var(--grape-soft);
    cursor: progress;
}

.pz-status {
    min-height: 1.5rem;
    font-size: 0.9rem;
    color: var(--grape-soft);
}

.pz-status[data-tone="error"] {
    color: var(--coral);
    font-weight: 700;
}

/* Switch and its explanation in one bounded block, so the copy is visibly
   about the control rather than about the page. */
.pz-prefs {
    margin: 1.25rem auto 0;
    max-width: 26rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.7rem;
    padding: 1rem 1.25rem;
    background: var(--surface);
    border: var(--rule) solid var(--grape);
    border-radius: var(--radius);
}

.pz-note {
    margin: 0;
    font-size: 0.85rem;
    font-weight: 300;
    line-height: 1.5;
    color: var(--grape-soft);
}

/* Hide-the-clock switch */

.pz-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 700;
}

.pz-switch input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}

.pz-switch-track {
    width: 44px;
    height: 26px;
    border: var(--rule) solid var(--grape);
    border-radius: 999px;
    background: var(--surface);
    display: inline-flex;
    align-items: center;
    padding: 2px;
    flex-shrink: 0;
}

.pz-switch-thumb {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--grape);
    transition: transform 0.18s ease;
}

.pz-switch input:checked + .pz-switch-track {
    background: var(--sky);
}

.pz-switch input:checked + .pz-switch-track .pz-switch-thumb {
    transform: translateX(18px);
}

.pz-switch input:focus-visible + .pz-switch-track {
    outline: 3px solid var(--grape);
    outline-offset: 2px;
}

/* Board layout.
   The side-by-side / stacked decision is keyed off the board's own inline
   size, not the viewport and not the puzzle's column count. A 15-wide grid on
   a narrow phone and a 5-wide grid on a desktop are the same question: is
   there room beside the grid for a clue list? */

.pz-board {
    container-type: inline-size;
    margin: 1rem 0 0;
}

.pz-board[hidden],
.pz-done[hidden],
.pz-explain[hidden] {
    display: none;
}

.pz-layout {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

@container (min-width: 52rem) {
    .pz-layout {
        display: grid;
        grid-template-columns: minmax(0, auto) minmax(18rem, 1fr);
        grid-template-areas:
            "bar     bar"
            "cluebar cluebar"
            "grid    clues"
            "hints   clues"
            "explain explain";
        align-items: start;
        column-gap: 2rem;
    }

    .pz-bar      { grid-area: bar; }
    .pz-cluebar  { grid-area: cluebar; }
    .pz-gridwrap { grid-area: grid; }
    .pz-hints    { grid-area: hints; }
    .pz-clues    { grid-area: clues; }
    .pz-explain  { grid-area: explain; }

    .pz-clues {
        grid-template-columns: 1fr;
    }

    /* Beside the grid there is now a clue list to pay for: the 18rem minimum
       column and the 2rem gutter come off the board before the columns are
       divided up. */
    .pz-grid {
        --fit: calc((100cqw - 20rem - var(--gutters)) / var(--cols));
    }
}

/* Top bar: clock and the in-play copy of the hide-clock switch */

/* The toggle is pinned to the end of the bar and the clock pushes itself away
   from it with an auto margin, rather than the two being spaced apart. When
   the clock is display:none the toggle therefore stays exactly where it was —
   space-between would have slid it across the page at the moment you pressed
   it, which reads as the control running away from the pointer. */
.pz-bar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 1rem;
    flex-wrap: wrap;
    min-height: 2.5rem;
}

.pz-clock {
    margin-right: auto;
}

.pz-clock {
    font-family: var(--display);
    font-weight: 600;
    font-size: 1.6rem;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* Clue bar — the entry you are on. aria-live, so it is announced on every
   change of entry. */

.pz-cluebar {
    background: var(--surface);
    border: var(--rule) solid var(--grape);
    border-radius: var(--radius);
    padding: 0.7rem 0.9rem;
    display: flex;
    gap: 0.6rem;
    align-items: baseline;
    min-height: 3.4rem;
}

.pz-cluebar-num {
    font-weight: 800;
    white-space: nowrap;
}

.pz-cluebar-text {
    font-weight: 400;
}

/* Grid */

/* Deliberately not a container itself. It used to be, and the cell size was
   measured against it — which is circular, because the width of this box is
   whatever the grid inside it turns out to be. The browser resolves that
   circle by calling the grid 0 wide, which was invisible only while the hint
   row underneath was propping the column open. Hide the hints on a solve and
   the column collapsed, dropping the grid to its 22px minimum and sliding it
   under the completion card. The measurements now come from .pz-board, whose
   width is settled before any of this is laid out. */
.pz-gridwrap {
    display: flex;
    justify-content: center;
}

.pz-grid {
    /* Defaults, and load-bearing ones. --cols and --rows are set inline from
       the puzzle data, but an unset var() poisons every declaration that
       mentions it: --cell becomes invalid, so grid-template-columns becomes
       invalid, so it computes to `none` and all 55 cells of an 11x5 stack into
       a single column. A browser holding a cached copy of an older puzzle.js
       against a fresh puzzle.css is enough to do that, and did — the script
       that sets --rows arrived in the same deploy as the stylesheet that reads
       it. The stylesheet now stands on its own; these values are never the ones
       used when the script is doing its job.

       Anything read from the script belongs here with a fallback. */
    --cols: 5;
    --rows: 5;

    /* Cell size follows from the puzzle's own column count, which is set
       inline from the data. The 94vw term is the phone case from §2; the
       container term keeps a wide grid inside its column once the clue list
       moves alongside it. Neither the count nor the aspect ratio is ever
       assumed here.

       --fit is the width the grid may spend per column. On a phone a height
       term joins it (see the compact block at the end of this file) so the
       puzzle also fits between the clue bar and the keyboard. */
    --fit: calc((min(94vw, 100cqw) - var(--gutters)) / var(--cols));
    --cell: clamp(22px, var(--fit), 52px);

    /* The grid lines are the gaps, plus the border of grape around the outside:
       one more than there are columns. Left out of the sum, a wide grid spent
       them off the end of its container and sat flush against both edges of
       the screen. */
    --gutters: calc((var(--cols) + 1) * var(--rule));

    display: grid;
    grid-template-columns: repeat(var(--cols), var(--cell));
    grid-auto-rows: var(--cell);
    gap: var(--rule);
    padding: var(--rule);
    background: var(--grape);
    border-radius: 6px;

    /* The browser keeps full control of panning, so a finger dragged across
       the grid scrolls the page as it would anywhere else. `manipulation`
       only gives up double-tap zoom, which would otherwise add a delay to
       every cell tap. */
    touch-action: manipulation;
    user-select: none;
    -webkit-user-select: none;
    /* The gap is the grid line: grape shows between white cells at exactly the
       2px §2 asks for, and a blocked square is the same grape, so it merges
       with the lines rather than being drawn on top of them. */
}

.pz-cell {
    position: relative;
    background: var(--surface);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    cursor: pointer;
    font-family: var(--ui);
    font-weight: 800;
    font-size: calc(var(--cell) * 0.5);
    line-height: 1;
    color: var(--grape);
    text-transform: uppercase;
    -webkit-tap-highlight-color: transparent;
}

.pz-cell-block {
    background: var(--grape);
    cursor: default;
}

.pz-cell-num {
    position: absolute;
    top: 1px;
    left: 2px;
    font-size: calc(var(--cell) * 0.26);
    font-weight: 700;
    line-height: 1;
    color: var(--grape-soft);
    pointer-events: none;
}

/* The current entry, then the current cell on top of it. These are the only
   two states the grid carries before a Check: where you are, and what you
   have typed. Nothing here knows whether a letter is right. */
.pz-cell-entry {
    background: var(--sky);
}

.pz-cell-active {
    background: var(--sun);
}

/* Wrong letters after a Check. Colour alone would not do it, so the cell also
   gets a corner wedge — visible whatever the display or the eyes reading it. */
.pz-cell-wrong {
    color: var(--coral);
}

.pz-cell-wrong::after {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    border-style: solid;
    border-width: 0 0 calc(var(--cell) * 0.3) calc(var(--cell) * 0.3);
    border-color: transparent transparent var(--coral) transparent;
}

/* Hints */

.pz-hints {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    justify-content: center;
}

/* Column, so each cost sits under the button it belongs to rather than beside
   it. Still a separate element from the button label — that part matters, it
   is what lets hiding the clock remove every mention of seconds. */
.pz-hint {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
}

/* Available and unavailable have to read at a glance, so they differ in more
   than tint: an enabled button is raised on a hard offset shadow, a disabled
   one is flat and sunk into the page. */
.pz-hint button {
    background: var(--surface);
    border: var(--rule) solid var(--grape);
    border-radius: 999px;
    padding: 0.5rem 1.1rem;
    font-size: 0.9rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 3px 3px 0 var(--grape);
}

.pz-hint button:active:not([disabled]) {
    transform: translate(3px, 3px);
    box-shadow: none;
}

.pz-hint button[disabled] {
    background: var(--muted);
    color: var(--grape-soft);
    border-color: var(--grape-soft);
    border-style: dashed;
    box-shadow: none;
    cursor: not-allowed;
    opacity: 0.7;
}

.hint-cost {
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--grape-soft);
    font-variant-numeric: tabular-nums;
}

/* Hiding the clock removes the costs outright rather than fading them, so they
   leave the accessibility tree with the pixels. A solver who has opted out of
   the timer should not hear "+15 seconds" read to them either. */
.pz-hide-clock .pz-clock,
.pz-hide-clock .hint-cost {
    display: none;
}

/* Clue lists */

.pz-clues {
    display: grid;
    gap: 1.5rem;
}

@media (min-width: 34rem) {
    .pz-clues {
        grid-template-columns: 1fr 1fr;
    }
}

.pz-clues h2 {
    font-family: var(--display);
    font-weight: 600;
    font-size: 1.1rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--grape-soft);
    margin-bottom: 0.4rem;
}

.pz-clue {
    display: flex;
    gap: 0.5rem;
    align-items: baseline;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    border-left: 3px solid transparent;
    padding: 0.35rem 0.5rem;
    border-radius: 0 8px 8px 0;
    cursor: pointer;
    font-size: 0.95rem;
}

/* Only ever the current entry. Never the solved ones: dimming a clue once its
   answer is right is the same correctness leak as colouring the cells, taking
   a different route to the solver's eye. */
.pz-clue-current {
    background: var(--sky);
    border-left-color: var(--grape);
}

.pz-clue-num {
    font-weight: 800;
    min-width: 1.6rem;
    flex-shrink: 0;
}

.pz-clue-text {
    flex: 1;
}

/* "Filled" is information the solver already has — they typed it. It says
   nothing about whether the letters are right.

   Marked by inverting the clue number rather than by another small dot: the
   difficulty pip on the right is already a dot, and two dots per row that mean
   entirely different things is a puzzle nobody asked for. */
.pz-clue-num {
    padding: 0 0.25rem;
    border-radius: 5px;
}

.pz-clue-filled .pz-clue-num {
    background: var(--grape);
    color: var(--surface);
}

/* The per-clue difficulty pip is not rendered. `level` is still carried on
   every entry in the data and the validator still checks it, so switching the
   indicator back on is a rendering change and not a content migration. */

/* The on-screen keyboard.
 *
 * There is no hidden input under the active cell any more, so tapping a square
 * raises nothing: the grid is made of divs and the phone has no reason to think
 * you are editing text. This is what you type on instead — pinned to the bottom
 * of the screen, always in the same place, never covering the puzzle, because
 * the page reserves its height underneath itself rather than sliding out from
 * under it.
 *
 * Shown on touch devices at any width, and on any window narrow enough to be
 * using the stacked layout. A wide window with a mouse does without: it has a
 * real keyboard already. */

.pz-keyboard {
    display: none;
}

/* Measured by the script and written back here, so the value is whatever the
   keyboard actually is at this width — and 0 the moment it is not drawn. */
.pz-body {
    padding-bottom: var(--pz-kb-h, 0px);
}

@media (max-width: 863px), (pointer: coarse) {
    .pz-keyboard {
        display: flex;
        flex-direction: column;
        gap: 0.4rem;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 900;
        padding: 0.45rem 0.3rem;
        /* Clears the home indicator on a modern iPhone; 0 everywhere else. */
        padding-bottom: calc(0.45rem + env(safe-area-inset-bottom, 0px));
        background: var(--muted);
        border-top: var(--rule) solid var(--grape);
        touch-action: manipulation;
        user-select: none;
        -webkit-user-select: none;
    }
}

.pz-kb-row {
    display: flex;
    justify-content: center;
    gap: 0.3rem;
}

/* Clue navigation, sitting above the letters: an arrow at each end and the
   entry you are on named in between. */
.pz-kb-nav {
    display: flex;
    align-items: center;
    gap: 0.3rem;
}

.pz-kb-here {
    flex: 1;
    min-width: 0;
    text-align: center;
    font-size: 0.85rem;
    font-weight: 800;
    color: var(--grape-soft);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.pz-kb-arrow {
    flex: 0 0 auto;
    width: 3.4rem;
    height: 2.2rem;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Half a square, turned. Drawn rather than typed so the chevron is the same
   weight as the grid lines and the borders around everything else. */
.pz-kb-arrow::before {
    content: '';
    width: 0.55rem;
    height: 0.55rem;
    border-left: 3px solid var(--grape);
    border-bottom: 3px solid var(--grape);
    /* The rotation leaves the shape sitting a touch right of centre. */
    margin-right: -0.15rem;
    transform: rotate(45deg);
}

.pz-kb-arrow[data-key="next"]::before {
    margin-right: 0;
    margin-left: -0.15rem;
    transform: rotate(-135deg);
}

.pz-kb-key {
    flex: 1 1 0;
    min-width: 0;
    max-width: 3rem;
    height: 2.6rem;
    padding: 0;
    background: var(--surface);
    border: var(--rule) solid var(--grape);
    border-radius: 8px;
    font-family: var(--ui);
    font-weight: 800;
    font-size: 1.05rem;
    line-height: 1;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

/* The only feedback a key gives. It has to land inside the press, so it is a
   state and not a transition. */
.pz-kb-key:active {
    background: var(--sun);
}

.pz-kb-wide {
    flex: 1.6 1 0;
    max-width: 4.5rem;
}

/* A solved puzzle takes no more letters. The script removes the keyboard
   outright on the last one; this covers the frame before it does. */
.pz-complete .pz-keyboard {
    display: none;
}

/* Short screens: a small phone, or any phone turned on its side. What gives is
   the keyboard, because a key's height is pure convention and a cell's is the
   puzzle. */
@media (max-width: 863px) and (max-height: 640px) {
    .pz-keyboard {
        gap: 0.3rem;
        padding: 0.35rem 0.3rem;
        padding-bottom: calc(0.35rem + env(safe-area-inset-bottom, 0px));
    }

    .pz-kb-key {
        height: 2.2rem;
    }

    .pz-kb-arrow {
        height: 1.9rem;
    }
}

/* Completion
 *
 * Once the puzzle is solved the clue list has done its job, so the completion
 * card takes its place: beside the grid on a wide screen, directly under it on
 * a narrow one. The clue bar and the hint buttons go with it — a disabled row
 * of hints under a finished grid is clutter. */

.pz-complete .pz-clues,
.pz-complete .pz-cluebar,
.pz-complete .pz-hints {
    display: none;
}

/* A solved grid is a picture of the answer. Nothing about it is interactive,
   and on a phone a stray tap must not raise the keyboard. */
.pz-complete .pz-grid {
    pointer-events: none;
}

.pz-complete .pz-cell {
    cursor: default;
}

.pz-done {
    margin: 1.5rem 0 0;
}

/* In the layout it inherits the grid gap, and on wide screens it occupies the
   column the clue list used to. The wordplay does not follow it there: a
   column narrow enough for a clue list is the wrong shape for paragraphs, and
   — less obviously — a column sized to its own contents cannot hold something
   as wide as those paragraphs want to be without squeezing the grid beside it
   down to nothing. It gets a full-width row of its own underneath. */
.pz-layout .pz-done {
    margin: 0;
}

@container (min-width: 52rem) {
    .pz-layout .pz-done {
        grid-area: clues;
    }
}

.pz-done-card {
    position: relative;
}

.pz-done-card {
    background: var(--surface);
    border: var(--rule) solid var(--grape);
    border-radius: var(--radius);
    padding: 1.5rem 1.25rem;
    text-align: center;
}

.pz-done-title {
    font-family: var(--display);
    font-weight: 600;
    font-size: clamp(1.75rem, 7vw, 2.5rem);
    color: var(--leaf);
    line-height: 1.1;
}

.pz-done-sub {
    color: var(--grape-soft);
    font-weight: 300;
}

.pz-done-breakdown {
    margin-top: 0.85rem;
    font-size: 0.85rem;
    color: var(--grape-soft);
}

.pz-show-time {
    margin-top: 0.85rem;
    background: var(--surface);
    border: var(--rule) solid var(--grape);
    border-radius: 999px;
    padding: 0.4rem 1.1rem;
    font-size: 0.85rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 3px 3px 0 var(--grape);
}

.pz-show-time:active {
    transform: translate(3px, 3px);
    box-shadow: none;
}

.pz-results {
    margin-top: 1.25rem;
}

/* The close button, and the covering treatment it belongs to.
 *
 * Below the width where the clue list moves alongside the grid, finishing the
 * puzzle used to leave the card somewhere far down the page with nothing to say
 * it was there. The card now floats over the puzzle and is dismissed with the
 * X, which drops it back into the flow beneath the grid. Above that width the
 * card is already beside the grid and in view, so none of this applies and the
 * button stays hidden.
 *
 * Only the card floats. The wordplay stays in the page underneath — it is a
 * page of reading, and putting it in a covering panel buried the one thing the
 * solver had just earned under fifteen explanations they had not read yet. */

.pz-done-close {
    display: none;
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    width: 2.25rem;
    height: 2.25rem;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    line-height: 1;
    background: var(--surface);
    color: var(--grape);
    border: var(--rule) solid var(--grape);
    border-radius: 50%;
    cursor: pointer;
}

/* The page behind the floating card: dimmed, so the card reads as being in
   front of the puzzle rather than as part of it, and tappable to dismiss. */
.pz-scrim {
    position: fixed;
    inset: 0;
    z-index: 1050;
    background: rgba(46, 37, 85, 0.45);
}

.pz-scrim[hidden] {
    display: none;
}

@media (max-width: 863px) {
    /* inset:0 with margin:auto and a content-sized height centres the card on
       both axes without a transform — which matters, because the solve
       animation is a transform and the two would fight. */
    .pz-done-card.pz-card-modal {
        position: fixed;
        inset: 0;
        z-index: 1100;
        width: min(100% - 2rem, 24rem);
        height: max-content;
        max-height: calc(100% - 2rem);
        margin: auto;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        box-shadow: 0 16px 40px rgba(46, 37, 85, 0.28);
    }

    .pz-done-card.pz-card-modal .pz-done-close {
        display: inline-flex;
    }

    /* The page behind a covering card should not scroll with it. */
    .pz-modal-open {
        overflow: hidden;
    }
}

/* Your figures against today's, side by side. A real table because that is
   what this is, and because the row and column headers are what make the
   comparison legible to a screen reader. */
.pz-compare {
    margin: 0 auto;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}

.pz-compare th,
.pz-compare td {
    padding: 0.4rem 0.9rem;
    text-align: right;
}

.pz-compare thead th {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--grape-soft);
    border-bottom: 1px dashed var(--grape-soft);
}

.pz-compare tbody th {
    text-align: left;
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--grape-soft);
}

/* Your number is the payoff, so it carries the display face and the size. The
   average is context, and stays quiet. */
.pz-compare-you {
    font-family: var(--display);
    font-weight: 600;
    font-size: 1.6rem;
    line-height: 1.2;
}

.pz-compare-avg {
    color: var(--grape-soft);
    font-weight: 700;
    font-size: 1.05rem;
}

.pz-stats-note {
    margin-top: 0.85rem;
    font-size: 0.75rem;
    font-weight: 300;
    color: var(--grape-soft);
}

.pz-parses {
    margin-top: 1.5rem;
}

.pz-parses h2 {
    font-family: var(--display);
    font-weight: 600;
    font-size: 1.25rem;
    margin-bottom: 0.75rem;
}

.pz-parse {
    background: var(--surface);
    border: var(--rule) solid var(--grape);
    border-radius: var(--radius);
    padding: 0.85rem 1rem;
    margin-bottom: 0.6rem;
}

.pz-parse-head {
    font-weight: 800;
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    align-items: baseline;
}

.pz-parse-answer {
    color: var(--leaf);
    letter-spacing: 0.06em;
}

.pz-parse-clue {
    font-weight: 400;
    color: var(--grape-soft);
    font-style: italic;
}

.pz-parse-body {
    margin-top: 0.35rem;
    font-weight: 300;
}

.pz-parse-body em {
    font-style: normal;
    font-weight: 700;
    color: var(--grape);
}

/* The phone layout.
 *
 * Below the width where the clue list moves beside the grid, the puzzle shares
 * the screen with the keyboard, and everything above it is competing for what
 * is left. So every band on the way down is put on a diet — the bar, the clue
 * bar, the hint row — and the grid itself is allowed to shrink to fit the space
 * that survives. The alternative was what this replaced: a grid whose first row
 * appeared at the bottom of the screen, under three rows of controls.
 *
 * Last in the file on purpose. These rules and the ones they override score the
 * same, so source order is what decides them. */

@media (max-width: 863px) {
    .pz-main {
        padding-top: calc(60px + 1rem);
    }

    .pz-layout {
        gap: 0.65rem;
    }

    .pz-board {
        margin-top: 0.5rem;
    }

    .pz-bar {
        min-height: 0;
        gap: 0.75rem;
    }

    .pz-clock {
        font-size: 1.3rem;
    }

    .pz-switch {
        font-size: 0.8rem;
        gap: 0.45rem;
    }

    .pz-switch-track {
        width: 38px;
        height: 22px;
    }

    .pz-switch-thumb {
        width: 14px;
        height: 14px;
    }

    .pz-switch input:checked + .pz-switch-track .pz-switch-thumb {
        transform: translateX(16px);
    }

    .pz-cluebar {
        padding: 0.5rem 0.7rem;
        min-height: 0;
        font-size: 0.95rem;
    }

    /* One row, never two. Three hint buttons wrapping onto a second line cost
       more height than the grid can spare, so they share the width instead. */
    .pz-hints {
        flex-wrap: nowrap;
        gap: 0.5rem;
    }

    .pz-hint {
        flex: 1 1 0;
        min-width: 0;
    }

    .pz-hint button {
        width: 100%;
        padding: 0.4rem 0.3rem;
        font-size: 0.78rem;
        box-shadow: 2px 2px 0 var(--grape);
    }

    .pz-hint button:active:not([disabled]) {
        transform: translate(2px, 2px);
    }

    .hint-cost {
        font-size: 0.7rem;
    }

    /* The height term. The keyboard reports its own height as --pz-kb-h, so
       only the rest has to be estimated: 16.5rem is the header clearance, the
       bar, the clue bar, the hint row and the gaps between them. --rows is set
       inline from the data, so a taller puzzle takes smaller cells rather than
       running off the bottom of the screen — and a keyboard that grows a row
       is taken out of the grid's budget rather than out of the last row of the
       puzzle. */
    .pz-grid {
        --cell: clamp(
            22px,
            min(var(--fit), (100dvh - var(--pz-kb-h, 10rem) - 16.5rem) / var(--rows)),
            52px
        );
    }
}

/* The logo is already at its larger size in this band, so the clearance above
   the board has to be too. */
@media (min-width: 768px) and (max-width: 863px) {
    .pz-main {
        padding-top: calc(80px + 1rem);
    }
}

/* Motion.
   One celebratory moment on full solve and nothing else: no entrance
   animations, no hover transitions, no per-letter feedback while solving —
   that last one is a correctness leak as well as a motion budget question. */

@keyframes pz-settle {
    0%   { transform: none; }
    40%  { transform: translateY(-14%) scale(1.06); }
    100% { transform: none; }
}

@keyframes pz-rise {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}

.pz-solved .pz-cell:not(.pz-cell-block) {
    animation: pz-settle 0.5s ease both;
    /* --wave is set per cell from its distance across the grid, so the pulse
       reads as one movement travelling over the puzzle rather than 55 cells
       each doing their own thing. */
    animation-delay: calc(var(--wave, 0) * 24ms);
}

/* Gated on .pz-solved, which is only set by an actual solve — revisiting a
   finished puzzle shows the card without replaying the moment. */
.pz-solved .pz-done-card {
    animation: pz-rise 0.35s ease both;
}

@media (prefers-reduced-motion: reduce) {
    .pz-body *,
    .pz-body *::before,
    .pz-body *::after {
        animation-duration: 0.001ms !important;
        animation-delay: 0ms !important;
        transition-duration: 0.001ms !important;
    }
}
