:root {
    --op-bg: #FEF1DC;
    --op-panel: #fdfbf3;
    --op-panel-alt: #f5f0df;
    --op-dark: #1b2118;
    --op-dark-soft: #2a3320;
    --op-text: #2b2b26;
    --op-text-muted: #3a362a;
    --op-text-faint: #4d4936;
    --op-border: #cfc9b0;
    --op-olive: #2b2b26;
    --op-olive-rgb: 139, 58, 58;
    --op-olive-dark: #1b2118;
    --op-olive-bright: #a3c37a;
    --op-olive-soft: #f3ded9;
    --op-olive-strong: #e0b7ac;
    --op-olive-border: #c17f6e;
    --op-card-border: #000000;
    --op-stamp: #8b1a1a;
    --op-stamp-dark: #6e1414;
    --op-cream: #e8e2d0;
    --op-cream-muted: #b6b09c;
    --op-disabled-bg: #736c5c;
    --op-disabled-text: #fdfbf3;

    /* --- Colours for text sitting directly ON the paper texture --- */
    /* Everything above is measured against --op-bg (#FEF1DC), which is a
       reasonable reference for text on --op-panel but is NOT what body copy
       actually sits on: body::before paints brown_textured_paper.jpg at full
       opacity across the viewport, so the real backdrop is a mid-brown
       averaging #a98c64 (relative luminance 0.284 median, 0.253 at the 10th
       percentile of the image). Measured against that, the tokens above are
       far weaker than their comments claim:

           --op-text        12.76:1 on --op-bg  ->  4.10:1 on the texture
           --op-text-muted  10.82:1             ->  3.48:1
           --op-text-faint   8.11:1             ->  2.61:1
           #3f6b2b (green)   5.62:1             ->  1.81:1

       The texture sits near the middle of the luminance range, which is the
       worst place for it: NOTHING reaches 7:1 against it, in either direction
       — pure black tops out at 6.06:1 and pure white at 3.47:1. So text on the
       paper cannot be fixed by reaching for a lighter or darker version of the
       same idea; it needs the darkest end plus extra stroke weight. These two
       are that end, and both are used with a bold weight wherever they appear.

       --op-ink is black to the eye (5.62:1, against pure black's 6.06:1) with
       a trace of warmth so it does not read as a hole punched in the paper.

       --op-green-deep replaces #3f6b2b for the Spelling Bee's valid-field
       signals. #3f6b2b stays in place for .jb-ring-count and .orf-msg-notice,
       which sit on --op-panel rather than the paper and are fine there.

       This one is a judgement call, not a maximum, because green and contrast
       pull in opposite directions on brown: the darker a green gets the better
       it measures and the less green it looks, and the paper is brown enough
       that anything reading as properly green lands between 2 and 3.5:1. The
       first attempt here was #162b0c at 4.37:1 and it was rejected on sight —
       it measured best of anything tried and read as plain black, which makes
       it a worse signal than the faint green it replaced, since the whole job
       of this colour is to say "this field is satisfied" at a glance.

       #1e4212 is 3.29:1 at the texture's 10th percentile — 1.8x the green it
       replaces, and visibly green. That clears AA for the 22px bold check mark
       (large text needs 3:1) but not for the 12px counter beside it, which
       formally wants 4.5:1. Accepted deliberately: the counter states its
       meaning in words, and the "satisfied" state is carried by the check, the
       bold weight and the text itself, so the colour is the redundant signal
       rather than the only one. Do not darken this back toward black without
       re-reading that trade-off — it has already been made once and reverted.

       Numbers from scratchpad/texture_contrast.py; re-measure if the texture
       image is ever swapped. */
    --op-ink: #0d0d09;
    --op-green-deep: #1e4212;
}

/* ==========================================================================
   1. CORE GLOBAL RESETS & GENERAL BODY LAYOUT
   ========================================================================== */
/* The page's base colour lives on html, not body, and body carries no
   background at all. Both matter, for the same reason.

   This colour is the canvas background: what the browser fills its compositor
   surface with before anything has rasterised, what it reuses for navigation
   transitions, and what shows for however long the 573 KB paper texture takes
   to arrive. It reaches the canvas either way — from html directly, or
   propagated up from body when html has none — so the visible result is
   identical in the ordinary case.

   It has to be html specifically because of the texture layer below.
   body::before paints at step 2 of the stacking order (negative z-index
   positioned descendants) while body's own background paints at step 3, so
   any background on body covers the texture. That stays hidden only while
   body's background is being propagated away to the canvas rather than
   painted on body — and propagation stops the moment html is given a
   background of its own, which is exactly what the homepage intro does to
   black out the canvas. Leaving the colour on body made the texture vanish
   for the duration of the intro and pop back in at cleanup (measured).
   Putting it on html means nothing ever paints between the texture and the
   canvas, in any state. */
html {
    background-color: var(--op-bg);
    scroll-behavior: smooth;
}

body {
    margin: 0;
    padding: 0;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: var(--op-text);
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100dvh;
}

.page-row {
    display: flex;
    flex: 1;
    align-items: stretch;
    width: 100%;
    box-sizing: border-box;
    margin-bottom: 44px;
    border: 16px solid;
    border-image: repeating-linear-gradient(
        45deg,
        var(--op-stamp) 0 22px,
        transparent 22px 34px
    ) 16;
}

.main-content {
    flex: 1;
    min-width: 0;
    padding: 20px;
    box-sizing: border-box;
}

/* Homepage only: the search page's own heading stack sits right under the
   striped border with nothing else above it, so the shared 20px reads as a
   bigger gap here than it does on a page whose first element is body text.
   Scoped with :has() rather than edited in place so every other page using
   .main-content keeps the standard padding. */
.main-content:has(.search-page-wrapper) {
    padding-top: 10px;
}

/* ==========================================================================
   2. STICKY MODERN SITE NAVIGATION HEADER
   ========================================================================== */
.main-header {
    background-color: var(--op-dark);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
    position: sticky;
    top: 0;
    z-index: 1000;
}

/* 1140px, not 1000px. Measured: the logo side wants 560.8px at the 58px
   ceiling and the nav wants 520.5px once the Solvers dropdown is in it — 1081px
   of content that was being squeezed into a 1000px box. Both are flex items
   with the default flex-shrink: 1, so nothing overflowed visibly; they simply
   compressed until "Word Operative", "Word Lists" and "Training Center" each
   broke onto a second line. See "Header slack" in CODEBASE_NOTES.md.

   Widening the header does not misalign it with anything: .page-row is
   width: 100%, so the content column was never tied to this cap. */
.header-container {
    max-width: 1140px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 20px 8px 8px;
}

.site-logo-link {
    text-decoration: none;
    color: inherit;
}

.site-logo {
    margin: 0;
    font-family: 'SpecialElite', sans-serif;
    /* Everything on the logo side of the header scales with this: the title is
       7.81x the font-size and the logo image plus its flex gap is 1.858x, so
       the logo side costs 9.668 x font-size. That figure is what the header
       arithmetic in CODEBASE_NOTES.md is built on — re-derive it if the logo
       text or the tagline text changes.

       This base rule applies only where the nav is collapsed behind the
       hamburger and the logo therefore has the row to itself. Above the
       collapse breakpoint the nav is on screen and takes a fixed ~520px, so the
       ceiling has to answer to it instead — see the min-width: 941px block. */
    font-size: clamp(38px, 5vw, 58px);
    font-weight: normal;
    color: var(--op-cream);
    -webkit-text-stroke: 0.4px var(--op-cream);
    display: flex;
    align-items: center;
    gap: 0.3em;
}

.logo-img {
    height: 2em;
    width: auto;
    display: block;
    flex-shrink: 0;
}

.logo-text-group {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 0;
}

/* The paper texture, pinned to the viewport rather than painted on body.
   body's own background-attachment: fixed does not survive on mobile — Chrome
   for Android resolves it to `scroll`, which hands `cover` the whole document
   box to fill instead of just the viewport. On a Pixel 7 that is a 2348px-tall
   area rather than a 915px one, so a 2000px-tall source got stretched over it
   and then multiplied by the 2.625x device pixel ratio: roughly a 3x upscale,
   which is what was visibly softening the grain. A fixed-position layer gives
   `cover` the viewport regardless of page height or platform, taking that to
   ~1.2x with the same image file.

   position: fixed keeps this out of flow, so it does not become a flex item of
   body despite body being display: flex (verified — it adds no box to the
   column). z-index: -1 puts it behind page content while still painting above
   the canvas, which is html's background-color.

   AVIF first, JPEG as the fallback: 400 KB against 560 KB, on every page.
   Encoded at quality 60, measured at 40.2 dB PSNR against the JPEG, and
   approved side by side (2026-09-30). The texture is mostly fine grain, which
   is what codecs smooth away, so this is close to the floor: WebP only beat
   the JPEG by softening it (34.6 dB at a 37% saving) and AVIF q50 saved 48% at
   visibly softer grain. The plain url() line is for engines without typed
   image-set(), which drop the second declaration as invalid. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    background-image: url("../images/brown_textured_paper.7024671bef34.jpg");
    background-image: image-set(
        url("../images/brown_textured_paper.34d6bd62be6d.avif") type('image/avif'),
        url("../images/brown_textured_paper.7024671bef34.jpg") type('image/jpeg')
    );
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    /* Decorative only, and it spans the whole viewport — without this it would
       be the hit target anywhere page content does not cover. */
    pointer-events: none;
}

/* Flex columns stretch children to the container's cross-size by default.
   .logo-tagline's own font-size formula can make it wider than the title's
   natural text width at some viewports (e.g. ~412px), which — via that
   default stretch — was making .site-title-text's box wider than its actual
   text. Purely a bounding-box effect (text stays left-aligned and renders
   identically either way) until intro.js started measuring this element
   directly via getBoundingClientRect() for the homepage intro animation,
   where the inflated box shifted its computed center and opened a visible
   gap next to the logo. */
.site-title-text {
    align-self: flex-start;
}

.logo-tagline {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
    font-weight: 600 !important;
    font-style: italic;
    color: var(--op-cream-muted) !important;
    letter-spacing: 0.04em;
    line-height: 1.3;
    white-space: nowrap;
    -webkit-text-stroke: 0px !important;
    display: block;
    padding-left: 2px;
    overflow: hidden;

    /* Sized to match the width of "Word Operative" above it.
       Both strings and both font families are fixed, so the size that makes the
       two widths equal is a constant multiple of the title's font-size — it does
       not need measuring at runtime. Matching exactly would be 0.301, i.e. (title
       width per 1px of font-size, 7.81) / (tagline width per 1px, 25.95), both
       measured in-browser. `em` in a font-size resolves against the parent, so
       this is relative to .site-logo.

       Deliberately 0.298, ~1% under that. .logo-text-group is as wide as whichever
       of the two is wider, and the tagline is nowrap while the title is not. At an
       exact match the two are in a dead heat and sub-pixel rounding decides, which
       made the title flip to two lines at some widths (770px was one). Undershooting
       keeps the title the wider of the two, so it always governs — the ~3px the
       tagline gives up is not perceptible.

       Two media queries adjust this: ≥769px also caps by the room the visible nav
       leaves, and ≤600px fills the leftover width instead. Between them (601-768px)
       the nav is collapsed and space is ample, so this plain match is what applies. */
    font-size: 0.298em;
}

/* Above the nav-collapse breakpoint the nav links are on screen and take a fixed
   ~520px, so both the logo and the tagline have to fit what is left of the
   header row. 549px is that nav (520.5px, re-measured with the Solvers
   dropdown) plus the header's 28px padding.

   Both formulas below are measured constants. Re-derive them whenever the nav
   links change — CODEBASE_NOTES.md has the procedure, and the trap that
   .nav-menu must be measured with flex-shrink disabled or it reports its
   compressed width rather than the width it actually wants. */
