/* Spelling Bee solver — page-scoped, linked from spelling_bee.html's extra_css
   block. Kept out of style.css (the intro.css and jumble.css precedent) so
   every other page stops paying for it.

   Everything here is prefixed .sb-. It deliberately mirrors jumble.css's
   measurements rather than inventing new ones — 44px controls, 6px button
   radius, 12px card radius, the shared hover lift — so the two solver pages
   feel like one tool. Colours come from the tokens in style.css; there is no
   local palette, with one exception noted at .sb-input::placeholder, which is
   the only place on the site that wants a colour deliberately WEAKER than the
   text beside it.

   Text on this page falls into two groups and they are not interchangeable:
   anything inside a panel (.sb-stat, the word pills, .sb-howto, the inputs
   themselves) sits on --op-panel and can use the ordinary tokens, while
   anything else sits directly on the brown paper texture and needs --op-ink
   plus a bold weight. See the block comment beside --op-ink in style.css for
   the measurements, and "Ink on the paper" below for which rules are in the
   second group.

   The form gate here is CSS, not JavaScript. `required` + `pattern` on the two
   inputs makes the browser itself refuse to submit an incomplete puzzle, and
   :has(:invalid) below is what fades the button to match. See CLAUDE.md on
   form state being CSS. */

/* ── Intro ──────────────────────────────────────────────────────────────── */

/* Two classes, so this beats .wl-intro's own colour on specificity rather than
   on which stylesheet happened to load last — the .sb-word-list.accordion-list
   precedent further down. .wl-intro is --op-dark, which is 4.74:1 on the paper;
   ink takes it to 5.62:1.

   Left at normal weight, unlike the small text below. This is three full lines
   of running prose and it is already 16px, so it is not the copy that was
   losing the fight with the grain — bolding it would flatten the page's one
   piece of ordinary reading into another label. */
.wl-intro.sb-intro {
    margin-bottom: 18px;
    color: var(--op-ink);
}

.sb-scroll-link {
    white-space: nowrap;
    margin-left: 6px;
}

.sb-arrow {
    display: inline-block;
    animation: sb-bounce 1.6s ease-in-out infinite;
}

@keyframes sb-bounce {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(3px); }
}

/* ── The form ───────────────────────────────────────────────────────────── */

.sb-form {
    margin-bottom: 26px;
}

.sb-fields {
    display: flex;
    flex-wrap: wrap;
    gap: 22px;
    align-items: flex-start;
    margin-bottom: 18px;
}

.sb-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.sb-field-label {
    font-weight: bold;
    font-size: 14px;
    color: var(--op-ink);
}

.sb-input-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Monospace and letter-spaced so six characters read as six tiles rather than
   a word. The size attribute on the input sets the width in characters, which
   is what keeps each box "just big enough" for the letters it takes — this
   only supplies the type treatment and the 44px touch height. */
.sb-input {
    font-family: 'Courier New', Courier, monospace;
    font-size: 20px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 3px;
    text-align: center;
    color: var(--op-text);
    background-color: var(--op-panel);
    border: 2px solid var(--op-border);
    border-radius: 6px;
    padding: 8px 10px;
    /* min-height rather than height, and border-box because this project has no
       global border-box rule — same reasoning as .jb-input. */
    box-sizing: border-box;
    min-height: 44px;
}

/* letter-spacing pushes the last character off-centre, since the tracking is
   applied after it too. Half of it back as padding re-centres the run. */
.sb-input-outer { padding-right: 7px; }
.sb-input-centre { padding-right: 7px; letter-spacing: 0; }

/* The placeholder is an example, not an answer, and at --op-text-faint it was
   8.73:1 on --op-panel — darker than plenty of real text on this page, so
   "ABCDEF" read as six letters already entered rather than as a hint about what
   to enter. #837d66 is 3.98:1 on the same surface: still comfortably legible,
   but visibly lighter than the value that replaces it. */
.sb-input::placeholder {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    font-weight: normal;
    letter-spacing: 1px;
    color: #837d66;
    text-transform: none;
}

