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

   Everything here is prefixed .jb-. Deliberately NOT reusing .word-accordion /
   .word-header / .word-panel: unscrambler.js delegates on those class names
   across the whole document — outside-click close, exclusive open, row-aware
   panel placement — and letter tiles would trip all three.

   Colours come from the tokens in style.css. There is no local palette here;
   --op-stamp is the site's accent and does the ringing. */

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */

.jb-form {
    margin: 0;
}

/* Wraps the reset link when it sits above the form — see jumble_solver.html
   for why it lives up here instead of beside Unscramble. */
.jb-top-actions {
    margin: 0 0 20px;
}

/* Two cards per row on desktop, one on narrow screens. auto-fit + minmax
   rather than a media query so the reflow point follows the card's own
   content width instead of a number that has to be maintained. */
.jb-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
    gap: 20px;
    margin-bottom: 20px;
}

.jb-card {
    margin-bottom: 0;
}

.jb-card-title,
.jb-card h2 {
    color: var(--op-olive-dark);
    font-size: 18px;
    margin: 0 0 10px;
}

.jb-card-title label {
    cursor: pointer;
}

/* --------------------------------------------------------------------------
   Intro
   -------------------------------------------------------------------------- */

.jb-intro {
    font-weight: bold;
}

/* --op-stamp, same as .jb-btn-primary's fill, so the link reads as loudly
   as the Unscramble button rather than blending into the intro's bold body
   text. The arrow sits inside the link so the whole phrase — text and arrow
   both — is one click target. */
.jb-scroll-link {
    color: var(--op-stamp);
    font-weight: bold;
    text-decoration: underline;
    text-decoration-color: var(--op-stamp);
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
}

.jb-scroll-link:hover {
    color: var(--op-stamp-dark);
    text-decoration-color: var(--op-stamp-dark);
}

.jb-arrow {
    display: inline-block;
    text-decoration: none;
    animation: jb-arrow-bounce 2s ease-in-out infinite;
}

@keyframes jb-arrow-bounce {
    0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
    40% { transform: translateY(6px); }
    60% { transform: translateY(3px); }
}

/* Shown only inside <noscript> — see jumble_solver.html. Styled like a
   warning rather than .jb-msg-error's plain inline text, since it's a
   page-level notice rather than feedback on one field. */
.jb-noscript-warning {
    margin: 0 0 20px;
    padding: 14px 16px;
    border: 2px solid var(--op-stamp);
    border-radius: 6px;
    background-color: var(--op-olive-soft);
    color: var(--op-text);
    font-size: 14px;
    line-height: 1.6;
}

/* --------------------------------------------------------------------------
   Text inputs
   -------------------------------------------------------------------------- */

.jb-input-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
}

/* Monospace + wide tracking so a typed scramble reads as separated letters
   rather than a word — the same trick .rack-chip uses on the homepage. */
.jb-input {
    font-family: 'Courier New', Courier, monospace;
    font-size: 18px;
    font-weight: bold;
    letter-spacing: 3px;
    text-transform: uppercase;
    padding: 10px 12px;
    border: 2px solid var(--op-border);
    border-radius: 6px;
    background-color: var(--op-panel);
    color: var(--op-text);
    min-width: 0;
    /* 16px+ computed size stops iOS Safari zooming the viewport on focus. */
    max-width: 100%;
    /* Padding alone lands at 42px once the font shrinks on mobile, so the
       minimum is stated rather than implied. There is no global border-box
       rule in this project, so it is set here too or the padding would be
       added on top of the 44. */
    box-sizing: border-box;
    min-height: 44px;
}

.jb-input::placeholder {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    font-size: 14px;
    font-weight: normal;
    letter-spacing: 0;
    text-transform: none;
    color: var(--op-text-faint);
    opacity: 0.75;
}

.jb-input:focus {
    border-color: var(--op-olive);
    outline: none;
}

.jb-input-hint,
.jb-field-hint {
    font-size: 13px;
    color: var(--op-text-faint);
    margin: 6px 0 0;
    line-height: 1.5;
}

.jb-input-hint {
    margin: 0;
}

.jb-field {
    margin-bottom: 18px;
}

.jb-field-label {
    display: block;
    font-weight: bold;
    font-size: 14px;
    color: var(--op-olive-dark);
    margin-bottom: 8px;
}