@media (min-width: 941px) {
    /* The logo ceiling answers to the nav rather than to the viewport alone.
       568.5px is the nav's 549px plus ~20px of deliberate slack, so a browser
       whose font metrics differ slightly from ours still has somewhere to put
       the difference — the old rule left exactly zero and wrapped as soon as
       anything moved. 9.668 is the logo side's width per 1px of font-size.

       Above 1140px viewport .header-container stops growing, so 100vw
       overstates the room; the 58px ceiling takes over there and the check is
       the static one: 560.8 logo + 520.5 nav = 1081.2 inside 1112px. */
    .site-logo {
        font-size: clamp(38px, calc((100vw - 568.5px) / 9.668), 58px);
    }

    /* 1.858em is the logo image (1.558em) and flex gap (0.3em), which scale
       with the title. 25.95 is the tagline's width per 1px of font-size. The
       8px floor matches what the old JS used. */
    .logo-tagline {
        font-size: max(8px, min(0.298em, calc((100vw - 549px - 1.858em) / 25.95)));
    }
}

.nav-menu {
    display: flex;
    gap: 5px;
}

/* Only ever shown alongside the off-canvas drawer below 940px; see that
   media query for the rest of its rules. */
.nav-backdrop {
    display: none;
}

.nav-link {
    text-decoration: none;
    color: var(--op-cream-muted);
    font-weight: 600;
    font-size: 15px;
    padding: 8px 16px;
    border-radius: 4px;
    position: relative;
    /* A nav label is a label, not prose — "Word Lists" breaking across two
       lines is always wrong, and it is how the header's overcrowding used to
       show up rather than something worth allowing. With this, the sizing
       formulas above are the only thing keeping the row honest, which is the
       point: a squeeze now has to be fixed rather than absorbed silently. */
    white-space: nowrap;
    transition: color 0.2s ease, background-color 0.2s ease;
}

.nav-link:hover {
    color: var(--op-olive-bright);
    background-color: var(--op-dark-soft);
}

.nav-link.active {
    color: var(--op-olive-bright);
    background-color: var(--op-dark-soft);
}

.nav-link.active::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 16px;
    right: 16px;
    height: 3px;
    background-color: var(--op-stamp);
    border-radius: 2px;
}

/* --- Solvers dropdown ---------------------------------------------------
   Holds the two puzzle solvers so the nav keeps five top-level items. The
   toggle is a <button> carrying .nav-link, so it inherits every nav state
   (hover, active, the stamp underline) without duplicating them — it only has
   to undo the UA button chrome. */
.nav-dropdown {
    position: relative;
}

.nav-dropdown-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: inherit;
    background: none;
    border: none;
    cursor: pointer;
}

.nav-caret {
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid currentColor;
    transition: transform 0.2s ease;
}

.nav-dropdown-toggle[aria-expanded="true"] .nav-caret {
    transform: rotate(180deg);
}

/* Absolutely positioned so opening the menu cannot change the header's height
   and shove the page down. */
.nav-dropdown-menu {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 1100;
    display: flex;
    flex-direction: column;
    min-width: 170px;
    padding: 6px;
    background-color: var(--op-dark);
    border: 2px solid var(--op-dark-soft);
    border-radius: 8px;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}

.nav-dropdown-link {
    text-decoration: none;
    color: var(--op-cream-muted);
    font-weight: 600;
    font-size: 15px;
    padding: 9px 14px;
    border-radius: 4px;
    white-space: nowrap;
    transition: color 0.2s ease, background-color 0.2s ease;
}

.nav-dropdown-link:hover,
.nav-dropdown-link.active {
    color: var(--op-olive-bright);
    background-color: var(--op-dark-soft);
}

/* Required, not belt-and-braces: the `display: flex` above outranks the UA's
   [hidden] { display: none }, which is exactly the bug .word-panel and
   .more-info-link both shipped. Without this the menu is permanently open. */
.nav-dropdown-menu[hidden] {
    display: none;
}

/* ==========================================================================
   3. SEARCH BAR COMPONENT SYSTEM WITH INTEGRATED DROPDOWN STACK
   ========================================================================== */
/* Top and bottom trimmed independently: the top margin collapses against
   .page-intro's own bottom margin above it (whichever is larger wins), so
   tightening only .page-intro left this gap at 20px regardless — the
   bottom margin isn't part of that heading stack and is left as it was. */
.search-container {
    max-width: 500px;
    width: 100%;
    margin: 10px auto 20px;
}

/* Vertically bundles search text entry and dropdown row neatly */
.search-form-layout-stack {
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: 100%;
}

.search-bar-row {
    display: flex;
    width: 100%;
}

.input-wrapper {
    position: relative;
    flex: 1;
    display: flex;
    align-items: center;
}

/* Height is stated rather than derived, which is what lets the type grow
   without the box growing with it. Left to padding + line + border the two
   move against each other, and raising the font 16px -> 18px shrank this
   field by a pixel at every width even with the padding trimmed to match.
   42px is what it measured before; an input centres its own text in a fixed
   height, so the vertical padding goes to 0 the way .advanced-field input
   already does, and only the horizontal pair is left to tune.

   The submit button needs no matching change — .search-bar-row is a flex row
   at the default align-items:stretch, so it takes its height from here.

   The right inset stays well clear of the ✖, which is positioned 10px from
   the edge and is why that side was never symmetric.

   Colour is stated rather than left to the UA default: unset, this rendered
   pure black while .advanced-field input's explicit --op-text rendered
   #2b2b26, so the two modes disagreed about what typed text looked like. */
.search-input {
    width: 100%;
    height: 42px;
    padding: 0 32px 0 12px;
    font-size: 18px;
    font-weight: 700;
    color: var(--op-ink);
    border: 2px solid var(--op-card-border);
    border-right: none;
    border-top-left-radius: 4px;
    border-bottom-left-radius: 4px;
    outline: none;
    box-sizing: border-box;
}

/* font-weight on the input is inherited by its placeholder too, and "Enter
   letters..." bold reads as a filled-in value rather than a hint — the one
   thing a placeholder must not do. Reset here rather than left bold, so only
   what is actually typed gets the emphasis. */
.search-input::placeholder {
    font-weight: 400;
}

.search-input::-webkit-search-decoration,
.search-input::-webkit-search-cancel-button,
.search-input::-webkit-search-results-button,
.search-input::-webkit-search-results-decoration {
    display: none;
}

.search-input:focus {
    border-color: var(--op-olive);
}

#clear-input-btn {
    position: absolute;
    right: 10px;
    background: none;
    border: none;
    color: var(--op-text-faint);
    font-size: 20px;
    font-weight: bold;
    cursor: pointer;
    display: block;
    padding: 0;
    line-height: 1;
    transition: color 0.15s ease;
    user-select: none;
}

/* Show the clear button only when the input has content. :placeholder-shown
   matches exactly when the value is empty, so this needs no JS. The button is
   the input's next sibling inside .input-wrapper. */
.search-input:placeholder-shown ~ #clear-input-btn {
    display: none;
}

#clear-input-btn:hover {
    color: var(--op-text-muted);
}

/* The .search-kbd-tip rules that sat here are gone with the tip itself. The
   Ctrl+K / ⌘K shortcut they advertised still works (unscrambler.js) — it is
   unadvertised now, not removed.

   Two findings from styling it are worth keeping, since both are about this
   palette rather than about that element. --op-text-faint measures 8.11:1 on
   --op-bg, so faint text was never the legibility problem here; weight was,
   at 400 beside a 600-weight neighbour. And --op-panel-alt is 1.02:1 against
   --op-bg with --op-border at 1.49:1, so a chip built from those two is all
   but invisible against the page — anything meant to read as raised needs a
   near-white fill and the darker --op-cream-muted edge. */

/* Icon-only. The accessible name is on the button's aria-label (home.html),
   not in the markup, so nothing here has text to size — the width is fixed
   rather than derived from a label, and the flex centring is what places the
   glyph. flex-shrink is pinned because this sits in .search-bar-row beside a
   flex:1 input that would otherwise squeeze it at narrow widths. */
.search-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 46px;
    padding: 0;
    background-color: var(--op-stamp);
    color: white;
    border: 2px solid var(--op-card-border);
    border-top-right-radius: 4px;
    border-bottom-right-radius: 4px;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

/* Flush against .search-input's own missing right border (border-right: none
   above) so the two read as one continuous bar rather than a seam of black
   between them. The standalone advanced-mode submit button keeps all four
   sides — it sits beside a label, not glued to an input. */
.search-bar-row .search-button {
    border-left: none;
}

/* Sized independently of the button, which stays 46x42 (39px square on
   mobile) — this only fills more of it. The button's 2px border makes the
   usable box 38px on desktop and 35px on mobile, so 26px leaves 6px and
   4.5px of clear space each side respectively: close to the button edge by
   design, but not touching it. Push much past this and the mobile glyph
   starts reading as a button that has run out of room rather than as an
   icon. */
.search-button .search-icon {
    width: 26px;
    height: 26px;
    display: block;
}

.search-button:hover {
    background-color: var(--op-stamp-dark);
    border-color: var(--op-card-border);
}

/* Greyed out until the input holds 3+ letters (basic) or at least one
   advanced field is filled. The input's own pattern attribute does the real
   gating there — the browser blocks submission — so this selector only has
   to mirror that state visually; the advanced button, which has no such
   per-field pattern to lean on (see unscrambler.js), is instead genuinely
   given the disabled attribute, so :disabled here is doing real work rather
   than mirroring something else.

   Declared AFTER .search-button:hover on purpose: .search-button:disabled
   alone ties :hover on specificity (one class, one pseudo-class each), so
   without the later position a hovered, disabled button would repaint dark
   red as if it were clickable — a real bug the advanced button hit the
   first time anything here actually used :disabled for real. The :has()
   half of this selector already outranks :hover on specificity alone and
   didn't need the reorder, but it reads oddly split across a hover rule, so
   both stay together here. */
.search-button:disabled,
.search-bar-row:has(.search-input:invalid) .search-button {
    background-color: var(--op-disabled-bg);
    color: var(--op-disabled-text);
    cursor: not-allowed;
    box-shadow: none;
    border-color: var(--op-card-border);
}

/* Secondary Selector Component Alignment Row */
.dropdown-row {
    display: flex;
    flex-direction: column;     /* Label always stacks above the select */
    align-items: flex-start;
    gap: 6px;
    padding-left: 2px;
    width: 100%;
}

.dropdown-row label {
    font-size: 14px;
    font-weight: 600;
    color: var(--op-ink);
    white-space: normal;
    display: inline-flex;
    align-items: center;
}

.dropdown-row select {
    width: 100%;                /* Full width since it's now stacked below the label */
    padding: 6px 36px 6px 12px; /* Extra right padding to clear the custom arrow */
    font-size: 14px;
    border: 1px solid var(--op-border);
    border-radius: 4px;
    background-color: var(--op-panel);
    color: var(--op-text);
    outline: none;
    cursor: pointer;
    transition: border-color 0.2s ease;
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24'%3E%3Cpath fill='%232d3748' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 22px 22px;
}

.dropdown-row select:focus {
    border-color: var(--op-olive);
}

/* --------------------------------------------------------------------------
   Basic / Advanced mode toggle
   --------------------------------------------------------------------------
   The underlying mechanism is the Spelling Bee's .sb-sort control: visually
   hidden radios with a label per option, :checked driving the active look.
   Kept here rather than in a page stylesheet because this one is on the
   homepage. The look itself has since departed from .sb-sort's free-standing
   chips for a single segmented pill, at the owner's request — see the rules
   below for what that changes.

   The radios are hidden with opacity/size, never display:none — that would
   cost focusability, screen-reader announcement, and inclusion in the
   submitted form all at once, and the mode has to reach the server. */
/* A segmented pill rather than the two free-standing chips this used to be
   (still the .sb-sort family's hidden-radio + label mechanism underneath —
   only the visual is a deliberate departure, at the owner's request, toward
   a single capsule track with the active side filled in). The outer element
   carries the border and the round-both-ends shape; the labels carry none of
   their own, so only the checked one ever looks like a distinct control. */