/* Cleared on focus rather than on the first keystroke. The native behaviour
   keeps the hint on screen right up until a character lands, so the moment the
   box is active is exactly the moment it still looks occupied — click in and
   the caret sits among letters you have not typed. Going transparent on focus
   means the field is visibly empty before you type into it.

   color: transparent, not `content` or a JS swap: it keeps the placeholder in
   the accessibility tree (it is still the field's description for a screen
   reader, which does not care that it is invisible) and it animates nothing,
   so there is no flicker on tab-through. */
.sb-input:focus::placeholder {
    color: transparent;
}

/* The invalid state is deliberately NOT styled red while the field is still
   empty or half-typed — :invalid matches from the first keystroke, and a box
   that turns red as you begin typing reads as an error you have made rather
   than a field you have not finished. The counter carries the progress
   instead, and only the server's message ever colours the field. */
.sb-input:focus-visible,
.sb-btn:focus-visible {
    outline: 3px solid var(--op-stamp);
    outline-offset: 2px;
}

/* ── The green check ────────────────────────────────────────────────────── */

/* Shown once its own field is satisfied. opacity rather than display or the
   hidden attribute: a `display` declaration in our CSS outranks the UA's
   [hidden] { display: none }, which has shipped as a bug twice in this
   codebase (.word-panel, .more-info-link). Nothing here is ever given a
   display, so there is nothing to fight.

   This used to be #3f6b2b, the green .jb-ring-count[data-state="ok"] still
   uses. That sits on --op-panel; this one sits on the paper texture,
   where that green measures 1.81:1 and effectively disappears. --op-green-deep
   is 3.29:1 in the same spot and, unlike the darker green tried first, still
   looks green — which matters more here than the ratio does, because a check
   mark that has gone black is not reporting anything. At 22px bold this is
   large text, so 3:1 is the bar and it clears it. See the note beside the
   token in style.css before changing it. */
.sb-check {
    font-size: 22px;
    font-weight: bold;
    line-height: 1;
    color: var(--op-green-deep);
    opacity: 0;
    transition: opacity .15s ease;
}

.sb-field:has(.sb-input:valid) .sb-check {
    opacity: 1;
}

/* Was --op-text-muted at weight 600, on the reasoning that --op-text-faint
   reads recessive at small sizes (a finding recorded in style.css, where the
   element it came from has since been removed but the note kept).
   That reasoning held; the numbers behind it did not, because both tokens were
   measured against --op-bg rather than the paper actually behind them — muted
   is 3.48:1 there. Ink and full bold, since 12px is the smallest type on the
   page and it is sitting on a mottled photograph. */
.sb-counter {
    font-size: 12px;
    font-weight: bold;
    color: var(--op-ink);
}

/* The counter goes green and stays bold to match its check mark, so "done" is
   two signals rather than colour alone. */
.sb-field:has(.sb-input:valid) .sb-counter {
    color: var(--op-green-deep);
    font-weight: bold;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */

.sb-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
}

.sb-btn {
    font-family: inherit;
    font-size: 15px;
    font-weight: bold;
    padding: 13px 22px;
    border-radius: 6px;
    cursor: pointer;
    transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease, opacity .15s ease;
    box-sizing: border-box;
    min-height: 44px;
}

.sb-btn-primary {
    color: var(--op-panel);
    background-color: var(--op-stamp);
    border: 2px solid var(--op-stamp);
}

.sb-btn-primary:hover {
    background-color: var(--op-stamp-dark);
    border-color: var(--op-stamp-dark);
}

.sb-btn-quiet {
    color: var(--op-olive-dark);
    background-color: var(--op-panel);
    border: 1px solid var(--op-border);
    font-weight: 600;
    padding: 12px 16px;
}