/* Grows to fit what is typed. Almost every puzzle needs only "4 6", but a
   finale that prints a word between the groups is typed "3 of your 5" and at a
   fixed 7em the reader cannot see what they wrote.

   field-sizing does this natively and is the whole mechanism — CSS keeps
   owning form layout here, per the rule in CLAUDE.md. min-width holds the old
   7em so an empty field looks unchanged; max-width stops a long glue word
   pushing .jb-shape-tally off the row.

   jumble.js supplies --jb-shape-chars as a fallback for engines without
   field-sizing, and only for those — see the @supports block below. */
.jb-shape-input {
    field-sizing: content;
    min-width: 7em;
    max-width: 22em;
    /* Undoes .jb-input's uppercase for this field only. That rule exists so a
       typed scramble reads as separated letters, which is right for a pool of
       tiles and wrong here: the shape is digits, and its only letters are the
       word the puzzle already printed. Uppercasing turned a typed "of your"
       into "OF YOUR", which reads as part of the answer rather than as the
       annotation it is — the same mistake the results list was making, and
       reported at the same time.

       Digits are unaffected by text-transform, so "3 of your 5" renders
       exactly as typed. The value submitted is untouched either way;
       parse_shape_and_layout lowercases for itself. */
    text-transform: lowercase;
}

/* The cartoon clue: a sentence, so it takes the full row and keeps its own
   case and spacing. .jb-input uppercases and letter-spaces for a pool of tiles,
   which is exactly wrong for prose — the same reason .jb-shape-input undoes it.

   No field-sizing here: a caption is long enough that growing the input would
   push it past the column, and a full-width box is what a sentence wants. */
.jb-clue-input {
    width: 100%;
    max-width: 34em;
    text-transform: none;
    letter-spacing: normal;
}

.jb-field-optional {
    font-weight: 400;
    opacity: 0.72;
}

/* Legacy path. An <input> cannot size to its content in CSS alone, so JS
   contributes the one number CSS cannot observe and CSS still does the sizing
   — the same division as the Spelling Bee's letter counter. The custom
   property is server-rendered from shape_raw as well, so a shared link
   carrying "3 of your 5" arrives at the right width before any JS runs. */
@supports not (field-sizing: content) {
    .jb-shape-input {
        width: clamp(7em, calc(var(--jb-shape-chars, 7) * 1ch + 2.5em), 22em);
    }
}

.jb-shape-tally {
    font-size: 13px;
    font-weight: bold;
    color: var(--op-text-muted);
    margin-left: 10px;
}

/* The site's one "fulfilled" green, shared with .jb-ring-count below and
   reused here rather than inventing a second one. */
.jb-shape-tally[data-state="ok"] {
    color: #3f6b2b;
}

/* Same red as .jb-msg-error, for a sum that's already too big to fix by
   typing more digits — not just "not there yet". */
.jb-shape-tally[data-state="error"] {
    color: var(--op-stamp);
    font-weight: 600;
}

#jb-shape[aria-invalid="true"] {
    border-color: var(--op-stamp);
}

/* --------------------------------------------------------------------------
   Answer tiles
   -------------------------------------------------------------------------- */

.jb-answer-block {
    margin-top: 14px;
}

.jb-answer-label {
    font-size: 13px;
    color: var(--op-text-muted);
    margin: 0 0 8px;
}

.jb-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

/* 44px square is the WCAG 2.2 / iOS minimum target, and it is the floor at
   every width — mobile shrinks the type inside, never the tile. */
.jb-tile {
    position: relative;
    width: 44px;
    height: 44px;
    padding: 0;
    font-family: 'Courier New', Courier, monospace;
    font-size: 20px;
    font-weight: bold;
    text-transform: uppercase;
    color: var(--op-text);
    background-color: var(--op-panel);
    border: 2px solid var(--op-border);
    border-radius: 6px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    cursor: pointer;
    transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}