/* align-self: flex-start is load-bearing, not decoration. This sits in
   .search-form-layout-stack, a column flex container, and align-items there
   defaults to stretch — which stretches an auto-width child to the full
   cross-axis size regardless of the child's own display type. Without this
   the pill was stretching to the width of the form itself rather than
   hugging "Basic"/"Advanced", which is the bug that made it read too wide. */
/* Sits directly under the field it governs, so switching mode reads as the
   next step after looking at what was just typed. The negative margin pulls
   it in from .search-form-layout-stack's 12px gap, the same technique
   .search-kbd-tip used for the same reason; the gap below it, to the Word
   List row, is untouched. */
.search-mode-toggle {
    display: inline-flex;
    align-self: flex-start;
    margin-top: -8px;
    padding: 2px;
    background-color: var(--op-panel);
    border: 2px solid var(--op-card-border);
    border-radius: 999px;
}

.search-mode-opt input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

/* Sized to land the whole control (this plus the outer pill's 2px padding
   and 2px border on each side) at 32px tall — a fixed height rather than
   padding alone, since line-height varies enough across engines that padding
   targeting a height indirectly drifts by a pixel or two between them.
   Horizontal padding is what was making Basic and Advanced read far apart;
   14px keeps the pill snug to its own text, matching the reference. */
.search-mode-opt label {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 24px;
    padding: 0 14px;
    font-size: 13px;
    font-weight: 700;
    color: var(--op-ink);
    border-radius: 999px;
    cursor: pointer;
    transition: background-color .15s ease, color .15s ease;
}

/* A hover on the currently active side has nothing to invite — it is already
   selected — so this only lights up the inactive one. */
.search-mode-opt input:not(:checked) + label:hover {
    background-color: var(--op-panel-alt);
}

.search-mode-opt input:checked + label {
    background-color: var(--op-stamp);
    color: #fff;
}

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

/* --------------------------------------------------------------------------
   Basic / Advanced field swap
   --------------------------------------------------------------------------
   Both field groups sit in the same grid cell, so the stack is always as tall
   as the taller of the two and switching mode never moves the Word List row
   below it up or down.

   grid-template-columns is explicit rather than implicit on purpose: an
   implicit auto column sizes to its content's max-content width, which lets
   the three full-size advanced inputs push the card wider than the viewport
   on a phone. minmax(0, 1fr) pins the column to the container, and the 0
   floor lets the content shrink rather than forcing an overflow.

   Hiding is visibility, not display, so both groups keep their size and the
   cell stays stable. That alone would leave the hidden fields focusable and
   still subject to validation, so home.html and unscrambler.js also mark them
   disabled — see the comment there. */
.mode-fields-stack {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    width: 100%;
}

.mode-fields-stack > .basic-fields,
.mode-fields-stack > .advanced-search-fields {
    grid-area: 1 / 1;
    min-width: 0;
}

/* gap is tighter than it once was — the fields' 2px borders already mark
   them off from each other and from the two buttons, so the extra breathing
   room was spent on nothing. Every pixel trimmed here goes straight to the
   three fields' own width: they carry flex-grow, the buttons don't. */
.advanced-search-fields {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 4px;
    align-self: start;
    visibility: hidden;
    pointer-events: none;
}

#unscrambler-form:has(#mode-advanced:checked) .basic-fields {
    visibility: hidden;
    pointer-events: none;
}

#unscrambler-form:has(#mode-advanced:checked) .advanced-search-fields {
    visibility: visible;
    pointer-events: auto;
}

/* Zero gap, matching .search-hint's own margin: 0 to the field below it in
   Basic mode — both field groups start at the same grid-cell top (see
   .mode-fields-stack), so their first two rows landing at the same spacing
   is what keeps the actual input box at one Y position across the toggle
   instead of shifting by the difference between the two gaps. */
.advanced-field {
    display: flex;
    flex-direction: column;
    gap: 0;
    flex: 1 1 110px;
    min-width: 0;
}

/* --op-ink rather than --op-text-muted: these labels sit directly on the
   paper texture rather than on a panel, and muted measures 3.48:1 against the
   photograph actually behind them (the .sb-counter finding in
   spelling_bee.css) rather than against the flat --op-bg it was picked for.
   Ink is the token this codebase uses for small text on that background. */
.advanced-field label {
    font-size: 13px;
    font-weight: 600;
    color: var(--op-ink);
}

/* Height is fixed here, so the padding being trimmed is the horizontal pair —
   that is what buys room for larger type in a field the mobile row keeps
   narrow (about 79px of it at 320px). --op-ink to match .search-input above;
   --op-text left the two modes rendering typed text differently. */
.advanced-field input {
    width: 100%;
    min-width: 0;
    height: 42px;
    padding: 0 8px;
    font-size: 17px;
    font-weight: 700;
    color: var(--op-ink);
    border: 2px solid var(--op-card-border);
    border-radius: 4px;
    background-color: var(--op-panel);
    outline: none;
    box-sizing: border-box;
}

/* Same reasoning as .search-input::placeholder: bold is for what's typed,
   not for the "e.g. pre" hint standing in for it. */
.advanced-field input::placeholder {
    font-weight: 400;
}

.advanced-field input:focus {
    border-color: var(--op-olive);
}

/* Sits between "Ends with" and the search button, and stays visible at all
   times in advanced mode — it takes no visibility of its own, so it simply
   inherits whatever .advanced-search-fields is set to: hidden in basic mode
   (visibility is inherited, so nothing here leaks into that row even with
   stale text still in a field) and visible the moment advanced mode is on,
   regardless of whether the three fields hold anything. Never laid out with
   display:none either, for the same reason as before: the three fields must
   never resize as someone types the first character or clears the last one.

   What toggles instead is how loud it looks. Muted by default — nothing to
   clear yet — and switching to the same red fill and glyph as before the
   moment a field actually holds something, via the same :has() test that
   used to control visibility. The border stays the row's usual black in
   both states — every other bordered control on this row keeps it fixed,
   and softening it here read as a different-shaped control rather than a
   quieter one. Still clickable while muted (clearing empty fields is a
   harmless no-op that also refocuses "Starts with"), so this is a style
   change only, not a disabled state — but the cursor still reads
   not-allowed while muted, and the hover invert below is scoped to the
   active state only, so hovering a muted button gives no colour feedback
   for an action that does nothing worth announcing. */
.advanced-clear {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: flex-end;
    height: 42px;
    width: 34px;
    padding: 0;
    background-color: var(--op-panel-alt);
    border: 2px solid var(--op-card-border);
    border-radius: 4px;
    color: var(--op-text-faint);
    font-size: 22px;
    font-weight: 900;
    line-height: 1;
    cursor: not-allowed;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.advanced-search-fields:has(input:not(:placeholder-shown)) .advanced-clear {
    background-color: var(--op-panel);
    color: var(--op-stamp);
    cursor: pointer;
}

.advanced-search-fields:has(input:not(:placeholder-shown)) .advanced-clear:hover {
    background-color: var(--op-stamp);
    color: #fff;
}

.advanced-field-submit {
    flex: 0 0 auto;
}

/* .search-button rounds only its right corners, expecting to sit flush
   against the basic row's input. Standalone here, it needs all four — and an
   explicit height, since it has no input beside it to match. Square, so the
   width comes down from the basic row's 46px to match that height. */
.advanced-field-submit .search-button {
    width: 42px;
    height: 42px;
    border-radius: 4px;
}

/* ==========================================================================
   4. UNSCRAMBLER CARD DISPLAY SECTIONS
   ========================================================================== */
.results-container {
    max-width: 800px;
    margin: 30px auto;
}

.hint-text {
    font-size: 14px;
    font-weight: 600;
    color: var(--op-ink);
    margin-top: -10px;
    margin-bottom: 25px;
}

.search-hint {
    margin: 0;
    text-align: center;
    align-self: center;
    width: max-content;
    max-width: none;
    white-space: nowrap;
}

.word-group h4 {
    border-bottom: 2px solid var(--op-olive);
    padding-bottom: 4px;
    margin-bottom: 15px;
    color: var(--op-text);
}

/* position: relative makes this the offsetParent, so the offsetTop/offsetLeft
   unscrambler.js reads off a pill are grid-relative — which is exactly the
   coordinate space --tab-left needs.

   --pill-gap is a variable because .word-panel has to subtract it to shorten
   the neck, and it changes twice in the media queries below.
   --tab-neck is the space left between the open pill and the panel: as tight as
   it goes before the two collide.
   --tab-border is the open pill's stroke, shared by the neck and the panel so
   the fused tab is one continuous outline. */
.accordion-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, var(--pill-width, 120px));
    --pill-gap: 12px;
    --tab-neck: 4px;
    --tab-border: 2px;
    gap: var(--pill-gap);
    margin-bottom: 35px;
    align-items: start;
    position: relative;
}

/* 35px is right when a .word-group heading follows, but a "Show more"
   control isn't a heading — .more-info-link's own 12px margin-top ends up
   governing instead (margins collapse to the larger of the two), tightening
   the gap without touching the general case. */
.accordion-list:has(+ .show-more-link) {
    margin-bottom: 8px;
}

/* --- Shared Structural Profile for Interactive and Static Word Blocks --- */
.structural-pill,
.word-accordion,
.no-definition {
    display: block;
    background-color: var(--op-panel);
    border: 1px solid var(--op-border);
    border-radius: 6px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
    overflow: hidden;
    height: 38px;
    box-sizing: border-box;
    width: 100%;
    min-width: 0;
    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}