.sb-btn-quiet:hover {
    transform: translateY(-3px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
    border-color: var(--op-olive);
}

/* The gate. The button is faded whenever either field fails its pattern, which
   is also exactly when the browser will refuse to submit — so the fade is
   honest about a restriction that already exists rather than being decoration
   over a JS check.

   No pointer-events: none, for the reason recorded on .jb-btn-reset: an element
   that receives no pointer events receives no cursor either, so not-allowed
   would never show, and that cursor is the one thing telling a mouse user why
   the click did nothing. Native validation blocks the submit regardless. */
.sb-form:has(.sb-input:invalid) .sb-btn-primary {
    opacity: .45;
    cursor: not-allowed;
}

/* A faded control that still rises to meet the cursor reads as clickable.
   Class + :has() + :hover outranks .sb-btn-primary:hover. */
.sb-form:has(.sb-input:invalid) .sb-btn-primary:hover {
    background-color: var(--op-stamp);
    border-color: var(--op-stamp);
    transform: none;
    box-shadow: none;
}

/* The reset is an <a>, not a <button> — it navigates to the bare URL, which is
   what actually clears the puzzle. Most of this makes a link sit level with the
   button beside it. */
.sb-btn-reset {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
    color: var(--op-olive-dark);
}

.sb-btn-reset:hover {
    text-decoration: none;
    color: var(--op-olive-dark);
}

.sb-btn-reset[aria-disabled="true"] {
    opacity: .45;
    cursor: not-allowed;
}

.sb-btn-reset[aria-disabled="true"]:hover {
    transform: none;
    box-shadow: none;
    border-color: var(--op-border);
}

/* Required, not belt-and-braces: the `display` declarations above outrank the
   UA's [hidden] { display: none }. */
.sb-btn[hidden],
.sb-results[hidden] {
    display: none;
}

/* ── Results summary ────────────────────────────────────────────────────── */

.sb-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 22px 0 16px;
}

.sb-stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    flex: 1 1 90px;
    padding: 12px 10px;
    background-color: var(--op-panel);
    border: 2px solid var(--op-border);
    border-radius: 12px;
}

.sb-stat-value {
    font-family: 'Courier New', Courier, monospace;
    font-size: 24px;
    font-weight: bold;
    color: var(--op-stamp);
    line-height: 1;
}

.sb-stat-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--op-text-faint);
    text-align: center;
}

/* ── Pangram callout ────────────────────────────────────────────────────── */

.sb-pangram-callout {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0 0 20px;
    padding: 12px 14px;
    background-color: var(--op-panel-alt);
    border: 2px solid var(--op-stamp);
    border-radius: 12px;
}

.sb-pangram-badge {
    font-size: 11px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--op-panel);
    background-color: var(--op-stamp);
    padding: 4px 9px;
    border-radius: 999px;
}

.sb-pangram-word {
    font-family: 'Courier New', Courier, monospace;
    font-size: 18px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 2px;
    color: var(--op-stamp);
}

/* ── Hints ──────────────────────────────────────────────────────────────── */

/* Everything inside the panel sits on --op-panel, so ordinary tokens apply; only
   the heading and note above it are on the paper and use --op-ink (via
   .sb-results-heading and .sb-tier-note). */
.sb-hint-panel {
    padding: 14px 16px 16px;
    background-color: var(--op-panel);
    border: 2px solid var(--op-border);
    border-radius: 12px;
}

/* Lengths 4 to 12 is eleven columns plus two; this scroll box, not the page,
   absorbs a grid wider than a phone. */
.sb-hint-scroll {
    overflow-x: auto;
}

.sb-hint-grid {
    border-collapse: collapse;
    font-family: 'Courier New', Courier, monospace;
    font-size: 15px;
    font-weight: bold;
    color: var(--op-olive-dark);
}

.sb-hint-grid th,
.sb-hint-grid td {
    min-width: 30px;
    padding: 5px 6px;
    text-align: center;
    border-bottom: 1px solid var(--op-border);
}

.sb-hint-grid thead th {
    color: var(--op-text-faint);
    font-size: 13px;
    border-bottom-width: 2px;
}

.sb-hint-grid tbody th {
    color: var(--op-stamp);
}

.sb-hint-grid tfoot th,
.sb-hint-grid tfoot td {
    border-bottom: none;
    border-top: 2px solid var(--op-border);
}

.sb-hint-total {
    color: var(--op-stamp);
}

.sb-hint-zero {
    color: var(--op-text-faint);
    font-weight: normal;
}

.sb-hint-grid abbr {
    text-decoration: none;
}

/* Narrow cells on a phone, so a typical grid (six lengths plus the letter and
   total columns) fits a 390px screen with the totals in view; only an unusually
   wide one falls back to .sb-hint-scroll. */
@media (max-width: 480px) {
    .sb-hint-panel {
        padding: 12px 10px 14px;
    }

    .sb-hint-grid {
        font-size: 14px;
    }

    .sb-hint-grid th,
    .sb-hint-grid td {
        min-width: 22px;
        padding: 4px 3px;
    }
}