.jb-tile:hover {
    /* The site's canonical hover, shared with .wl-pill-link and .detail-chip. */
    transform: translateY(-3px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
    border-color: var(--op-olive);
}

.jb-tile:active {
    transform: translateY(-1px);
}

/* Ringed. Three separate signals, none of them colour on its own: the drawn
   ring, the heavier border, and the visually-hidden "ringed" text in the
   markup. aria-pressed is the source of truth — there is no class to keep in
   sync, so the visual state cannot drift from the announced one. */
.jb-tile[aria-pressed="true"] {
    border-color: var(--op-stamp);
    color: var(--op-stamp);
    background-color: var(--op-panel-alt);
}

.jb-tile[aria-pressed="true"]::after {
    content: "";
    position: absolute;
    inset: 4px;
    border: 2px solid var(--op-stamp);
    border-radius: 50%;
    pointer-events: none;
}

/* The codebase had no :focus-visible before this page. Keyboard users get a
   clear ring; mouse users don't get one on click. */
.jb-tile:focus-visible,
.jb-btn:focus-visible,
.jb-input:focus-visible,
.jb-candidate input:focus-visible + label {
    outline: 3px solid var(--op-stamp);
    outline-offset: 2px;
}

.jb-ring-count {
    font-size: 12px;
    color: var(--op-text-faint);
    margin: 8px 0 0;
}

/* Same green as .jb-shape-tally[data-state="ok"] — set by jumble.js once
   every tile in the group is ringed. */
.jb-ring-count[data-state="ok"] {
    color: #3f6b2b;
    font-weight: bold;
}

/* --------------------------------------------------------------------------
   Candidate chooser (native radio group)
   -------------------------------------------------------------------------- */

.jb-candidates {
    border: none;
    padding: 0;
    margin: 14px 0 0;
}

.jb-candidates-legend {
    font-size: 13px;
    color: var(--op-text-muted);
    padding: 0;
    margin-bottom: 8px;
}

.jb-candidate {
    display: inline-block;
    margin: 0 6px 6px 0;
}

/* The radio itself is taken out of the visual flow but stays focusable and
   announced — its :checked and :focus-visible states drive the label. */
.jb-candidate input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.jb-candidate label {
    display: inline-block;
    /* 44px minimum height via padding + line-height, so the chip is a legal
       touch target without being a 44px-tall slab of text. */
    padding: 12px 14px;
    line-height: 18px;
    font-family: 'Courier New', Courier, monospace;
    font-size: 14px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--op-text);
    background-color: var(--op-panel);
    border: 1px solid var(--op-border);
    border-radius: 6px;
    cursor: pointer;
    transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}

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

/* Checked is marked by weight and a solid border as well as colour. */
.jb-candidate input:checked + label {
    border: 2px solid var(--op-stamp);
    padding: 11px 13px;
    color: var(--op-stamp);
    background-color: var(--op-panel-alt);
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.jb-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-bottom: 24px;
}

.jb-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;
    /* min-height rather than height, so a label that wraps on a narrow screen
       grows the button instead of being clipped. Measured at 43px on padding
       alone — one pixel under the target minimum, which is exactly the kind of
       miss that only a bounding-box check catches. */
    box-sizing: border-box;
    min-height: 44px;
}

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

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

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

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

/* The reset is an <a>, not a <button> — it navigates to the bare URL, which is
   what actually clears the puzzle. Most of this is about making a link sit
   level with the buttons beside it; the border is the section edge's 2px at
   Unscramble's stamp red, overriding .jb-btn-quiet's lighter 1px --op-border,
   so a control that is on the page permanently reads as page furniture rather
   than as a stray link above the form. Border only — the fill stays quiet, or
   it would compete with Unscramble for the one primary action on the page.
   Radius stays the buttons' 6px, not the card's 12px: it is still a button.
   The leading `a` is load-bearing, not decoration: this page is wrapped in
   .word-lists-page (base_content.html), whose `.word-lists-page a` rule in
   style.css underlines and recolors every link at the same (0,1,1)
   specificity as a bare `.jb-btn-reset` — so without the element qualifier
   here, source order let that generic rule win and the button rendered as
   an underlined red link instead of quiet page furniture. Same reasoning
   applies to the :hover rule below it. */
a.jb-btn-reset {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
    color: var(--op-olive-dark);
    border: 2px solid var(--op-stamp);
}

/* .jb-btn-quiet:hover swings the border to olive, which would throw away the
   stamp on hover. Same specificity, so this wins on source order alone —
   moving it above that rule silently restores the olive. Deepens to the same
   --op-stamp-dark .jb-btn-primary uses, so the two reds move together. */
a.jb-btn-reset:hover {
    text-decoration: none;
    color: var(--op-olive-dark);
    border-color: var(--op-stamp-dark);
}