/* --- Lift & Shadow on Hover (accordion/definition pills only) --- */
.word-accordion:not(.is-open):hover {
    transform: translateY(-3px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
    border-color: var(--op-olive);
}

/* --- No lift, no color change for pills without definitions --- */
.no-definition:hover {
    transform: none;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}

.no-definition:hover .word-header {
    background-color: transparent;
}

/* --- A. Clickable Accordion Base Styles --- */
.word-accordion {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

/* Now a <button> on the interactive pills (a <div> on the inert ones), so it
   has to opt out of the UA button styling — font, background, border and the
   centred text — before the shared layout below applies.

   The horizontal padding goes through --pill-pad-x rather than being written
   into each context's `padding`, because the open state has to subtract from
   whatever the current value is. Contexts below set the variable, not padding. */
.word-header {
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    --pill-pad-x: 10px;
    padding: 8px var(--pill-pad-x);
    font-family: inherit;
    font-size: inherit;
    font-weight: 600;
    color: var(--op-olive);
    background: none;
    border: 0;
    text-align: left;
    width: 100%;
    cursor: pointer;
    user-select: none;
    transition: background-color 0.2s;
    box-sizing: border-box;
    height: 36px;
}

.word-content-wrapper {
    display: inline-flex;
    align-items: center;
    flex: 1;
    min-width: 0;
    overflow: hidden;
}

.word-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

.accordion-arrow {
    flex-shrink: 0;
    padding-left: 8px;
    font-size: 10px;
    color: var(--op-stamp);
    transition: transform 0.2s ease;
}

/* --- Active Open Accordion Modifiers ---
   The open pill keeps its grid slot and its size. It no longer expands: the
   content opens in .word-panel below the row (see the note in home.html).
   All the pill does here is turn into the panel's tab — flat bottom corners,
   no bottom border, and a neck (::after) bridging the gap so the two shapes
   read as one. overflow must go visible for that neck to escape the pill.

   The open state carries no fill of its own: the tab takes the panel's own
   --op-panel-alt so the two surfaces are continuous. What marks it open is the
   2px border (--tab-border), which the neck and the panel pick up too — a fused
   tab is a single outline, so a 2px tab on a 1px panel shows at the join.

   The 2px border would push the 36px header past the pill's fixed 38px, so the
   header gives back the 2px. That keeps the text's centre line at 19px in both
   states — closed is 1 + 18, open is 2 + 17 — so nothing shifts on open. */
.word-accordion.is-open {
    border-width: var(--tab-border);
    border-color: var(--op-olive);
    border-bottom-color: transparent;
    border-radius: 6px 6px 0 0;
    overflow: visible;
    position: relative;
    z-index: 1;
    box-shadow: none;
}

/* The radius here is not decoration — it replaces a clip. .structural-pill uses
   `overflow: hidden` to trim this rectangular background to the pill's rounded
   corners, and the open state has to give that up so the neck can escape. Left
   at 0 the fill paints square into both top corners and the border visibly
   thickens around the curve. An inner corner is the outer radius less the
   border it sits inside.

   height and padding both hand back exactly what the extra border took, so the
   open header's content box is identical to the closed one and nothing inside
   moves. The padding is load-bearing, not cosmetic: a 7-letter .rack-chip needs
   66px and gets exactly 66px, so 2px of border ellipsises it down to five
   letters. See .rack-chip's comment. */
.word-accordion.is-open > .word-header {
    height: calc(36px - var(--tab-border));
    padding-inline: calc(var(--pill-pad-x) - (var(--tab-border) - 1px));
    background-color: var(--op-panel-alt);
    border-radius: calc(6px - var(--tab-border)) calc(6px - var(--tab-border)) 0 0;
}

/* left/right resolve against the padding box, so the negative border-width
   inset spans the pill's border box exactly and the neck's side borders land in
   the same columns as the pill's own. The height covers three things in order:
   the pill's transparent bottom border, the gap itself, and the panel's top
   border — so no page background shows anywhere down the seam. */
.word-accordion.is-open::after {
    content: "";
    position: absolute;
    top: 100%;
    left: calc(-1 * var(--tab-border));
    right: calc(-1 * var(--tab-border));
    height: calc(var(--tab-neck) + var(--tab-border) * 2);
    box-sizing: border-box;
    background-color: var(--op-panel-alt);
    border-left: var(--tab-border) solid var(--op-olive);
    border-right: var(--tab-border) solid var(--op-olive);
}

.word-accordion.is-open .accordion-arrow {
    transform: rotate(180deg);
}

/* --- The shared panel ---
   A full-width grid item that unscrambler.js relocates to sit directly after
   the last pill in the open pill's row, and labels with the open pill's
   geometry so the tab notch lands under it.

   The negative margin is what shortens the neck. The panel is the next grid
   row, so the space under the open pill is the grid's own row gap — pulling the
   panel up by the difference trims it to --tab-neck without touching the gap
   between any other pair of pills. */
.word-panel {
    grid-column: 1 / -1;
    position: relative;
    margin-top: calc(var(--tab-neck) - var(--pill-gap, 12px));
    border: var(--tab-border) solid var(--op-olive);
    border-radius: 6px;
    background-color: var(--op-panel-alt);
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    animation: wordpanel-in 0.18s ease-out;
}

/* A display declaration on .word-panel would outrank the UA [hidden] rule, so
   the closed state has to be spelled out. */
.word-panel[hidden] {
    display: none;
}

/* Same trap, same fix: .structural-pill is display: block (see the shared
   rule above), so a pill unscrambler.js trims to keep a capped group's last
   row complete needs this or [hidden] does nothing. */
.structural-pill[hidden] {
    display: none;
}

/* Erases the panel's top border between the neck's two side borders, fusing
   tab and panel into one outline. --tab-left is the open pill's offsetLeft
   within the grid, i.e. relative to the panel's border box; this element is
   positioned against the padding box, which the border-width inset cancels out,
   leaving the strip exactly the tab's inner width. */
.word-panel::before {
    content: "";
    position: absolute;
    top: calc(-1 * var(--tab-border));
    left: var(--tab-left, 0px);
    width: calc(var(--tab-width, 0px) - var(--tab-border) * 2);
    height: var(--tab-border);
    background-color: var(--op-panel-alt);
}

/* A tab flush against either end has no border left to round off — leaving the
   radius there shows a notch of background between pill and panel. A pill alone
   on its row (one-column mobile) reaches both ends at once. */
.word-panel[data-tab-edge="start"],
.word-panel[data-tab-edge="both"] { border-top-left-radius: 0; }

.word-panel[data-tab-edge="end"],
.word-panel[data-tab-edge="both"] { border-top-right-radius: 0; }

@keyframes wordpanel-in {
    from { opacity: 0; transform: translateY(-6px); }
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    .word-panel { animation: none; }
}

/* Inside definitions text container */
.word-body {
    padding: 15px 20px;
}

.definition-list {
    margin: 0;
    padding-left: 20px;
}

.definition-list li {
    font-size: 15px;
    line-height: 1.5;
    color: var(--op-text-muted);
    margin-bottom: 8px;
}

.definition-list li:last-child {
    margin-bottom: 0;
}

.more-info-link {
    display: block;
    margin-top: 12px;
    padding: 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--op-olive);
    background: none;
    border: none;
    text-decoration: none;
    cursor: pointer;
}

.more-info-link:hover {
    text-decoration: underline;
}

/* The rack tiles' crawl links ship with `hidden` — display: block above would
   otherwise outrank the UA [hidden] rule and put fourteen stray "See all
   words" links on the homepage. Same trap as .word-panel[hidden]. */
.more-info-link[hidden] {
    display: none;
}

/* Same red as a pill's score and its accordion arrow (--op-stamp), not
   .more-info-link's olive — this control sits right under a row of pills
   carrying that color, so it reads as part of them rather than a separate
   footer link like the racks' "See all words" (which keeps the base style). */
.show-more-link {
    color: var(--op-stamp);
    font-weight: bold;
}

.word-detail-extra {
    margin-top: 12px;
    border-top: 1px solid var(--op-border);
}

.word-detail-extra .word-detail-section {
    padding: 14px 0;
}

/* --- B. Non-Clickable Flat Static Pill Styles --- */
.no-definition {
    user-select: none;
    cursor: default;
}

.no-results-msg {
    color: var(--op-text-faint);
    font-style: italic;
}

/* ==========================================================================
   5. DEEP SCRABBLE SUBSCRIPT CONFIGURATIONS
   ========================================================================== */
.scrabble-score {
    font-size: 11px;
    font-weight: bold;
    color: var(--op-stamp);
    flex-shrink: 0;
    margin-left: 2px;
    user-select: none;
    vertical-align: sub;
    position: relative;
    top: 3px;
}

/* Child combinator on .word-header, not a descendant selector: the rack
   fragment's nested word pills carry their own .scrabble-score, and they no
   longer live inside the tile but they do share these class names. */
.word-accordion:hover > .word-header .scrabble-score {
    color: var(--op-stamp-dark);
}

/* ==========================================================================
   6. WORD DETAIL PANEL — direct word lookup card
   ========================================================================== */

.word-detail-panel {
    max-width: 800px;
    width: 100%;
    box-sizing: border-box;
    margin: 0 auto 30px auto;
    background-color: var(--op-panel);
    border: 1px solid var(--op-olive-border);
    border-radius: 10px;
    box-shadow: 0 2px 12px rgba(var(--op-olive-rgb), 0.15);
    overflow: visible;
}

/* Header bar */
.word-detail-header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 20px 12px 20px;
    background: linear-gradient(135deg, var(--op-panel-alt) 0%, var(--op-olive-soft) 100%);
    border-bottom: 1px solid var(--op-olive-border);
    /* Clip the gradient to the panel's top rounded corners */
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
    overflow: hidden;
}

.word-detail-badge {
    font-size: 44px;
    line-height: 1;
    flex-shrink: 0;
}

.word-detail-title {
    margin: 0;
    font-size: clamp(18px, 3vw, 24px);
    font-weight: 700;
    color: var(--op-olive-dark);
    letter-spacing: 0.05em;
    line-height: 1.2;
}

.word-detail-subtitle {
    font-size: 12px;
    font-weight: 600;
    color: var(--op-text-faint);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-left: auto;
    flex-shrink: 0;
}

/* Individual sections */
.word-detail-section {
    padding: 14px 20px;
    border-bottom: 1px solid var(--op-border);
}

.word-detail-section:last-child {
    border-bottom: none;
}

/* Inline row: label + chips on the same line */
.word-detail-inline-row {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    flex-wrap: wrap;
}

.word-detail-section-label {
    font-size: 13px;
    font-weight: 700;
    color: var(--op-text-muted);
    white-space: nowrap;
    padding-top: 4px;
    min-width: 110px;
    flex-shrink: 0;
}

/* Definitions list */
.word-detail-def-list {
    margin: 8px 0 0 0;
    padding-left: 22px;
}

.word-detail-def-list li {
    font-size: 14px;
    line-height: 1.6;
    color: var(--op-text);
    margin-bottom: 6px;
}

.word-detail-def-list li:last-child {
    margin-bottom: 0;
}

/* Chip container */
.word-detail-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    flex: 1;
    min-width: 0;
}

/* Detail chips — styled to match the unscrambled word tiles */
.detail-chip {
    display: inline-block;
    padding: 6px 12px;
    font-size: 13px;
    font-weight: 600;
    color: var(--op-olive);
    background-color: var(--op-panel);
    border: 1px solid var(--op-border);
    border-radius: 6px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
    line-height: 1.4;
    white-space: nowrap;
    text-decoration: none;
}

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

.detail-chip:active {
    transform: translateY(-1px);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}

/* Synonyms groups container — CSS grid matching the inline-row layout:
   col 1 = 110px (same as word-detail-section-label min-width)
   col 2 = 1fr (chips, starts at same position as Anagrams/Homophones chips)
   gap between cols = 12px (same as word-detail-inline-row gap)
   Placed below the "🔁 Synonyms" header with a small top margin. */
.synonym-groups-wrap {
    display: grid;
    /* 26px indent puts NOUN/VERB under the "S" in Synonyms.
       84px POS label col + 12px gap = chips start at 26+84+12=122px,
       matching the Anagrams/Homophones chip column exactly. */
    grid-template-columns: 84px 1fr;
    column-gap: 12px;
    row-gap: 8px;
    margin-top: 8px;
    padding-left: 26px;
}

/* display:contents makes label + chips-div direct grid children,
   each occupying their own column on the same row */
.synonym-inline-group {
    display: contents;
}

/* POS label — occupies the left 110px column, vertically centered */
.synonym-pos-label {
    font-size: 11px;
    font-weight: 700;
    color: var(--op-text-faint);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    white-space: nowrap;
    align-self: center;
    padding-top: 4px;
}

/* Chips — occupy the right column, wrap naturally */
.synonym-pos-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}

/* Responsive adjustments for the panel */
@media (max-width: 600px) {
    .word-detail-panel {
        max-width: 100%;
        width: 100%;
        margin: 0 0 20px 0;
        border-radius: 8px;
    }

    .word-detail-header {
        padding: 12px 14px 10px 14px;
        border-top-left-radius: 8px;
        border-top-right-radius: 8px;
    }

    .word-detail-section {
        padding: 12px 14px;
    }

    .word-detail-inline-row {
        flex-direction: column;
        gap: 6px;
    }

    .word-detail-section-label {
        min-width: unset;
        padding-top: 0;
    }

    /* On mobile, remove the left-indent so synonyms don't overflow */
    .synonym-groups-wrap {
        margin-left: 0;
    }

    .word-detail-subtitle {
        font-size: 10px;
    }
}

/* ==========================================================================
   7. GLOBAL APP LAYOUT FOOTER
   ========================================================================== */
.main-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    text-align: center;
    padding: 10px 20px;
    background-color: var(--op-dark);
    color: var(--op-cream);
    font-size: 14px;
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.15);
    z-index: 900;
}

.main-footer p {
    margin: 0;
}

.main-footer a,
.main-footer a:visited {
    color: var(--op-cream);
    text-decoration: underline;
    text-decoration-color: rgba(232, 226, 208, 0.5);
    text-underline-offset: 2px;
}

.main-footer a:hover,
.main-footer a:focus-visible {
    color: #fff;
    text-decoration-color: #fff;
}

.footer-sep {
    color: var(--op-cream-muted);
    margin: 0 6px;
}