.sb-hint-subhead {
    margin: 16px 0 8px;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--op-text-faint);
}

.sb-two-letter {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.sb-two-letter li {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 14px;
}

.sb-two {
    font-family: 'Courier New', Courier, monospace;
    font-size: 15px;
    font-weight: bold;
    color: var(--op-olive-dark);
}

/* ── Show answers ───────────────────────────────────────────────────────── */

/* A native <details>, styled as a button so it reads as a choice rather than a
   collapsed heading. The marker is removed and the label swaps with [open]. */
.sb-answers {
    margin-top: 20px;
}

.sb-answers-toggle {
    display: inline-block;
    margin-bottom: 16px;
    padding: 9px 16px;
    font-size: 14px;
    font-weight: bold;
    color: var(--op-panel);
    background-color: var(--op-stamp);
    border-radius: 8px;
    cursor: pointer;
    list-style: none;
}

.sb-answers-toggle::-webkit-details-marker {
    display: none;
}

.sb-answers-toggle:hover {
    background-color: var(--op-stamp-dark);
}

.sb-answers-toggle:focus-visible {
    outline: 3px solid var(--op-stamp);
    outline-offset: 2px;
}

.sb-answers-hide,
.sb-answers[open] .sb-answers-show {
    display: none;
}

.sb-answers[open] .sb-answers-hide {
    display: inline;
}

/* ── Word lists ─────────────────────────────────────────────────────────── */

/* Flex so the tier subtotal can sit at the end of the row. Wraps rather than
   squashing, which on a narrow screen drops the subtotal onto its own line
   still right-aligned.

   The colour is an "ink on the paper" case (see that section below): this
   inherited --op-text from body, which is 4.10:1 against the texture. The
   heading is already bold, so only the colour changes. */
.sb-results-heading {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px;
    margin: 24px 0 4px;
    font-size: 18px;
    color: var(--op-ink);
}

/* The known tier's heading shares its row with the sort control, which is a
   <fieldset> — flow content, invalid inside an <h2> — so the two are siblings
   in this wrapper and the outer margin moves up here. Only the known tier has
   one; the possible tier's heading is still a bare <h2>. */
.sb-tier-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 12px;
    margin: 24px 0 4px;
}

.sb-tier-head .sb-results-heading {
    margin: 0;
}

/* ── Sort control (native radio group, known tier only) ─────────────────── */

/* margin-left: auto is the same trick .sb-tier-subtotal uses to reach the end
   of its row. */
.sb-sort {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    margin-left: auto;
}

.sb-sort-label {
    margin-right: 4px;
    font-size: 12px;
    font-weight: bold;
    color: var(--op-ink);
}

/* Out of the visual flow but still focusable, still announced, and still
   submitted with the form — display: none would cost all three. Its :checked
   and :focus-visible states drive the label, exactly as .jb-candidate does. */
.sb-sort-opt input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.sb-sort-opt label {
    display: inline-block;
    padding: 7px 12px;
    font-size: 13px;
    font-weight: 600;
    color: var(--op-olive-dark);
    background-color: var(--op-panel);
    border: 1px solid var(--op-border);
    border-radius: 6px;
    cursor: pointer;
    transition: border-color .15s ease, background-color .15s ease;
}

.sb-sort-opt label:hover {
    border-color: var(--op-olive);
}

/* Checked carries weight and a solid border as well as colour, so the state is
   not colour-only. Padding drops 1px as the border gains 1px, or the chip jumps
   a pixel every time it is picked. */
.sb-sort-opt input:checked + label {
    border: 2px solid var(--op-stamp);
    padding: 6px 11px;
    color: var(--op-stamp);
    background-color: var(--op-panel-alt);
    font-weight: bold;
}

.sb-sort-opt input:focus-visible + label {
    outline: 3px solid var(--op-stamp);
    outline-offset: 2px;
}

/* ── Ink on the paper ───────────────────────────────────────────────────── */