/* Dimmed until there is a puzzle to throw away — see the comment on the link
   in jumble_solver.html for why it holds its place instead of appearing. The
   contrast drop is deliberate and permitted: WCAG 1.4.3 exempts inactive
   controls, and matching the live state would make "not yet" unreadable.
   opacity is what fades the border with it: the border is on this element,
   so one declaration covers text and edge together and they cannot drift.

   No pointer-events: none here, deliberately. It would block the mouse for
   free, but an element that receives no pointer events receives no cursor
   either — the not-allowed pointer below would never show, which is the one
   thing that tells a mouse user *why* nothing happens. Blocking the click is
   jumble.js's job instead. The keyboard needs no equivalent: this control is
   pointer-only by decision and carries tabindex="-1" in both states, with
   Enter swallowed on top. See jumble_solver.html. */
.jb-btn-reset[aria-disabled="true"] {
    opacity: .45;
    cursor: not-allowed;
}

/* .jb-btn-quiet:hover lifts and shadows every quiet button, and now that the
   pointer reaches this one while disabled it would do it here too — a control
   that rises to meet the cursor and then does nothing. Outranks that rule on
   specificity (class + attribute + pseudo-class). */
.jb-btn-reset[aria-disabled="true"]:hover {
    transform: none;
    box-shadow: none;
    border-color: var(--op-stamp);
}

/* Required, not belt-and-braces: the `display` declarations above outrank the
   UA's [hidden] { display: none }, and this codebase has shipped that bug
   twice (.word-panel, and .more-info-link's fourteen stray rack links). */
.jb-btn[hidden],
.jb-tile[hidden],
.jb-card[hidden] {
    display: none;
}

/* --------------------------------------------------------------------------
   Finale
   -------------------------------------------------------------------------- */

.jb-finale {
    margin-bottom: 20px;
}

.jb-finale-intro {
    font-size: 14px;
    color: var(--op-text-muted);
    margin: 0 0 16px;
}

/* The only visible (and, via role="status" in the markup, accessible) record
   of the ringed-letter pool now that there is no backing text input — see the
   comment in _jumble_finale.html. */
.jb-pool-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-bottom: 10px;
    /* .jb-pool-tile is 32px content-box plus a 2px border on each side (no
       box-sizing: border-box in scope here) — 36px reserves exactly one row
       so the fields below don't jump down the instant the first tile is
       ringed. */
    min-height: 36px;
}

/* Same square shape and idle colours as .jb-tile — these are a read-only echo
   of tiles already ringed, not a separate "this one means something else"
   control, so they shouldn't look like one. */
.jb-pool-tile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    font-family: 'Courier New', Courier, monospace;
    font-size: 15px;
    font-weight: bold;
    text-transform: uppercase;
    color: var(--op-text);
    background-color: var(--op-panel);
    border: 2px solid var(--op-border);
    border-radius: 6px;
}

.jb-results {
    margin-top: 22px;
    border-top: 1px solid var(--op-border);
    padding-top: 16px;
}

.jb-results-heading {
    font-size: 15px;
    color: var(--op-olive-dark);
    margin: 16px 0 8px;
}

.jb-results-heading:first-child {
    margin-top: 0;
}

.jb-truncated {
    font-weight: normal;
    font-size: 13px;
    color: var(--op-text-faint);
}