/* The footer is `position: fixed`, so nothing in the flow accounts for it —
   .page-row's bottom margin is the only thing keeping content off it, and that
   margin has to match the footer's real height.

   Measured in Chromium with a 2px-step Playwright sweep, not guessed. The
   Contact link (added 2026-08-16) is the THIRD, and it invalidated the two
   numbers recorded here before it — the previous comment said in as many words
   that a third link would, and it did: the wrap moved from 490px to 564px, so
   the old `max-width: 500px` rule left 520px viewports overrunning by 8px. It
   also introduced a third state that two links never reached.

     >= 564px   36px, one line     reserve 44px  (the default above)
     324-563px  52px, two lines    reserve 60px
     <  324px   68px, three lines  reserve 76px

   Each reserve is the measured height + 8px, the gap this footer has always
   kept. The breakpoints carry ~11px of slack over the measured values, for
   font fallbacks, exactly as the 490 -> 500 rounding did before. The narrower
   query must stay LAST: both are max-width, so where they overlap the later
   one wins, and that is what makes the sub-324px case reach 76px.

   Changing the footer's text or adding a fourth link invalidates all of this.
   `manage.py test` cannot catch it — re-measure in a browser. */
@media (max-width: 575px) {
    .page-row {
        margin-bottom: 60px;
    }
}

@media (max-width: 335px) {
    .page-row {
        margin-bottom: 76px;
    }
}

/* ==========================================================================
   7. LEXICON TOTAL DICTIONARY LOADED BADGE TYPOGRAPHY
   ========================================================================== */
.lexicon-count {
    font-size: 13px;
    font-weight: 600;
    color: var(--op-ink);
    margin-left: 4px;
}

/* ==========================================================================
   8. RESPONSIVE DESIGN — FLUID TYPOGRAPHY, HAMBURGER MENU, MEDIA QUERIES
   ========================================================================== */

/* --- Fluid Typography (scales smoothly between viewport sizes) --- */
body {
    font-size: clamp(14px, 1.5vw, 16px);
}

h2 {
    font-size: clamp(20px, 4vw, 28px);
}

h3 {
    font-size: clamp(16px, 3vw, 22px);
}

.word-group h4 {
    font-size: clamp(14px, 2.5vw, 18px);
}

/* Set on <body> while the off-canvas drawer is open, so the page behind the
   backdrop can't be scrolled with it. */
body.nav-scroll-locked {
    overflow: hidden;
}

/* --- Hamburger Button (hidden on desktop) --- */
.nav-toggle {
    display: none;
    flex-direction: column;
    justify-content: space-between;
    width: 28px;
    height: 20px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
    z-index: 1100;
}

.nav-toggle span {
    display: block;
    width: 100%;
    height: 3px;
    background-color: var(--op-cream);
    border-radius: 2px;
    transition: transform 0.25s ease, opacity 0.25s ease;
}

/* Animate hamburger → X when open */
.main-header.nav-open .nav-toggle span:nth-child(1) {
    transform: translateY(8.5px) rotate(45deg);
}
.main-header.nav-open .nav-toggle span:nth-child(2) {
    opacity: 0;
}
.main-header.nav-open .nav-toggle span:nth-child(3) {
    transform: translateY(-8.5px) rotate(-45deg);
}

/* --- Nav Collapse Breakpoint (≤ 880px) ---
   The nav links do not scale (fixed 15px), so the menu has a fixed natural width
   of ~420px. Below that the header flex row squeezes .logo-text-group narrower
   than "Word Operative" needs, the title wraps to two lines, and the tagline ends
   up sized against a half-width title.

   Where it stops fitting: the logo side needs 28px padding + 9.67em (image 1.558
   + gap 0.3 + title 7.81), and .site-logo is 5vw in this range. Solving
   vw ≥ 448 + 0.483vw gives ~867px, so 880px is the first round breakpoint with
   margin. Collapsing here also removes the ~420px that was forcing a horizontal
   scrollbar at 601px.

   NB: measure that ~420px at a WIDE viewport. .nav-menu has the default
   flex-shrink: 1, so measuring it in the cramped range returns its compressed
   width (~324px at 640px) and understates the space it actually wants. */
/* Collapse point moved 880 -> 940 when the Solvers dropdown grew the nav from
   442.5px to 520.5px. Derived, not guessed: at the 38px logo floor the row
   needs 38 x 9.668 + 520.5 + 28 padding = 916px, and the logo formula above
   only clears its floor from ~940px up. Below this the nav is behind the
   hamburger and the logo has the row to itself. */
@media (max-width: 940px) {
    /* Show hamburger, hide nav by default */
    .nav-toggle {
        display: flex;
    }

    /* Off-canvas drawer, not a dropdown. It is `position: fixed` against the
       viewport rather than `absolute` against .main-header, so its height can
       run the full remaining screen below the header — an absolutely
       positioned panel only has the header's own height to anchor to.
       `top` is `--nav-header-height`, set by unscrambler.js from the header's
       actual rendered height (it's fluid: the logo/tagline formulas above
       scale it with viewport width, so no fixed number is safe here). Falls
       back to 0px before that JS runs, which only matters for a moment
       between first paint and script execution and while the drawer is
       off-screen (translateX(100%)) regardless. */
    .nav-menu {
        flex-direction: column;
        position: fixed;
        top: var(--nav-header-height, 0px);
        right: 0;
        bottom: 0;
        width: min(280px, 80vw);
        background-color: var(--op-dark);
        box-shadow: -4px 0 16px rgba(0, 0, 0, 0.25);
        padding: 8px 0;
        overflow-y: auto;
        z-index: 1050;
        transform: translateX(100%);
        /* `visibility` is not smoothly interpolable, so browsers snap it at
           the END of the transition when animating TO hidden and at the
           START when animating TO visible (CSS Transitions spec, discrete
           properties) — exactly what a slide-out panel needs: present and
           tabbable for the whole slide-in, removed from tab order only once
           fully off-screen again. */
        visibility: hidden;
        transition: transform 0.28s ease, visibility 0.28s;
    }

    /* z-index here must clear .nav-backdrop's 1040. .main-header (position:
       relative + the base rule's z-index: 1000) is a stacking context, so
       .nav-menu's own z-index: 1050 is scoped INSIDE it and never compared
       against .nav-backdrop directly — what actually competes with the
       backdrop's 1040 is the header's own 1000, and 1000 loses. Confirmed
       with elementFromPoint: without this, the backdrop sits on top of the
       whole drawer, so every tap on a link hits the backdrop and closes the
       menu instead of navigating. */
    .main-header {
        position: relative; /* needed for absolute .nav-dropdown-menu positioning below */
        z-index: 1060;
    }

    .main-header.nav-open .nav-menu {
        transform: translateX(0);
        visibility: visible;
    }

    /* Dimmed backdrop behind the drawer, over the page content. Sits below
       the header's z-index (1000) so it never darkens the header itself, and
       stops at the same --nav-header-height top offset as the drawer so
       there's no seam. It's a sibling of .main-header rather than a child so
       the existing outside-click handler (which checks
       `!mainHeader.contains(e.target)`) already closes the menu on a tap
       here — no separate listener needed. */
    .nav-backdrop {
        display: block;
        position: fixed;
        top: var(--nav-header-height, 0px);
        right: 0;
        bottom: 0;
        left: 0;
        background-color: rgba(0, 0, 0, 0.45);
        z-index: 1040;
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.28s ease, visibility 0.28s;
    }

    .main-header.nav-open ~ .nav-backdrop {
        opacity: 1;
        visibility: visible;
    }

    /* --op-cream-muted (the base .nav-link color) is tuned for a compact bar
       sitting right against the bright header — against the drawer's much
       taller expanse of --op-dark it reads as dim rather than deliberately
       understated, so the drawer gets the brighter --op-cream instead.
       :hover/.active still win via source order, unaffected. */
    .nav-link {
        color: var(--op-cream);
        padding: 12px 20px;
        border-radius: 0;
        font-size: 16px;
    }

    .nav-link.active::after {
        display: none;
    }

    /* In the collapsed nav the menu is already a vertical stack, so the
       dropdown opens inline as a nested block rather than floating over it.
       An absolutely positioned panel here would overhang the links below and
       sit half off the screen edge. The toggle keeps working exactly as it
       does on desktop — only the panel's placement changes. */
    .nav-dropdown-toggle {
        width: 100%;
        justify-content: space-between;
    }

    .nav-dropdown-menu {
        position: static;
        min-width: 0;
        margin: 0;
        padding: 0;
        border: none;
        border-radius: 0;
        box-shadow: none;
        background-color: var(--op-dark-soft);
    }

    .nav-dropdown-link {
        padding: 12px 20px 12px 34px;
        border-radius: 0;
        font-size: 16px;
    }
}