/* Some text on this page sits directly on the paper texture with no panel under
   it, and that is what the --op-ink note in style.css is about. The rules
   concerned are NOT all below this heading — the file is ordered by page
   region, so .sb-field-label, .sb-counter, .sb-results-heading and
   .sb-sort-label are all members and all appear earlier. Each carries a pointer
   back here. What follows is the rest of them.

   Two weights, deliberately: figures and one-word labels take full bold,
   sentence-length text takes 600. Both are heavier than they were — stroke
   weight is doing as much work as colour here, because the backdrop is mottled
   rather than flat and a thin stroke loses against the grain in a way no
   contrast ratio expresses. But bolding a whole sentence at 700 turns a note
   into a warning, so the prose stops one step short.

   Two exclusions. The tiles (.sb-stat), the word pills and the pangram callout
   each carry their own --op-panel surface, so their existing --op-text-faint is
   8.7:1 and correct as it stands. And .sb-disclaimer is on the paper but stays
   quiet on purpose — see its own note further down. */

.sb-count {
    font-family: 'Courier New', Courier, monospace;
    font-size: 14px;
    font-weight: bold;
    color: var(--op-ink);
}

/* The "possible" tier's own count and points. It lives here rather than in the
   summary tiles because the tiles describe the puzzle — the known tier — and
   folding this into them is exactly the inflation being fixed. Same faint
   monospace treatment as .sb-count so it reads as a figure, not a subheading. */
.sb-tier-subtotal {
    margin-left: auto;
    font-family: 'Courier New', Courier, monospace;
    font-size: 12px;
    font-weight: bold;
    color: var(--op-ink);
}

.sb-tier-note {
    margin: 0 0 10px;
    font-size: 13px;
    font-weight: 600;
    color: var(--op-ink);
}

/* auto-fill rather than auto-fit so a short list keeps its column rhythm
   instead of stretching a handful of pills across the full width.

   148px, which is five columns in this page's 800px content width rather than
   four. It is deliberately NOT wide enough for the longest answers: a tile that
   never clips anything needs 171px (108px for `availability` plus 63px of
   chrome), and five of those do not fit. The two ways out of that were to clip
   the longest words or to widen .word-lists-page, and both were rejected —
   the longest word in a puzzle is very often the pangram (`availability` IS
   abltvy/i's pangram), and .word-lists-page is shared by every content page.
   So the rare over-long word takes two columns instead; see .sb-word-wide.

   The list also carries .accordion-list, whose own rules this overrides. Those
   overrides are written as .sb-word-list.accordion-list rather than relying on
   source order, because a single-class selector would tie on specificity and
   the winner would then depend on which stylesheet loaded last.

   position: relative is NOT cosmetic. It makes the list the offsetParent, so
   the offsetTop/offsetLeft unscrambler.js reads off a tile are grid-relative —
   which is the coordinate space --tab-left needs. Leave it static and the tab
   notch is placed against the viewport and lands nowhere near its tile.

   --pill-gap must equal the real gap: .word-panel subtracts it to trim the neck
   to --tab-neck. At the inherited 12px against this 6px grid the panel would be
   pulled 6px too far and overlap the row above. */
.sb-word-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 148px), 1fr));
    gap: 6px;
    list-style: none;
    padding: 0;
    margin: 10px 0 0;
}

.sb-word-list.accordion-list {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 148px), 1fr));
    gap: 6px;
    margin: 10px 0 0;
    position: relative;
    align-items: start;
    --pill-gap: 6px;
    --tab-neck: 4px;
    --tab-border: 2px;
}

/* The tile is now a container for the header rather than the text box itself:
   padding and the flex row moved down to .word-header, because that is the
   element the accordion opens and closes. What stays here is the tile's
   identity — surface, stroke, corner, and the monospace uppercase face the
   whole page is set in, which .word-header inherits.

   height 39px is the measured height of the pre-accordion tile, kept so the
   grid rhythm does not change: 17px of line box + 18px padding + 4px border.
   The invariant the tab geometry rests on is
       tile height = header height + 2 x border,
   so 39 = 35 + 2x2. Change the font size or the padding and both numbers move
   together or the open state will not line up. */