.jb-phrase-list {
    list-style: none;
    margin: 0;
    padding: 0;
    /* As many columns per row as the card's own width fits — no media query
       needed, and it degrades to a single column on narrow screens same as the
       old column-width approach did. Unlike the old column layout (which
       filled one column top-to-bottom before starting the next, so a 5th
       result could land under result #1 instead of after #4), this reads in
       rank order left-to-right, top-to-bottom, and a dangling last row starts
       fresh at the left rather than under some arbitrary earlier column.
       PHRASE_DISPLAY_LIMIT in core/jumble/common.py caps this at 25 entries, so a
       handful of rows shows all of them without a height limit or a scrollbar.

       Grid, and a track sized to the longest phrase that can occur, because a
       finale must never wrap. Two lines reads as two separate answers — the
       results are a list of guesses at one punchline, so "COUNT LESS / ONES"
       invites parsing as a phrase break that isn't there. The old flex basis
       was a flat 140px and anything past ~13 characters wrapped, which was
       most of them.

       The track is sized from the longest phrase ACTUALLY RENDERED, which the
       view measures and passes as --jb-phrase-chars. It used to be a flat
       170px, deliberately narrower than a 19-character worst case derived from
       MAX_POOL_LEN (16) + MAX_SHAPE_WORDS - 1, relying on the overflow leaning
       into the 24px of clearance between neighbours: 191 - 170 = 21px, with a
       width sweep finding 1px of slack at the tightest point. That invariant
       died the moment MAX_POOL_LEN became 18 — a 21-character phrase leans
       41px, well past the clearance — and rather than re-deriving a new
       constant that the next cap change would break again, every list now
       measures itself. Short phrases get narrower tracks and MORE columns than
       the old constant allowed, long ones get what they need, and no cap in
       core/jumble/ can invalidate it.

       `ch` must resolve in this element's own font, which is why the
       font-family and font-size are restated on each list — see the note on
       .jb-phrase-list-glued. 1fr rather than a fixed track so the columns
       divide the card's full width instead of leaving a ragged gutter, and
       min(100%, …) so a card narrower than one track collapses to a single
       column instead of overflowing it. */
    display: grid;
    font-family: 'Courier New', Courier, monospace;
    font-size: 15px;
    grid-template-columns: repeat(
        auto-fill,
        minmax(min(100%, calc(var(--jb-phrase-chars, 19) * (1ch + 1px) + 4px)), 1fr)
    );
    column-gap: 20px;
    /* Last-resort valve. Every case above fits, but a phone under ~300px CSS
       pixels leaves the known list's widest phrase a few pixels over — better
       that the list scrolls than that the text spills out of the card. */
    overflow-x: auto;
}

/* Known phrases run bigger (17px, below) and carry the star, so the same
   character count needs more room here. Sized from its own longest rendered
   phrase like the list above, with the star's 22px added; the old flat 240px
   was derived from `cost an arm and a leg` being the longest phrase in
   core/cliches.py whose letters could fit a legal pool, which stopped being
   true when MAX_POOL_LEN moved. */
.jb-phrase-list-known {
    font-size: 17px;
    grid-template-columns: repeat(
        auto-fill,
        minmax(min(100%, calc(var(--jb-phrase-chars, 21) * (1ch + 1px) + 26px)), 1fr)
    );
}

/* A printed glue word breaks the arithmetic above. That 170px track is
   measured for MAX_POOL_LEN (16) letters plus MAX_SHAPE_WORDS - 1 (3) spaces
   = 19 characters, and a glued phrase is nothing like that: two printed words
   at MAX_LAYOUT_GLUE plus their spaces can push "out of your weigh" style
   readings well past 28 characters, which would lean ~80px into 24px of
   clearance and land on the neighbouring column.

   Sized from the longest phrase actually rendered rather than from a second
   guessed constant — the view passes its character count as
   --jb-phrase-chars. `ch` is exact here because .jb-phrase is Courier New, a
   monospace face; the +1px is its letter-spacing, and the 4px is the li's 2px
   of padding either side. min() keeps a narrow card collapsing to one column
   instead of overflowing, exactly as the base rule does.

   Only the constructed list takes this; `known` carries no layout. */
.jb-phrase-list-glued {
    /* `ch` resolves against THIS element's font, not against .jb-phrase's, and
       the calc below is the only reason that matters. Without these two lines
       the unit came from the inherited body face at its own size, which is
       neither monospace nor 15px, and the track came out ~10% narrow: a
       21-character phrase rendered 210px wide into a 189px column and leaned
       21px into the one beside it. Restating the child's font-family and
       font-size here changes nothing visually — .jb-phrase sets both again for
       the text itself — it only makes 1ch mean what the comment above claims
       it means. Measured at 15px Courier New: 9px advance + 1px letter-spacing
       = the 10px per character this arithmetic assumes. */
    font-family: 'Courier New', Courier, monospace;
    font-size: 15px;
    grid-template-columns: repeat(
        auto-fill,
        minmax(min(100%, calc(var(--jb-phrase-chars, 19) * (1ch + 1px) + 4px)), 1fr)
    );
}

/* The printed word itself: lowercase and muted against the solved words'
   uppercase, so the line reads as answer-plus-annotation rather than as one
   run of equal words. Overrides .jb-phrase's text-transform for this span
   only. */
.jb-glue {
    text-transform: lowercase;
    color: var(--op-text-muted);
    font-weight: normal;
}