/* --- Mobile Breakpoint (≤ 600px) --- */
@media (max-width: 600px) {
    /* iOS Safari doesn't handle background-attachment: fixed well */
    body {
        background-attachment: scroll;
    }

    /* Below 600px the screen is narrow enough that matching the title would
       leave the tagline very small, so it fills the leftover header width
       instead. Subtracted: 76px for the header's 28px padding plus the hamburger
       and its gap, and 1.86em for the logo image (1.558em) and the flex gap
       (0.3em) — both scale with .site-logo, so they must be in em. Divided by
       25.95, the tagline's width per 1px of font-size. Between 601 and 768px the
       nav is collapsed but there is ample room, so the base rule (match the
       title) applies there instead. */
    .logo-tagline {
        font-size: clamp(8px, calc((100vw - 76px - 1.86em) / 25.95), 24px);
    }

    /* Tighten main content padding */
    .main-content {
        padding: 12px;
    }

    /* Same reasoning as the desktop rule above, scaled to this padding. */
    .main-content:has(.search-page-wrapper) {
        padding-top: 6px;
    }

    /* Search container full width */
    .search-container {
        max-width: 100%;
        margin: 6px auto 12px;
    }

    /* Not enough room to keep the hint on one line at this width */
    .search-hint {
        white-space: normal;
        width: 100%;
        align-self: stretch;
        text-align: left;
    }

    /* Hanging indent so wrapped lines align under the first word, not the 💡 */
    .hint-text {
        padding-left: 1.4em;
        text-indent: -1.4em;
    }

    /* All three fields and the two buttons stay on one row here, as the
       approved mockup has them. flex-basis is 0 rather than a measured width
       so the three fields simply split whatever the row has left after the
       buttons and the gaps — measured at 320px that is 62px each, still wide
       enough to type a prefix into, and it needs no per-breakpoint number to
       keep right. Without nowrap a button drops to a line of its own well
       before 320px.

       Getting there needed a second fix below, .advanced-field-submit
       {flex:0 0 auto}, restated after .advanced-field's flex:1 1 0 just
       above. .advanced-field-submit carries that class name ALONGSIDE
       .advanced-field (it needs that rule's column layout for the button it
       wraps), so at this width .advanced-field's flex had always overridden
       .advanced-field-submit's own base flex:0 0 auto — equal specificity,
       later in the file — and the search button's wrapper had been taking a
       fourth share of the row's grow space this whole time, not just once
       the clear button joined it. Fields measured 61.5px before either
       control existed, 53px right after the clear button landed, and only
       55px after a first gap trim that barely moved anything — the submit-
       wrapper fix is what actually mattered, and a second gap trim (6px to
       3px) after that is what the 62px above reflects. */
    .advanced-search-fields {
        flex-wrap: nowrap;
        gap: 3px;
    }

    .advanced-field {
        flex: 1 1 0;
        gap: 2px;
    }

    /* .advanced-field-submit carries the .advanced-field class too (it needs
       that rule's column layout for the button it wraps), so the rule above
       has always overridden its base flex:0 0 auto here — equal specificity,
       later in the file. Restated after it so this div goes back to fixed
       width and stops taking a fourth share of the row's grow space; the
       button inside was always pinned to its own width regardless; only the
       three real fields were shrinking to pay for this. */
    .advanced-field-submit {
        flex: 0 0 auto;
    }

    /* The labels are what would otherwise set a floor under the row's width:
       "Starts with" is wider than its field's share at 320px. Ellipsis keeps
       them from doing that, and they stay on one line so the three fields'
       inputs line up with each other. */
    .advanced-field label {
        font-size: 11px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* Tighter again where the row is narrowest. 16px is a floor, not a
       preference: iOS zooms the page when a field under 16px takes focus,
       which on this row would scroll the other two out of view. */
    .advanced-field input {
        height: 39px;
        padding: 0 6px;
        font-size: 16px;
    }

    /* Matches the input height beside it rather than the 42px it takes on
       desktop, so the row reads as one control strip. */
    .advanced-field-submit .search-button {
        width: 39px;
        height: 39px;
    }

    /* Shrinks with the row rather than keeping its desktop 34px — this
       button is always on screen in advanced mode now (see the base rule),
       so the narrowest row pays the smallest tax it can for it. */
    .advanced-clear {
        height: 39px;
        width: 28px;
        font-size: 19px;
    }

    /* Stack dropdown label above select */
    .dropdown-row {
        flex-direction: column;
        align-items: flex-start;
        gap: 6px;
    }

    .dropdown-row label {
        white-space: normal;
    }

    .dropdown-row select {
        width: 100%;
    }

    /* Results full width */
    .results-container {
        max-width: 100%;
        margin: 16px auto;
    }

    /* Tighter pill grid gap. Columns also switch from fixed pill-width to
       minmax(pill-width, 1fr): with only 1-2 columns fitting at this width,
       fixed tracks leave leftover row space that the panel's
       grid-column: 1/-1 can't reach (it only spans existing tracks), so the
       open panel reads as narrow. Letting existing tracks stretch removes
       that leftover so the panel reaches the true row edge. */
    .accordion-list {
        grid-template-columns: repeat(auto-fill, minmax(calc(var(--word-len, 8) * 9px + 42px), 1fr));
        --pill-gap: 8px;
        margin-bottom: 24px;
    }

    .word-header {
        --pill-pad-x: 6px;
    }

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

    .scrabble-score {
        font-size: 10px;
        margin-left: 1px;
    }
}

/* --- Small Mobile Breakpoint (≤ 430px) ---
   Was 400px. .site-logo's clamp() floors at 38px, which is wider than the space
   available around 410-430px, so the title wrapped to two lines there — a band
   that includes the 414px iPhone XR/11/14 Plus width. Dropping to the smaller
   clamp at 430px instead keeps it on one line; the cliff already existed, this
   just moves it up to where it is actually needed. */
@media (max-width: 430px) {
    .site-logo {
        font-size: clamp(18px, 6vw, 24px);
    }

    /* Steps down from the 18px base, but stops at 16px rather than the 15px
       it used to take: iOS zooms the page whenever a field below 16px takes
       focus, and this is the field most likely to be tapped first. The height
       is pinned at the 41px it measured here before, a pixel under the base
       for the same reason it always was.

       .search-button is not listed here any more — it is icon-only now, so
       it has no text for a font-size to reach. */
    .search-input {
        height: 41px;
        font-size: 16px;
    }

    .accordion-list {
        --pill-gap: 6px;
        grid-template-columns: repeat(auto-fill, minmax(calc(var(--word-len, 8) * 9px + 36px), 1fr));
    }

    .word-header {
        --pill-pad-x: 4px;
    }
}

/* --- Word Lists Page --- */
.word-lists-page {
    max-width: 800px;
    margin: 0 auto;
    padding-bottom: 20px;
}

.word-lists-page h1 {
    color: var(--op-dark);
}

/* Body links on content pages — the stamp red, and underlined.
   Added 2026-08-17; before it, ~36 links across the Privacy Policy, Terms,
   About and Data Credits were rendering in the browser's DEFAULT #0000EE blue,
   because nothing here had ever styled them. That is off-palette and, on this
   paper, barely legible.

   VISITED IS THE SAME COLOUR, deliberately. :visited is nearly the only
   stateful colour a browser applies on its own, and the default #551A8B purple
   is both off-palette and — since the paper sits mid-luminance — no easier to
   read than the blue it replaces. Nothing on this site depends on knowing
   which pages you have already read.

   THE UNDERLINE IS NOT DECORATION, it is the WCAG 1.4.1 conformance mechanism,
   and the numbers are why. That criterion permits an unlinked look only when
   the link text reaches BOTH 3:1 against surrounding body text AND 4.5:1
   against the background. Measured against the paper figures recorded at the
   top of this file (median luminance 0.284):

       #8b1a1a vs --op-ink   2.10:1   (needs 3.00)
       #8b1a1a vs the paper  2.96:1   (needs 4.50)

   Both fail, and not marginally — so colour alone cannot carry it here and the
   underline is load-bearing. Worth knowing before someone removes it on the
   grounds that the NYT, Apple and GitHub have: they all did so in body copy
   they could afford to, and the shared advice is still that underlines come
   off navigation and link *lists*, not off links inside prose.

   Note also that this red is NOT a legibility improvement over the blue it
   replaces (2.96:1 vs 2.99:1) — it is a palette fix. --op-stamp-dark would
   reach 3.77:1 if the contrast ever matters more than matching the buttons. */
.word-lists-page a,
.word-lists-page a:visited {
    color: var(--op-stamp);
    text-decoration: underline;
    /* Real underlines: pushed clear of the descenders and thick enough to
       survive against the texture's grain at 13-16px. */
    text-underline-offset: 0.18em;
    text-decoration-thickness: 0.08em;
}

.word-lists-page a:hover,
.word-lists-page a:focus-visible {
    color: var(--op-stamp-dark);
    text-decoration-thickness: 0.14em;
}

/* --- Breadcrumb trail (the six nested pages) --- */

/* LAYOUT ONLY. The two links here deliberately inherit .word-lists-page a
   above — stamp red, underlined — instead of taking the muted, underline-free
   look breadcrumbs usually get. That underline is the WCAG 1.4.1 conformance
   mechanism documented above, and both contrast ratios fail without it, so a
   `.wl-breadcrumbs a { text-decoration: none }` override would quietly break
   conformance on the pages carrying it. If the trail ever needs to read
   lighter, the move is --op-stamp-dark (3.77:1), not removing the underline.

   The trailing item is a <span>, not an <a>, so it never picks up either and
   renders as plain ink — which is also correct breadcrumb behaviour, since
   linking to the page you are already on is noise. */
/* Bold for the same reason the rest of the small text on this paper is: at
   14px the texture's grain eats a normal weight. */
.wl-breadcrumbs {
    margin: 0 0 16px;
    font-size: 0.875rem;
    font-weight: bold;
}

.wl-breadcrumbs ol {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    /* Row gap carries the wrapped case: the longest trail here is "Recognizing
       Word Patterns & Common Endings", which wraps below ~400px. */
    gap: 2px 8px;
}

.wl-breadcrumbs li {
    display: flex;
    align-items: baseline;
    gap: 8px;
}

/* Decorative only — the <ol> carries the sequence semantically, so the
   separator is generated content rather than markup.

   Sized up and pushed to full ink because the angle quote is a thin glyph: at
   the inherited 14px muted it reads as a stray mark rather than a separator,
   even with the nav's bold weight on it. */
.wl-breadcrumbs li + li::before {
    content: "\203A";
    color: var(--op-ink);
    font-size: 1.8em;
    line-height: 1;
}

.wl-breadcrumbs [aria-current="page"] {
    color: var(--op-ink);
}

/* --- Generic page heading (home + other simple pages) --- */
.page-heading {
    color: var(--op-dark);
    text-align: center;
    margin: 0 0 8px;
}

.wl-intro {
    color: var(--op-dark);
    margin-bottom: 25px;
}

/* Opt-in modifier, not a change to .wl-intro itself -- that class is shared
   by every lesson page's intro. Requested for the Letter Frequency Strategy
   page and the About page, the same texture-grain legibility reasoning as
   .wl-sources above. */
.wl-intro-bold {
    font-weight: 600;
}

.page-intro {
    color: var(--op-dark);
    text-align: center;
    margin: 0 0 10px;
    font-weight: bold;
    font-style: italic;
}

.wl-card {
    background-color: var(--op-panel);
    border: 2px solid var(--op-card-border);
    border-radius: 12px;
    box-shadow: 0 2px 12px rgba(var(--op-olive-rgb), 0.15);
    padding: 18px 22px;
    margin-bottom: 20px;
}

.wl-card h2 {
    color: var(--op-olive-dark);
    font-size: 18px;
    margin-top: 0;
    margin-bottom: 10px;
}

.wl-card h2 a,
.wl-card h2 a:visited {
    color: var(--op-olive-dark);
    text-decoration: underline;
    text-decoration-color: var(--op-olive-border);
}

.wl-card h2 a:hover {
    text-decoration-color: currentColor;
}

.wl-card p {
    color: var(--op-text);
    margin: 0;
    line-height: 1.6;
}

/* .wl-card p is margin:0, and there is no general p + p rule for these cards,
   so a card that genuinely runs to several paragraphs has to opt in. Scoped to
   an explicit class rather than applied to every .wl-card, because the Privacy
   Policy and Terms reuse .wl-card heavily and re-spacing those is a separate
   decision from this one. */
.wl-card-prose p + p {
    margin-top: 10px;
}

/* Tool-to-word-list mapping on the Word Lists page. */
.wl-tool-lists {
    color: var(--op-text);
    line-height: 1.6;
    margin: 10px 0 0;
    padding-left: 22px;
}

.wl-tool-lists li + li {
    margin-top: 8px;
}

.wl-inline-link,
.wl-inline-link:visited {
    color: inherit;
    text-decoration: none;
}

.credit-quote {
    color: var(--op-text-muted);
    font-size: 14px;
    font-style: italic;
    line-height: 1.6;
    background-color: var(--op-panel-alt);
    border-left: 3px solid var(--op-olive-border);
    border-radius: 4px;
    padding: 10px 14px;
    margin-top: 10px !important;
}

.credit-quote a {
    color: var(--op-olive-dark);
}

.wl-credits-link {
    text-align: right;
    margin: -10px 0 20px;
}

.wl-credits-link a {
    color: var(--op-olive-dark);
    font-weight: bold;
    text-decoration: none;
}

.wl-credits-link a:hover {
    text-decoration: underline;
}

.wl-disclaimer {
    background-color: var(--op-panel-alt);
    border: 1px solid var(--op-border);
    border-left: 3px solid var(--op-olive);
    border-radius: 6px;
    padding: 14px 20px;
    margin-top: 30px;
}

.wl-disclaimer p {
    color: var(--op-text-muted);
    font-size: 14px;
    line-height: 1.6;
    margin: 0;
}

/* ==========================================================================
   LEGAL PAGES — Privacy Policy and Terms of Service
   ==========================================================================
   Both pages reuse .wl-card for their numbered sections, so only the parts
   with no equivalent elsewhere on the site are defined here: the dateline, the
   plain-English summary box, the table of contents, and the two-column tables.

   Sections carry ids and the ToC links to them, so :target needs a scroll
   offset — without it an anchored heading lands flush against the top of the
   viewport with its own text half under the browser chrome. */
.legal-dates {
    color: var(--op-text-muted);
    font-size: 14px;
    margin: -6px 0 22px;
}

.legal-summary {
    background-color: var(--op-panel);
    border: 2px solid var(--op-card-border);
    border-left: 6px solid var(--op-stamp);
    border-radius: 12px;
    box-shadow: 0 2px 12px rgba(var(--op-olive-rgb), 0.15);
    padding: 18px 22px;
    margin-bottom: 24px;
}

.legal-summary h2 {
    color: var(--op-stamp-dark);
    font-size: 18px;
    margin: 0 0 12px;
}

.legal-summary ul {
    margin: 0;
    padding-left: 20px;
    color: var(--op-text);
}

.legal-summary li {
    margin-bottom: 8px;
    line-height: 1.6;
}

.legal-summary-note {
    color: var(--op-text-muted);
    font-size: 13px;
    font-style: italic;
    margin: 14px 0 0;
}

.legal-toc {
    background-color: var(--op-panel-alt);
    border: 1px solid var(--op-border);
    border-radius: 10px;
    padding: 16px 22px;
    margin-bottom: 26px;
}

.legal-toc h2 {
    font-size: 15px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--op-text-muted);
    margin: 0 0 10px;
}