.sb-word {
    display: block;
    height: 39px;
    padding: 0;
    overflow: hidden;
    box-sizing: border-box;
    background-color: var(--op-panel);
    border: 2px solid var(--op-border);
    border-radius: 6px;
    font-family: 'Courier New', Courier, monospace;
    font-size: 15px;
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* The paddings and gaps here are the 16px that bought the fifth column, so they
   are load-bearing rather than taste: chrome went 63px -> 47px (4 border +
   8x2 padding + 4 gap + 14 score + 9 arrow), which is exactly what a 155px
   column can spare for an 11-letter word. Widen any of them and the grid
   silently drops back to four columns at 800px. */
.sb-word > .word-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px;
    height: 35px;
    --pill-pad-x: 8px;
    padding: 9px var(--pill-pad-x);
    font-weight: normal;
    color: inherit;
}

.sb-word .accordion-arrow {
    padding-left: 4px;
    font-size: 9px;
}

/* Words too long for one column take two rather than being ellipsised. Set by
   the template from the word's own length, because CSS cannot select on text
   content — and measured rather than guessed: at the narrowest five-column case
   (149px at a 1200px viewport) a tile holds 11 characters, so 12 and up span.
   Rare enough to cost nothing — 0, 1, 3 and 2 tiles on the four puzzles checked
   — and grid auto-placement absorbed every one of them without leaving a hole
   in any row. */
.sb-word-wide {
    grid-column: span 2;
}

/* The score sat at the far end of the tile before the accordion, pushed there
   by space-between on .sb-word itself. That job moves to the wrapper, because
   the header's own space-between now separates the wrapper from the arrow.
   Without this the score would tuck up against the word like the homepage's
   subscript, which is not how this page reads its tiles.

   baseline, not the wrapper's inherited center: the score is a <sub> at 11px
   beside 15px text, and centring the two makes the smaller one float. */
.sb-word .word-content-wrapper {
    justify-content: space-between;
    align-items: baseline;
    gap: 4px;
}

/* .word-header sets font-weight on itself, so the bold a pangram gets from
   .sb-word-pangram no longer reaches the text by inheritance. */
.sb-word.sb-word-pangram > .word-header {
    font-weight: bold;
}

/* Nothing to open, so nothing that looks openable. */
.sb-word.no-definition > .word-header {
    cursor: default;
}

/* The homepage pill goes 1px -> 2px on open and hands the 2px back out of the
   header's height and padding. A Bee tile is already 2px closed, so its border
   does not change and there is nothing to hand back — but the shared rules
   still compute 36px - 2px and 12px - 1px. Restating both is what keeps the
   text from shifting up 1px and left 1px at the moment of opening. */
.sb-word.word-accordion.is-open > .word-header {
    height: 35px;
    padding-inline: var(--pill-pad-x);
}

/* Hover marks the tile with the page's own accent rather than the homepage's
   olive, and a pangram keeps its accent border instead of losing it to hover. */
.sb-word.word-accordion:not(.is-open):hover {
    border-color: var(--op-olive);
}

.sb-word.sb-word-pangram.word-accordion:not(.is-open):hover {
    border-color: var(--op-stamp);
}

.sb-word-score {
    font-size: 11px;
    font-weight: bold;
    color: var(--op-text-faint);
    bottom: 0;
}

/* Pangrams get the page's accent, the same treatment ringed tiles get on the
   Jumble page. Border and weight carry it as well as colour, so it does not
   depend on colour alone. */
.sb-word-pangram {
    border-color: var(--op-stamp);
    background-color: var(--op-panel-alt);
    font-weight: bold;
}

.sb-word-pangram .sb-word-text {
    color: var(--op-stamp);
}

.sb-word-pangram .sb-word-score {
    color: var(--op-stamp);
}

/* ── The re-sort flip ───────────────────────────────────────────────────── */

/* Every tile turns top-over-bottom as the new order arrives, so a re-sort reads
   as the same words rearranging rather than the list silently becoming a
   different list.

   perspective() inside the transform rather than `perspective` on the <ul>:
   a container perspective has ONE vanishing point, at the centre of a grid that
   is often ten tiles wide, so tiles far from that centre rotate about a point
   off to their side and render visibly skewed while the middle ones do not.
   Per-element perspective gives each tile its own vanishing point and the
   whole row turns identically. 600px rather than something larger because
   these tiles are short — about 40px against 150px of width — and a distant
   vanishing point leaves so little foreshortening that an X-axis flip reads as
   a vertical squash rather than a turn. */