.jb-phrase-list li {
    padding: 6px 2px;
    border-bottom: 1px solid var(--op-panel-alt);
}

.jb-phrase-list li:last-child {
    border-bottom: none;
}

.jb-phrase {
    font-family: 'Courier New', Courier, monospace;
    font-size: 15px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--op-text);
    /* The actual guarantee that a finale stays on one line. The grid track
       above is sized so this never has to overflow, but the track is the
       thing that can be wrong — a longer phrase, a wider font, a browser that
       rounds differently — and if it is, this fails by pushing into the
       gutter, where the answer is still readable, rather than by breaking a
       punchline across two lines. */
    white-space: nowrap;
}

.jb-phrase-list-known .jb-phrase {
    color: var(--op-stamp);
    font-size: 17px;
}

.jb-star {
    color: var(--op-stamp);
    margin-right: 6px;
}

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

.jb-msg {
    font-size: 13px;
    color: var(--op-text-muted);
    line-height: 1.5;
    margin: 12px 0 0;
}

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

/* A correction that was applied, not a problem to fix — deliberately quieter
   than .jb-msg-error. It sits under a field whose value now differs from what
   the results used, so it has to be visible, but the puzzle solved. */
.jb-msg-notice {
    color: var(--op-text);
    font-weight: 600;
}

.jb-howto .jb-steps {
    margin: 0 0 12px;
    padding-left: 22px;
    color: var(--op-text);
    line-height: 1.7;
}

.jb-howto .jb-steps li {
    margin-bottom: 6px;
}

.jb-howto p {
    font-size: 14px;
    color: var(--op-text-muted);
}

/* The shape marks a reader can type. Full ink rather than .jb-howto p's muted
   14px: these are instructions to follow, not the small print around them. */
.jb-shape-guide {
    margin: 0 0 12px;
    padding-left: 22px;
    color: var(--op-text);
    line-height: 1.7;
}

.jb-shape-guide li {
    margin-bottom: 8px;
}

/* Matches .home-brief code, which is the site's only other inline-code style. */
.jb-shape-guide code {
    font-family: 'Courier New', Courier, monospace;
    font-weight: bold;
    background-color: var(--op-panel-alt);
    border: 1px solid var(--op-border);
    border-radius: 4px;
    padding: 1px 5px;
    white-space: nowrap;
}

.jb-howto a {
    color: var(--op-olive-dark);
    font-weight: bold;
    text-decoration: underline;
    text-decoration-color: var(--op-olive-border);
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
}

.jb-howto a:hover {
    text-decoration-color: currentColor;
}

.jb-disclaimer {
    font-size: 12px;
    line-height: 1.6;
    color: var(--op-text);
    margin: 4px 0 20px;
    padding: 0 4px;
}

/* --------------------------------------------------------------------------
   Narrow screens
   -------------------------------------------------------------------------- */

@media (max-width: 600px) {
    .jb-grid {
        /* One column. The desktop minmax would still fit two 330px tracks on
           a 700px tablet, which is right; below 600 it must not try. */
        grid-template-columns: 1fr;
        gap: 16px;
    }

    /* Buying width for the answer row, which is the one thing on this page
       that must not wrap: the tiles map one-to-one onto the circled letters in
       a printed puzzle, and a six-letter answer broken as 5 + 1 is markedly
       harder to check against it. A six-tile row needs 6x44 + 5x4 = 284px; the
       card's default 22px side padding left only 286px at 390px, so it wrapped
       from the iPhone 12/13/14 width down. Trimming the padding and the gap
       holds one row to ~370px, which covers the iPhone SE at 375px. Below
       that, and for seven-letter answers anywhere, it wraps — unavoidable
       without going under the 44px target, which is not worth trading. */
    .jb-card {
        padding-left: 12px;
        padding-right: 12px;
    }

    .jb-tiles {
        gap: 4px;
    }

    /* Type shrinks, the 44px target does not. */
    .jb-tile {
        font-size: 17px;
    }

    .jb-input {
        font-size: 16px;
        letter-spacing: 2px;
    }

    .jb-actions .jb-btn {
        flex: 1 1 auto;
    }
}

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

    .jb-tile,
    .jb-btn,
    .jb-candidate label {
        transition: none;
    }

    .jb-tile:hover,
    .jb-btn-quiet:hover,
    .jb-candidate label:hover {
        transform: none;
    }
}