/* Two columns on anything roomy enough — a 17-item list is a lot of vertical
   scroll to put between the reader and the document itself. */
.legal-toc ol {
    margin: 0;
    padding-left: 22px;
    columns: 2;
    column-gap: 32px;
}

.legal-toc li {
    margin-bottom: 6px;
    line-height: 1.5;
    break-inside: avoid;
}

.legal-toc a,
.legal-toc a:visited {
    color: var(--op-olive-dark);
}

.word-lists-page .wl-card h3 {
    color: var(--op-dark-soft);
    font-size: 16px;
    margin: 18px 0 6px;
}

.word-lists-page .wl-card h3:first-of-type {
    margin-top: 10px;
}

.legal-list {
    color: var(--op-text);
    padding-left: 20px;
    line-height: 1.65;
}

.legal-list li {
    margin-bottom: 8px;
}

/* Tables scroll inside their own box rather than widening the page — the
   cookie table has four columns of prose and will not fit a phone. */
.legal-table-wrap {
    overflow-x: auto;
    margin: 14px 0 4px;
    border: 1px solid var(--op-border);
    border-radius: 8px;
}

.legal-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
    min-width: 460px;
}

.legal-table-caption {
    caption-side: top;
    text-align: left;
    padding: 10px 14px;
    font-size: 13px;
    font-weight: 600;
    color: var(--op-text-muted);
    background-color: var(--op-panel-alt);
    border-bottom: 1px solid var(--op-border);
}

.legal-table th,
.legal-table td {
    padding: 10px 14px;
    text-align: left;
    vertical-align: top;
    line-height: 1.55;
    border-bottom: 1px solid var(--op-border);
}

.legal-table thead th {
    background-color: var(--op-panel-alt);
    color: var(--op-dark);
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.legal-table tbody th {
    color: var(--op-dark);
    font-weight: 600;
    white-space: normal;
}

.legal-table tbody td {
    color: var(--op-text);
}

.legal-table tbody tr:last-child th,
.legal-table tbody tr:last-child td {
    border-bottom: none;
}

/* Letter Frequency Strategy lesson's sources line, above its table. Sits
   directly on the page's paper texture rather than inside a .wl-card panel,
   so it needs the same treatment .wl-breadcrumbs uses for the same reason:
   --op-text-faint measures 2.61:1 there against the documented 8.11:1 on
   flat --op-bg, well under WCAG's 4.5:1, and at this size a normal weight
   is eaten by the texture's grain regardless of color. --op-ink plus a
   heavier weight is what the rest of this file uses to fix text on the raw
   texture; --op-text-muted/--op-text-faint are only safe once wrapped in a
   panel surface like .wl-card, as the table below now is. */
.wl-sources {
    font-size: 13px;
    font-weight: 600;
    color: var(--op-ink);
    line-height: 1.6;
    margin: -6px 0 24px;
}

/* Letter Frequency Strategy lesson's table: reuses .legal-table's chrome
   (header background, row borders) but not its 460px min-width or its
   10px/14px padding -- five columns at that padding force the wrap's
   horizontal scrollbar at any phone width. table-layout: fixed splits the
   five columns evenly instead of sizing to content. */
.letter-freq-table {
    min-width: 0;
    table-layout: fixed;
    /* The outline moved here from .legal-table-wrap, on purpose: a
       <caption> renders as its own box OUTSIDE its table's border box
       (that's standard table rendering, not something set here), so
       bordering the <table> itself keeps the header/data grid outlined
       while leaving the caption above it unboxed -- bordering the wrap
       instead enclosed both together, which put a visible frame around
       the now-white caption too. */
    border: 1px solid var(--op-border);
    border-radius: 8px;
}

/* Scoped to this page's wrap, not the shared .legal-table-wrap rule -- the
   wrap's own border would otherwise double up with the one now on the
   table above. overflow-x: auto (the actual reason .legal-table-wrap
   exists) is kept. */
.letter-freq-table-wrap {
    border: none;
    border-radius: 0;
    margin: 14px 0 4px 0;
}

/* Scoped to this table, not the shared .legal-table-caption rule -- the
   caption and the header row both used --op-panel-alt by default, reading
   as one continuous shaded block with no visual break. Whitening just the
   caption (matching the surrounding .wl-card) leaves the shading to do its
   job of marking the header row alone. */
.letter-freq-table .legal-table-caption {
    background-color: var(--op-panel);
}

/* Equal fifths (the default under table-layout: fixed with no explicit
   widths) starved Contains, the widest header word at 8 characters, so
   Contains and Starts read as crowded on a phone -- not from too little
   total width, but from every column getting the same share regardless of
   what it actually needs to fit.

   Two DIFFERENT things can be the binding constraint per column, and they
   were measured separately rather than assumed: the header word (Letter 6,
   Value 5, Has 3, Start 5, End 3 characters -- "Contains"/"Ends" were
   shortened to "Has"/"End" because they still wrapped to two lines at
   phone width even after this column-width pass, and "Value" briefly went
   to "Val" for the same reason before being asked back -- the caption
   spells every word out in full regardless, so an abbreviated header costs
   nothing there) versus the data below it (a single letter and a 1-2
   digit tile value for the first two columns, but a "NN.N%" percentage
   for the last three, measured at ~44px wide at this table's font-size --
   e.g. "37.9%" in the End column). Has/Start/End all carry that same
   ~44px data regardless of how short their own header word is -- "End" is
   only 3 characters but its column still needs the same room as Start's,
   which an earlier version of these widths missed by sizing purely off
   header length and left it too narrow for its data to fit, so those
   three are kept close to equal and near their ~21% floor. Letter and
   Value get most of what's left; a header that's still briefly tight at
   the narrowest phone widths degrades by safely wrapping to a second line
   (now that .sort-label below can actually shrink and wrap) rather than
   by overflowing, which a numeric data cell cannot do (a percentage has
   nowhere to wrap). table-layout: fixed honours the first row's (the
   header's) cell widths, so setting them
   here is what retargets the columns. */
.letter-freq-table th:nth-child(1),
.letter-freq-table td:nth-child(1) {
    width: 20%;
}

.letter-freq-table th:nth-child(2),
.letter-freq-table td:nth-child(2) {
    width: 17%;
}

.letter-freq-table th:nth-child(3),
.letter-freq-table td:nth-child(3) {
    width: 21%;
}

.letter-freq-table th:nth-child(4),
.letter-freq-table td:nth-child(4) {
    width: 21%;
}

.letter-freq-table th:nth-child(5),
.letter-freq-table td:nth-child(5) {
    width: 21%;
}

.letter-freq-table th,
.letter-freq-table td {
    padding: 6px 4px;
    text-align: center;
}

/* The outer edges of the whole table (Letter's left, End's right) don't
   need the same 4px as every other edge -- that padding exists to keep
   adjacent COLUMNS visually separated, and there is no neighbouring column
   past either end to separate from; the table's own border already reads
   as the boundary there. Reclaiming it gives Letter's own header a little
   more room directly, and lets Value (column 2, sharing in the width
   reclaimed from End below) fit its header on one line at more widths. */
.letter-freq-table th:first-child,
.letter-freq-table td:first-child {
    padding-left: 1px;
}

.letter-freq-table th:last-child,
.letter-freq-table td:last-child {
    padding-right: 1px;
}

/* Sortable column header. box-sizing must be restated: `all: unset` resets
   it to content-box, which silently undid the page-wide border-box reset
   and rendered each button ~4px wider than its own cell (width: 100% plus
   padding, added on top rather than absorbed). min-width: 0 is also
   required on both the <th> and the button -- a flex item's default
   min-width: auto refuses to shrink below its own text's natural width
   inside a table-layout: fixed cell otherwise, which was the other half of
   the same horizontal-scroll bug. */
.letter-freq-table thead th {
    min-width: 0;
}

/* A bare text node placed directly inside a flex container (which
   .sort-btn is) is wrapped by the browser in an ANONYMOUS flex item that
   is not selectable from CSS -- so it keeps the default min-width: auto
   no matter what min-width is set on the button around it, and refuses to
   shrink below its own unwrapped text width. That silently overflowed the
   header once Letter/Value's columns got narrower than their label's
   natural width. Wrapping the label in this real, selectable span is what
   makes min-width: 0 (and therefore wrapping) actually reach it. */
.letter-freq-table .sort-label {
    display: inline-block;
    min-width: 0;
    overflow-wrap: break-word;
    /* text-wrap requires a block-level (or inline-block) box to apply, which
       is the other reason this needed its own real element beyond the
       min-width fix above. Without it, a forced 2-line break at the
       narrowest phone widths could leave a single orphaned letter on its
       own line (e.g. "LETTE/R") -- balance instead splits close to evenly
       ("LET/TER"). Progressive enhancement: unsupported browsers just fall
       back to the ordinary break-word wrap already in place. */
    text-wrap: balance;
}

.letter-freq-table .sort-btn {
    all: unset;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
    width: 100%;
    gap: 2px;
    padding: 7px 2px;
    cursor: pointer;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.01em;
    line-height: 1.15;
    text-transform: uppercase;
    color: var(--op-dark);
    white-space: normal;
    overflow-wrap: break-word;
}

/* Has/Start/End are sized for their "NN.N%" data (measured earlier as a
   hard floor -- narrowing these columns reopens the data-overflow bug),
   which leaves their own short header word centered in a box much wider
   than it needs. A first attempt at fixing that used justify-content:
   space-between, which was wrong in the other direction -- it pushed the
   label and its OWN arrow apart from each other, growing the gap between a
   header and its sort control instead of shrinking it, so the arrow read
   as belonging to the next column over rather than the one it sorts.
   Keeping them as one tight group (gap: 2px, unchanged) and only moving
   that whole group is what was actually asked for. */
/* Mobile only -- on desktop, every column has plenty of width relative to
   its content regardless, so Has/Start/End centered there just fine before
   any of this, and jamming End flush right on a wide desktop table (while
   Letter/Value/Has/Start stay centered beside it) reads as broken, not
   tightened -- a real regression this had until it was scoped here. */
@media (max-width: 600px) {
    .letter-freq-table th:nth-child(5) .sort-btn {
        /* End only: pushes the tight label+arrow group flush to the
           table's right edge, which is the "space between End and the
           table edge" half of the request. Has/Start have no outer edge to
           anchor to and stay centered, same as before this whole change. */
        justify-content: flex-end;
    }
}

/* Same outer-edge reasoning as the <th>/<td> padding above, applied to the
   button's own internal padding, which is separate from the cell's. */
.letter-freq-table th:first-child .sort-btn {
    padding-left: 0;
}

.letter-freq-table th:last-child .sort-btn {
    padding-right: 0;
}

/* Widened from a 360px-only insurance rule to the same 600px mobile
   breakpoint used elsewhere on this table: it was only closing the gap for
   Letter/Start right at the narrowest phones, while wider ones (375-600px)
   were still carrying the full 10.5px size for no real reason -- data rows
   get their own, separately-set font-size and are not affected by this. */
@media (max-width: 600px) {
    .letter-freq-table .sort-btn {
        font-size: 9.5px;
        padding: 7px 1px;
    }

    .letter-freq-table tbody th,
    .letter-freq-table tbody td {
        font-size: 12px;
    }
}

/* Has/Start/End are sized wide enough to hold their "NN.N%" data safely
   (that floor can't move without reopening the data-overflow bug measured
   above), but their own header words are short, so on mobile they read as
   loosely padded compared to Letter/Value next to them. Tightening their
   padding here doesn't fight that floor -- it only affects how much of the
   column's width the button's own box claims, so it makes the header look
   tighter while, if anything, leaving slightly MORE room for the data
   cells below at the same column width. */
@media (max-width: 600px) {
    .letter-freq-table th:nth-child(3) .sort-btn,
    .letter-freq-table th:nth-child(4) .sort-btn,
    .letter-freq-table th:nth-child(5) .sort-btn {
        padding-top: 4px;
        padding-bottom: 4px;
        padding-left: 0;
        padding-right: 0;
    }
}

/* .wl-card's standard 22px side padding was never part of the table's own
   column math above -- it's real width the table never had access to in
   the first place, eaten before the five columns ever get split. Cutting
   it on mobile hands the whole table several more px per column (roughly
   28px total back on a 320-400px phone), which is the more direct lever
   than any further per-column squeezing: it lets Has/Start/End's already
   safe data-driven widths carry proportionally less empty space around
   their short header words, without touching the width split at all. */
@media (max-width: 600px) {
    .letter-freq-card {
        padding-left: 8px;
        padding-right: 8px;
    }
}

.letter-freq-table .sort-btn:hover,
.letter-freq-table .sort-btn:focus-visible {
    background-color: var(--op-olive-soft);
}

.letter-freq-table .sort-btn:focus-visible {
    outline: 2px solid var(--op-stamp);
    outline-offset: -2px;
}

.letter-freq-table .sort-arrow {
    font-size: 12px;
    flex-shrink: 0;
    color: var(--op-text-muted);
}

.letter-freq-table th[aria-sort="ascending"] .sort-arrow,
.letter-freq-table th[aria-sort="descending"] .sort-arrow {
    color: var(--op-stamp);
}

/* Mobile-only: the sorted column's arrow STAYS red here (matching desktop)
   -- what needed fixing was something else that reads as red/pink and was
   being mistaken for it. --op-olive-soft (used for both :hover and
   :focus-visible background) is a pale PINK, not the neutral its name
   suggests, and a tap can trigger either or both of those pseudo-classes
   depending on the browser -- most mobile browsers apply :focus-visible
   after a tap the same as a keyboard focus (no reliable way to tell the
   two apart), pairing that pink with a 2px --op-stamp outline, and some
   engines also leave :hover matched after a touch with no cursor to clear
   it. Either way it can STICK on the whole header cell until the reader
   taps elsewhere. That combination, not the arrow, is the "red column
   background." Neutralised on mobile only, for both pseudo-classes; kept
   at full strength on desktop, where :focus-visible is reserved for actual
   keyboard users and a visible ring is the right call for accessibility,
   and :hover behaves normally (clears the instant the pointer leaves). */
@media (max-width: 600px) {
    .letter-freq-table .sort-btn:hover,
    .letter-freq-table .sort-btn:focus-visible {
        background-color: transparent;
    }

    .letter-freq-table .sort-btn:focus-visible {
        outline: 2px solid var(--op-text-muted);
        outline-offset: -2px;
    }
}

.letter-freq-table th[aria-sort="ascending"] .sort-arrow::after {
    content: "\25B2";
}

.letter-freq-table th[aria-sort="descending"] .sort-arrow::after {
    content: "\25BC";
}

.letter-freq-table th[aria-sort="none"] .sort-arrow::after {
    content: "\2195";
}

.letter-freq-table tbody td {
    font-variant-numeric: tabular-nums;
}

.legal-address {
    font-style: normal;
    line-height: 1.7;
    color: var(--op-text);
    background-color: var(--op-panel-alt);
    border-left: 3px solid var(--op-olive-border);
    border-radius: 6px;
    padding: 12px 16px;
    margin: 10px 0 14px;
}

/* Anchored sections clear the sticky header instead of hiding under it.
   20px was a guess and left the target's top ~110px under the header
   (measured 132px tall, and it is fluid — see --nav-header-height in
   unscrambler.js). Read the real height from that same custom property,
   set on load/resize, so this tracks the header instead of assuming a
   size; the 132px fallback is only for the no-JS case. */
.word-lists-page [id] {
    scroll-margin-top: calc(var(--nav-header-height, 132px) + 20px);
}

@media (max-width: 560px) {
    .legal-toc ol {
        columns: 1;
    }
}

/* ==========================================================================
   AD PLACEHOLDER MOCKUPS — visual only, no ad network wired in yet
   ========================================================================== */
.ad-placeholder-label {
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--op-text-muted);
    text-align: center;
    line-height: 1.6;
}