@keyframes sb-word-flip {
    from {
        opacity: 0;
        transform: perspective(600px) rotateX(-90deg);
    }
    to {
        opacity: 1;
        transform: perspective(600px) rotateX(0);
    }
}

/* Scoped twice over, and both halves matter:

   .sb-results-swapped — set by solve_spelling_bee, so this plays on an htmx
   re-sort or re-solve and NOT on a plain page load. A CSS animation fires when
   an element is inserted and cannot tell why it was inserted, so without a
   server-set marker a shared link would flip its entire word list on arrival.

   .sb-word-list-known — the sort control reorders that tier alone, so this is
   the only list whose contents actually moved. */
/* Every tile on the same clock, no stagger. A ripple was tried first, keyed on
   nth-child(4n+…) modulo buckets — but the grid is auto-fill, so the column
   count is whatever the viewport allows and almost never a multiple of four.
   The buckets therefore land at a different offset on each row and the list
   reads as tiles turning at random rather than as a wave. Per-column delays
   would fix the pattern and break at every other width; getting a true wave
   needs the index as a custom property per element, which means generating it
   server-side for up to several hundred tiles to buy an effect that is over in
   a third of a second.

   `backwards`, not `both`: both hold the FIRST keyframe before the animation
   starts, but `both` also holds the LAST one forever, and that keyframe carries
   a perspective(), so every tile would keep a 3D transform for the life of the
   page and a compositor layer with it. `backwards` settles to the underlying
   style, which is visually identical to the last keyframe — opacity 1, no
   transform — and leaves nothing composited. */
.sb-results-swapped .sb-word-list-known .sb-word {
    animation: sb-word-flip 300ms ease-out backwards;
}

/* ── Messages, how-to, disclaimer ───────────────────────────────────────── */

.sb-msg {
    margin: 8px 0 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--op-ink);
}

/* .sb-field is an unconstrained flex column, so without a cap here the error
   text's own preferred (one-line) width becomes the column's width — pushing
   whichever field sits to its right (the letters field pushing the centre
   field was the reported bug). Each field's label is what actually sets its
   natural column width today (136px / 168.7px, measured); these caps are
   just under that so the message wraps to two lines inside the field's
   existing width instead of growing it. Two different values because the two
   messages and two labels are different lengths — a single shared number
   either wraps one message an extra line for no reason or fails to cap the
   other. */
#sb-letters-error {
    max-width: 128px;
}

#sb-centre-error {
    max-width: 160px;
}

.sb-msg-error {
    color: var(--op-stamp);
    font-weight: bold;
}

.sb-howto {
    margin-top: 30px;
}

.sb-steps,
.sb-rules {
    margin: 0 0 14px;
    padding-left: 22px;
    line-height: 1.7;
}

/* Deliberately NOT in the ink-and-bold group above, even though it sits on the
   same paper. It is a trademark notice — it has to be present and readable, but
   it is the one thing on the page nobody came here to read, and pushing it to
   bold ink gave it the same visual weight as the answers. --op-text at normal
   weight is 4.10:1, which is quiet without being the 2.61:1 it started at, and
   it matches .jb-disclaimer exactly so the two solver pages treat their small
   print the same way. */
.sb-disclaimer {
    margin-top: 22px;
    font-size: 12px;
    color: var(--op-text);
}

/* ── Narrow screens ─────────────────────────────────────────────────────── */

@media (max-width: 600px) {
    .sb-fields {
        gap: 16px;
    }

    .sb-word-list {
        grid-template-columns: repeat(auto-fill, minmax(min(100%, 128px), 1fr));
    }

    .sb-stat {
        flex-basis: 70px;
        padding: 10px 6px;
    }

    .sb-stat-value {
        font-size: 20px;
    }

    /* Its own line rather than squeezed against the count. margin-left: 0 kills
       the push to the end of the row, which on a wrapped line would otherwise
       strand the chips on the right with the heading alone above them. */
    .sb-sort {
        margin-left: 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    .sb-arrow {
        animation: none;
    }

    /* The new order still arrives, it just arrives without turning. */
    .sb-results-swapped .sb-word-list-known .sb-word {
        animation: none;
    }

    .sb-btn,
    .sb-check,
    .sb-sort-opt label {
        transition: none;
    }

    .sb-btn-quiet:hover {
        transform: none;
    }
}