.ad-placeholder-size {
    font-weight: 400;
    text-transform: none;
    font-size: 12px;
    letter-spacing: normal;
}

/* Side skyscrapers — real flex columns beside .main-content (not fixed
   overlays), so their space is always reserved and can never overlap the
   centered content column. Fixed at the standard 160x600 ad unit size. */
.ad-skyscraper {
    display: none;
    box-sizing: border-box;
}

@media (min-width: 1180px) {
    .ad-skyscraper {
        display: flex;
        align-items: center;
        justify-content: center;
        align-self: flex-start;
        flex: 0 0 160px;
        width: 160px;
        height: 600px;
        margin-top: 110px;
        position: sticky;
        /* .main-header is sticky too, at a measured 132px tall. `top` here used
           to be 110px — less than that — so once scrolling pinned the ad, its
           top 22px sat behind the header instead of below it: no gap, and the
           ad partly hidden. 240px clears the header with room to spare, giving
           a pinned-state gap (108px) in line with the ad's resting gap below
           it before the footer (~106px on a typical viewport), rather than the
           overlap this replaced. margin-top is untouched, so the unscrolled
           gap (126px) is unchanged and the transition into the pinned state
           stays the smooth native sticky animation. */
        top: 240px;
        background: repeating-linear-gradient(45deg, var(--op-panel-alt), var(--op-panel-alt) 12px, var(--op-panel) 12px, var(--op-panel) 24px);
        border: 2px dashed var(--op-border);
        border-radius: 8px;
        z-index: 5;
    }

    .ad-skyscraper-left {
        margin-left: 20px;
    }

    .ad-skyscraper-right {
        margin-right: 20px;
    }

    /* Production before real ad units exist: the column keeps its space, so
       the homepage grid stays the width it was designed at and an ad can drop
       in later without moving anything, but nothing is drawn in it. */
    .ad-skyscraper.ad-slot-reserved {
        background: none;
        border: none;
    }
}

/* Inline banner after results — shown on all sizes, only when results render */
.ad-slot-inline {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    max-width: 728px;
    height: 90px;
    margin: 30px auto 10px;
    background: repeating-linear-gradient(45deg, var(--op-panel-alt), var(--op-panel-alt) 12px, var(--op-panel) 12px, var(--op-panel) 24px);
    border: 2px dashed var(--op-border);
    border-radius: 8px;
}

@media (max-width: 600px) {
    .ad-slot-inline {
        height: 100px;
    }
}

/* A real responsive unit (the mockup rules above are for development). Block,
   not flex: as a flex item the display:block <ins> shrinks to zero width, and
   _ad_slot.html never requests an ad for a slot with no width. Height comes
   from the ad Google picks, which can be taller than 90px on a phone. */
.ad-slot-inline.ad-slot-reserved {
    display: block;
    height: auto;
    background: none;
    border: none;
    border-radius: 0;
}

/* AdSense marks a slot it had nothing for as unfilled. Hide the whole slot
   then, margins included, rather than leave a blank band between results. */
.ad-slot-inline:has(ins.adsbygoogle[data-ad-status="unfilled"]) {
    display: none;
}

/* ==========================================================================
   HOMEPAGE EMPTY STATE (core/partials/_home_intro.html)
   Only renders when there is no query, so none of this competes with results.
   Width matches .word-lists-page — .search-page-wrapper is unstyled and would
   otherwise let this run the full width of .main-content.
   ========================================================================== */
.home-brief {
    max-width: 800px;
    margin: 40px auto 0;
    padding-bottom: 20px;
}

.home-brief .wl-card h2 {
    color: var(--op-olive-dark);
    font-size: 18px;
    margin: 0 0 10px;
}

.home-brief .wl-card p + p {
    margin-top: 12px;
}

.home-brief .wl-card a {
    color: var(--op-olive-dark);
    font-weight: bold;
    white-space: nowrap;
}

.home-brief 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;
}

.wl-pill-link {
    display: inline-block;
    transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
                box-shadow 0.25s cubic-bezier(0.22, 1, 0.36, 1),
                border-color 0.25s cubic-bezier(0.22, 1, 0.36, 1),
                background-color 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}

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

.home-section-intro {
    color: var(--op-text);
    line-height: 1.6;
    margin: 0 0 14px;
}

/* Crawl entry point into the query space — see the partial's comment. */
/* Crawl entry point into the query space — see _home_intro.html's comment.
   Rides on .accordion-list rather than being its own flex row: the shared
   .word-panel spans the row via `grid-column: 1 / -1`, which only exists
   inside a grid. This rule must therefore NOT set `display`, or it would
   override .accordion-list — it is later in the file and would win.

   The deal is 14 racks, so 7 across lays them out as two even rows — but only
   once .home-brief has reached its 800px cap, which needs about 850px of
   viewport. Below that the card is narrower than 7 legible tracks (at 768px it
   is 650px wide, giving 83px a track against the ~88px a rack chip needs), so
   the fluid track is the default and the fixed 7 is the enhancement. Getting
   this backwards ellipsises every rack on tablets. margin-bottom keeps the
   card's own 20px rather than .accordion-list's 35px. */
.rack-grid {
    grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
    margin-bottom: 20px;
}

@media (min-width: 850px) {
    .rack-grid {
        grid-template-columns: repeat(7, 1fr);
    }
}

/* Sized to fit a 7-across track: 7 monospace characters at 14px with 1px
   tracking measure ~66px, leaving room for the header padding and arrow
   trimmed below. Scoped to .rack-grid so result tiles keep their 16px. */
.rack-grid .word-text.rack-chip {
    font-size: 14px;
}

.rack-grid > .word-accordion > .word-header {
    --pill-pad-x: 6px;
}

.rack-grid .accordion-arrow {
    font-size: 9px;
    padding-left: 4px;
}

/* Top-5 pill grid inside the open rack's panel. Only .accordion-list's bottom
   margin is wrong in this context — it would push the "See all words" link
   35px clear of the pills. Everything else, including both mobile column
   formulas, is wanted as-is. Later in the file than .accordion-list at equal
   specificity, so it also wins inside the media queries above.

   Note this is itself an .accordion-list but holds no .word-panel, which is
   how unscrambler.js knows to leave its (inert) pills alone. */
.rack-word-list {
    margin-bottom: 0;
}

/* Now sits on the .word-text span inside a rack tile's button, not on an
   <a class="detail-chip">. Nothing here is anchor-specific, so it transplants
   as-is. Tracking is 1px rather than 1.5px: across 7 characters the wider
   setting cost ~4px, which is the difference between fitting and ellipsising
   in a 7-across track. */
.rack-chip {
    text-transform: uppercase;
    letter-spacing: 1px;
    font-family: 'Courier New', Courier, monospace;
}

.brief-link-list {
    margin: 0;
    padding-left: 20px;
    color: var(--op-text);
    line-height: 1.7;
}

.brief-link-list li + li {
    margin-top: 8px;
}

.brief-link-list a,
.brief-link-list a:visited {
    color: var(--op-olive-dark);
    font-weight: bold;
    text-decoration: underline;
    text-decoration-color: var(--op-olive-border);
}

.brief-link-list a:hover {
    text-decoration-color: currentColor;
}

@media (max-width: 600px) {
    .home-brief {
        margin-top: 28px;
    }
}

/* --- Card header row ------------------------------------------------------
   Title on the left, Shuffle on the right. Used by the Take a Turn rack grid
   (core/partials/_rack_grid.html), which re-deals its own card in place. */
.card-header-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 10px;
}

.card-header-row h2 {
    margin-bottom: 0;
}

.shuffle-btn {
    flex-shrink: 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--op-olive-dark);
    background: var(--op-panel);
    border: 1px solid var(--op-olive-border);
    border-radius: 999px;
    padding: 6px 14px;
    cursor: pointer;
}

.shuffle-btn:hover {
    background: var(--op-olive-border);
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
