.grid-cell {
    /* A4-12 (1.4.12): min-* rather than fixed, so increased line-height /
       letter-spacing grows the cell instead of clipping the letter.

       THE MINIMUM IS 24, NOT 48. A 48px floor cannot shrink, and the grid is
       12/18/24 cells wide by difficulty: at 48px a hard grid measures 24 x 48
       + gaps = ~1218px and simply ran off the side of the screen, under the
       promo column and past the viewport. 48px is now the PREFERRED size
       (flex-basis) that cells shrink from, and 24px the floor — which is
       2.5.8's actual target-size minimum, so shrinking never costs a
       criterion. Still min-*, so text-spacing growth pushes the box out. */
    min-width: 24px;
    min-height: 24px;
    /* Square whatever width the row settles on, rather than being told a size
       twice and hoping the two agree. */
    aspect-ratio: 1 / 1;
    border: 0px solid #ddd;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    cursor: pointer;
    user-select: none;
    /* Fallback for a grid with no difficulty class on it (the multiplayer
       board): scales with the viewport rather than sitting at 1.4rem while
       the cell under it shrinks to 24px. */
    font-size: clamp(0.75rem, 1.8vw, 1.4rem);
    position: relative;
    background-color: white;
    border-radius: 8px;
}

@media (max-width: 576px) {
    .grid-cell {
        /* 2.5.8 Target Size (Minimum) is 24x24 CSS px, and 24 MEETS it — the
           a11y pass raised this to 28 for "tolerance", which is not a criterion
           and cost real ground under 1.4.10 Reflow: with 4px gaps and 10px of
           #game-grid padding, 28px cells push a 10-wide grid to 336px, past the
           320px reflow floor, where 24px cells land at 296px. Reflow is an AA
           criterion; extra target-size headroom is not. Back to the original
           24, kept as min-* for A4-12. */
        min-width: 24px;
        min-height: 24px;
        font-size: 0.8rem;
    }
}

.grid-cell.selected {
    background-color: #007bff;
    color: white;
}

.word-found-highlight {
    /* 1.4.3 — `color: white !important` used to live here and beat every later
       rule AND the renderer's inline colour, putting white on ten pastels as
       low as 1.78:1. The letter colour is now chosen per fill: see
       `#game-grid .grid-cell.word-found-highlight.highlight-color-N` below for
       the legacy grid, and WordSearchRenderer.textColorFor for the modular one. */
    animation: foundAnimation 0.4s ease-in-out;
    pointer-events: auto !important;
}

@keyframes foundAnimation {
    0% { transform: scale(.4); }
    50% { transform: scale(1.2); }
    80% { transform: scale(.8); }
    100% { transform: scale(1); }
}

.highlight-color-0 { background-color: #FF6B6B;  opacity: 90%}
.highlight-color-1 { background-color: #4ECDC4;  opacity: 90%}
.highlight-color-2 { background-color: #45B7D1;  opacity: 90%}
.highlight-color-3 { background-color: #96CEB4;  opacity: 90%}
.highlight-color-4 { background-color: #A163BA;  opacity: 90%}
.highlight-color-5 { background-color: #3498DB;  opacity: 90%}
.highlight-color-6 { background-color: #F1A94E;  opacity: 90%}
.highlight-color-7 { background-color: #2ECC71;  opacity: 90%}
.highlight-color-8 { background-color: #E74C3C;  opacity: 90%}
.highlight-color-9 { background-color: #1ABC9C;  opacity: 90%}

.word-list {
    list-style-type: none;
    padding: 0;
    font-weight: 600;
}

.word-item {
    display: flex;
    align-items: center;
    margin-bottom: 0.5rem;
    font-size: 1.1rem;
}

.word-text {
    flex-grow: 1;
}

.hint-btn {
    /* A4-10 (2.5.8): an icon plus 4px of padding fell under 24x24. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    min-height: 24px;
    padding: 4px;
    margin-left: 8px;
    cursor: pointer;
    color: #6c757d;
    transition: color 0.2s;
}

.hint-btn:hover {
    color: #0d6efd;
}

.word-found {
    text-decoration: line-through;
    opacity: 0.8;
}

.hint-highlight {
    background-color: rgba(255, 193, 7, 0.5);
}

#game-grid {
    display: flex;
    flex-direction: column;
    gap: var(--ws-run-gap);
    background-color: white;
    padding: 10px;
    border-radius: 16px;
    box-shadow: 0 0 10px rgba(0,0,0,0.1);
    /* fit-content already shrinks toward the space available; without a
       max-width to shrink against it took its max-content width and overflowed
       instead. Below the 24px floor a word search genuinely needs two
       dimensions, so the grid scrolls inside its own box rather than pushing
       the page sideways — 1.4.10 exempts content that requires 2D layout. */
    width: 100%;
    overflow-x: auto;
}

#timer {
    font-size: 1.5rem;
    font-weight: bold;
    color: #007bff;
}
.hint {
    background-color: #fff6c3 !important;
    box-shadow: 0 0 15px rgba(255, 215, 0, 0.6);
    filter: blur(0.6px);
    z-index: 1;
}

.word-found.highlight-color-0 { color: #FF6B6B; }
.word-found.highlight-color-1 { color: #4ECDC4; }
.word-found.highlight-color-2 { color: #45B7D1; }
.word-found.highlight-color-3 { color: #96CEB4; }
.word-found.highlight-color-4 { color: #9B59B6; }
.word-found.highlight-color-5 { color: #3498DB; }
.word-found.highlight-color-6 { color: #F1A94E; }
.word-found.highlight-color-7 { color: #2ECC71; }
.word-found.highlight-color-8 { color: #E74C3C; }
.word-found.highlight-color-9 { color: #1ABC9C; }

/* New classes for text color only */
.word-found-text-0 { color: #FF6B6B; }
.word-found-text-1 { color: #4ECDC4; }
.word-found-text-2 { color: #45B7D1; }
.word-found-text-3 { color: #96CEB4; }
.word-found-text-4 { color: #9B59B6; }
.word-found-text-5 { color: #3498DB; }
.word-found-text-6 { color: #F1A94E; }
.word-found-text-7 { color: #2ECC71; }
.word-found-text-8 { color: #E74C3C; }
.word-found-text-9 { color: #1ABC9C; }

.grid-row {
    display: flex;
    flex-direction: row;
    /* Same single source as the bridge. Inside .word-search-grid the variable
       resolves to 2px by inheritance, which is why the modular rows no longer
       need an inline gap. Declared HERE, on the shared rule, because the
       modular grid uses it too — moving it under #game-grid took the gap away
       from that grid entirely. */
    gap: var(--ws-run-gap);
    /* Cells cap at 48px, so a small grid in a wide column leaves slack at the
       end of every row. Centred, that slack reads as margin; left alone it
       read as the grid being shoved against the word list. */
    justify-content: center;
}

/* ONE SET OF COLUMNS FOR THE WHOLE BOARD, NOT ONE PER ROW.
   Rows used to be flex with `flex: 1 1 0` cells, and flex hands out CONTENT
   space - so a cell with a fatter border ends up wider overall. A word found
   by another player carries a 2px border (.word-removed-grid), which measured
   38.11px against its plain neighbours' 36.55px. The row's total width does
   not change, so the rest of that row shrinks to absorb it and EVERY cell in
   the row lands at a different x from the row above. On screen that showed up
   as vertical found-word bands stepping sideways partway down, which is what
   the bug looked like from the outside.

   Fixed tracks make it impossible: the column decides the width, not the cell,
   so no border, outline or state on one cell can move its neighbours.

   --ws-cols is written by _renderGrid from the board it is actually drawing,
   NOT inferred from the difficulty class: a joiner adopts the host's size,
   which need not be one of the three the difficulty classes name. The 12
   fallback only applies if script has not run, when there are no cells. */
#game-grid .grid-row {
    display: grid;
    grid-template-columns: repeat(var(--ws-cols, 12), minmax(24px, 48px));
}

/* New Game Modal Styles */
.word-set-btn {
    text-align: left;
    padding: 1rem;
    font-size: 1.1rem;
    transition: all 0.2s ease;
}

.word-set-btn:hover {
    transform: translateX(5px);
}

.word-set-btn.selected {
    background-color: #0d6efd;
    color: white;
}

#multiplayer-options {
    transition: all 0.3s ease;
}

.btn-check:checked + .btn-outline-primary {
    background-color: #0d6efd;
    color: white;
}

#connection-status {
    padding: 0.5rem;
    margin-bottom: 1rem;
    border-radius: 4px;
    background-color: #f8f9fa;
    font-size: 0.9rem;
}

.player-item {
    padding: 12px;
    padding-bottom: 1rem;
    margin-bottom: 8px;
    border-radius: 4px;
    background-color: #f8f9fa;
    font-size: 0.875rem;
}

.player-item.current-player {
    border: 2px solid #0d6efd;
}

.player-name {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.player-name-text {
    font-weight: 500;
}

.connected-status {
    font-size: 0.5rem;
}

.player-item.disconnected .connected-status {
    color: var(--bs-danger) !important;
}

.badge-container {
    display: flex;
    gap: 4px;
}

.player-progress {
    margin-top: 8px;
}

.player-progress .progress {
    background-color: rgba(0,0,0,0.05);
}

.player-progress .progress-bar {
    min-width: 2em;
}

.badge {
    margin-left: 5px;
}

.player-count {
    font-weight: bold;
    color: #666;
}

.progress {
    height: 20px;
    background-color: #f0f0f0;
}

.progress-bar {
    background-color: #0d6efd;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8em;
    transition: width 0.3s ease;
}

#countdown, #modal-countdown {
    font-size: 6rem;
    font-weight: bold;
    color: #0d6efd;
    text-shadow: 0 0 20px rgba(13, 109, 253, 0.5);
    /* 2.2.2 Pause, Stop, Hide: was `infinite`. The countdown itself is
       short-lived, but the element is also left in the DOM while a host
       waits, where the blink ran unbounded. */
    animation: epicCountdown 1s 5;
}

@keyframes epicCountdown {
    0% {
        transform: scale(1);
        opacity: 1;
    }
    50% {
        transform: scale(1.2);
        opacity: 0.8;
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

/* Add styles for the countdown container */
.countdown-container {
    min-height: 120px;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Make the waiting spinner fade out when countdown starts */
.countdown-active .spinner-border {
    display: none;
}

/* Make the waiting message fade out when countdown starts */
.countdown-active .lead {
    opacity: 0.5;
    font-size: 1rem;
}

.waiting-message {
    color: #6c757d;
}

/* Add these new classes */
/* Only the letter scales per difficulty now. The BOX is settled by the flex
   basis and the space in the row (see .grid-cell) — these rules used to set
   width AND height outright, which both defeated that and, being fixed
   heights, clipped the letter under increased text spacing (1.4.12). The grid
   is 12 / 18 / 24 cells wide, so the letter has to come down as it widens or
   it outgrows its own cell and stretches it out of square. */
.difficulty-easy .grid-cell {
    font-size: clamp(0.9rem, 2.2vw, 1.6rem);
}

.difficulty-medium .grid-cell {
    font-size: clamp(0.75rem, 1.5vw, 1.25rem);
}

.difficulty-hard .grid-cell {
    font-size: clamp(0.65rem, 1.1vw, 1rem);
}

@media (max-width: 576px) {
    .difficulty-easy .grid-cell {
        font-size: 1rem;
    }
    
    .difficulty-medium .grid-cell {
        font-size: 0.9rem;
    }
    
    .difficulty-hard .grid-cell {
        font-size: 0.8rem;
    }
}

/* Add styles for disabled start button */
#start-game:disabled {
    cursor: not-allowed;
    opacity: 0.6;
}

.waiting-screen {
    min-height: 300px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.waiting-screen .card {
    /* Capped and centred rather than edge to edge. It now sits in a full-width
       column (the words column beside it is hidden while you wait, but still
       reserved its quarter of the row, which is what pushed this card off
       centre), and a line of text stretched across the whole panel is not a
       card - it is a band. */
    width: 100%;
    max-width: 36rem;
    margin-inline: auto;
    background-color: white;
    border-radius: 16px;
    box-shadow: 0 0 10px rgba(0,0,0,0.1);
}

.room-code-display {
    font-size: 1.2rem;
    padding: 1rem;
    background-color: #f8f9fa;
    border-radius: 8px;
}

#multiplayer-controls {
    height: 100%;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

#game-status {
    font-size: 0.9rem;
    padding: 0.5rem 1rem;
}

/* Update the existing timer styles */
#timer {
    display: none;
}

#timer.active {
    display: block;
}

#current-game-code {
    font-size: 1rem;
    padding: 0.5rem 1rem;
}

#connection-status {
    padding: 0.5rem;
    margin-bottom: 1rem;
    border-radius: 4px;
    background-color: #f8f9fa;
    font-size: 0.9rem;
}

.card.h-100 {
    height: 100%;
    display: flex;
    flex-direction: column;
}

.card.h-100 .card-body {
    flex: 1;
}

/* Add these styles at the end of the file */
.ping-wave {
    position: fixed;
    font-size: 9rem;
    opacity: 0.4;
    pointer-events: none;
    z-index: 9999;
    animation: waveAndFade 1s ease-out forwards;
    text-shadow: 0 0 10px rgba(0,0,0,0.2);
}

@keyframes waveAndFade {
    0% {
        opacity: 0.4;
        transform: scale(1) rotate(0deg) translate(0, 0);
    }
    20% {
        transform: scale(1.1) rotate(15deg) translate(10px, -10px);
    }
    40% {
        transform: scale(1.1) rotate(-10deg) translate(-5px, 5px);
    }
    60% {
        transform: scale(1.1) rotate(5deg) translate(5px, -5px);
    }
    80% {
        transform: scale(1.2) rotate(-5deg) translate(-2px, 2px);
    }
    100% {
        opacity: 0;
        transform: scale(1.5) rotate(0deg) translate(0, 0);
    }
}

#test-ping .badge {
    font-family: monospace;
    font-size: 0.9em;
    padding: 0.4em 0.6em;
}

/* Add these styles if not already present */
#audio-button {
    margin-top: 10px;
}

#audio-player {
    display: none;
}

.word-removed {
    color: #adb5bd !important;
    text-decoration: line-through;
    opacity: 0.7;
    pointer-events: none;
    background-color: #f8f9fa;
    padding: 2px 5px;
}

.found-by {
    font-size: 0.8em;
    color: #6c757d;
    font-style: italic;
    margin-left: 5px;
}

#multiplayer-options .form-text {
    font-size: 0.85em;
    color: #6c757d;
}

#multiplayer-options .form-text span {
    display: block;
    margin-top: 0.25rem;
}

.word-removed-grid {
    background-color: #e9ecef !important;
    color: #adb5bd !important;
    border: 2px solid #dc3545 !important;
}

.word-removed-grid.selected {
    background-color: #e9ecef !important;
    color: #adb5bd !important;
}

/* Header styles */
.navbar {
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* Sidebar styles */
.sidebar {
    position: sticky;
    top: 20px;
}

/* Footer styles */
footer {
    margin-top: auto;
}

footer a {
    text-decoration: none;
}

footer a:hover {
    text-decoration: underline;
}

/* Advertisement spaces */
.ad-space {
    min-height: 250px;
    background-color: #f8f9fa;
    border: 1px solid #dee2e6;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 1rem;
}

/* Responsive adjustments */
@media (max-width: 991.98px) {
    .sidebar {
        margin-bottom: 1rem;
    }
}

/* Make the main content container responsive */
.container-fluid {
    max-width: 1920px;
    margin: 0 auto;
}

/* Add some spacing for the game content */
.game-container {
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0,0,0,0.1);
    padding: 20px;
}

/* Add these styles for more compact controls */
.player-item {
    padding: 4px 8px;
    margin-bottom: 4px;
    border-radius: 4px;
    background-color: #f8f9fa;
    font-size: 0.875rem;
}

.player-progress {
    margin-top: 2px;
}

#multiplayer-controls .badge {
    font-size: 0.75rem;
}

/* Make the connection status more subtle */
#connection-status {
    font-size: 0.75rem;
    padding: 4px 8px;
    margin-bottom: 8px;
}

/* Adjust spacing in the card */
.card-body {
    padding: 0.75rem;
}

.card-header {
    padding: 0.5rem 0.75rem;
}

#audio-button.muted .bi-volume-up-fill {
    display: none;
}

#audio-button:not(.muted) .bi-volume-mute-fill {
    display: none;
}

#audio-button {
    padding: 0.25rem 0.5rem;
}

#audio-button i {
    font-size: 1.1rem;
}

/* New Game Modal Styles */
.game-mode-sidebar {
    background-color: #f8f9fa;
    border-right: 1px solid #dee2e6;
    min-width: 200px;
}

/* The label over Competitive and Time Attack. Quiet enough to read as a
   grouping rather than as a fourth mode somebody might try to click, and
   #6a7178 on this sidebar's #f8f9fa is 4.69:1 - it is small text, so it still
   owes the full 4.5:1 whether or not it is interactive. */
.game-mode-sidebar__group {
    margin: 0.75rem 0 0.25rem;
    padding: 0 1.5rem;
    color: #6a7178;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.game-mode-sidebar .nav-link {
    padding: 1rem 1.5rem;
    color: #495057;
    border-radius: 0;
    border-left: 3px solid transparent;
}

.game-mode-sidebar .nav-link:hover {
    background-color: #e9ecef;
}

.game-mode-sidebar .nav-link.active {
    background-color: #fff;
    border-left-color: #0d6efd;
    color: #0d6efd;
}

.game-mode-sidebar .nav-link i {
    font-size: 1.25rem;
    margin-right: 0.75rem;
    width: 24px;
    text-align: center;
}

.game-options {
    max-width: 600px;
}

.modal-lg {
    max-width: 800px;
}

.tab-content {
    min-height: 400px;
}

/* Make difficulty buttons more appealing */
.btn-group .btn-outline-success:hover,
.btn-group .btn-outline-warning:hover,
.btn-group .btn-outline-danger:hover {
    color: white;
}

.btn-group .btn-check:checked + .btn-outline-success,
.btn-group .btn-check:checked + .btn-outline-warning,
.btn-group .btn-check:checked + .btn-outline-danger {
    color: white;
}

/* Add some animation to the tabs */
.tab-pane {
    animation: fadeIn 0.3s ease-in-out;
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}

/* Add these styles for the multiplayer status indicators */
.room-code {
    font-size: 1rem;
    color: var(--bs-primary);
}

.connection-status {
    display: inline-flex;
    align-items: center;
}

.connection-status i {
    font-size: 0.75rem;
}

.connected-icon {
    display: none;
}

.disconnected-icon {
    display: none;
}

/* Show appropriate icon based on connection state */
.is-connected .connected-icon {
    display: inline-block;
}

.is-disconnected .disconnected-icon {
    display: inline-block;
}

.room-info, .status-info {
    background: #fff;
    padding: 8px;
    border-radius: 4px;
}

.game-status {
    font-size: 0.9rem;
    color: var(--bs-secondary);
}

/* Add these styles for the room code display and copy button */
.room-code-display {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0.5rem;
    margin-bottom: 1rem;
    border-radius: 4px;
    background-color: #f8f9fa;
}

.copy-code-btn {
    padding: 4px 8px;
    display: inline-flex;
    align-items: center;
    transition: all 0.2s ease;
}

.copy-code-btn:hover {
    background-color: #e9ecef;
}

.copy-code-btn.copied {
    background-color: #198754;
    color: white;
    border-color: #198754;
}

.copy-code-btn.copied i::before {
    content: "\F26B"; /* Bootstrap icon for check mark */
}

#sync-status {
    transition: all 0.3s ease;
}

#sync-status i {
    font-size: 0.8rem;
    margin-right: 0.25rem;
}

.sync-warning {
    color: #856404;
}

.out-of-sync #game-grid {
    opacity: 0.9;
}

.word-search-grid {
    /* A4-4 (2.4.7): the browser default ring used to be zeroed here, leaving
       the app's most important focusable element with no indicator whenever
       the conditional .current substitute was absent (A2-17). */
    outline: revert;
}

.word-search-grid:focus-visible {
    outline: 3px solid #0d6efd;   /* 4.27:1 on #f8f9fa - clears 1.4.11 */
    outline-offset: 2px;
}

.grid-cell {
    user-select: none;
    position: relative;
}

.grid-cell.selected {
    z-index: 1;
}

.grid-cell.selection-start {
    z-index: 2;
}

.grid-cell.found-word {
    background-color: #d1e7dd !important;
    color: #0f5132 !important;
}

.grid-cell.current {
    outline: 2px solid #0d6efd;
    outline-offset: -2px;
    z-index: 3;
}

.grid-cell:hover {
    transform: scale(1.05);
    z-index: 4;
    /* 1.4.11 / A2-13 — the boundary the user asked for: visible only while the
       pointer is "actively over the letter". #495057 is 8.18:1 on the page and
       7.76:1 on the #f8f9fa cell fill, so it clears 3:1 with room to spare.
       !important because WordSearchRenderer writes the `border` shorthand
       inline on every cell, and inline would otherwise win. */
    border-color: #495057 !important;
}

.word-search-grid:focus .grid-cell.current {
    outline: 3px solid #0d6efd;
    outline-offset: -3px;
}
/* =========================================================================
   Accessibility — global (WCAG 2.2 AA). Agent 4.
   ========================================================================= */

/* A4-1 (2.4.1 Bypass Blocks) — the skip link. Bootstrap's
   .visually-hidden-focusable un-hides it on focus; this only positions it and
   gives it a visible box once it is focused. */
.skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 2000;
}

.skip-link:focus,
.skip-link:focus-within {
    display: inline-block;
    padding: 0.5rem 1rem;
    background-color: #ffffff;
    color: #0a58ca;              /* 6.94:1 on white */
    font-weight: 600;
    text-decoration: underline;
    outline: 3px solid #0d6efd;
    outline-offset: 2px;
}

/* A4-10 (2.5.8 Target Size Minimum) — 24x24 CSS px floor for the icon-only
   and .btn-sm controls that fell under it. */
.copy-code-btn,
.btn-close,
button.btn-sm.btn-outline-secondary {
    min-width: 24px;
    min-height: 24px;
}

/* A4-11 (1.4.11 Non-text Contrast) — an empty progress track was 1.36:1
   against the card behind it, i.e. invisible at 0%. #6c757d is 4.45:1 on
   #f8f9fa and 4.69:1 on white. */
.progress,
.progress-container .progress {
    border: 1px solid #6c757d;
    box-sizing: border-box;
}

/* A4-6 (supports 2.2.2, 1.4.10, and 2.3.3 AAA) — honour the OS preference.
   This is deliberately the last rule in the file and uses !important so it
   also overrides the component stylesheets injected at runtime by
   ProgressRenderer / TimerRenderer / ScoreRenderer / WordListRenderer, and the
   inline `transition` WordSearchRenderer writes onto every cell. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        animation-delay: 0ms !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0ms !important;
        scroll-behavior: auto !important;
    }

    .grid-cell:hover,
    .grid-cell.word-found:hover,
    .grid-cell.selection-start,
    .score.updated {
        transform: none !important;
    }
}

/* =========================================================================
   Accessibility — the LEGACY grid (index.html + js/wordsearch.js).
   WCAG 2.2 AA. Scoped under #game-grid so the modular renderer in
   demo/kitchen-sink.html, which paints its cells inline, is untouched — and so
   the historical `.grid-cell` / `.grid-cell.selected` declarations above keep
   their declared values for the baseline tests that pin them.
   ========================================================================= */

/* 1.4.3 — the resting letter, stated rather than inherited: 15.43:1.
   1.4.11 — #6c757d is 4.69:1 on the white grid ground, so the cell finally has
   a perceivable boundary (was `0px solid #ddd`, i.e. none at all). */
#game-grid .grid-cell {
    color: #212529;                 /* 14.63:1 on the fill below */
    background-color: #f8f9fa;
    /* A2-13 — ACCEPTED DEVIATION, see docs/ACCESSIBILITY-AUDIT.md.
       A resting cell deliberately has NO visible stroke: the grid reads as
       lettered tiles separated by gap, not as a table of boxes. The border is
       declared transparent (not omitted) purely so :hover can colour it in
       without the layout shifting. Boundary information is carried by the
       hover, focus, selected and found states instead.
       This width MUST stay equal to --ws-cell-border: the run bridge offsets
       compensate for it, and a mismatch puts the white sliver back. It is
       written as a literal rather than var() because the `border` shorthand
       with a var() width is not resolvable by the test environment's CSS
       parser; tests/a11y/grid-band-gap-single-source.test.js asserts the two
       agree on COMPUTED style instead. */
    border: 1px solid transparent;
    box-sizing: border-box;
    border-radius: 8px;
}

/* 1.4.3 — white on #007bff was 3.98:1. #0a58ca is 6.44:1, and 6.44:1 against
   the white page, which also satisfies 1.4.11 for the selection fill. */
#game-grid .grid-cell.selected {
    background-color: #0a58ca;
    color: #ffffff;
}

/* 2.4.7 / 1.4.11 Focus Visible — a dark ring inside a light ring. One of the
   two always clears 3:1 against whatever fill sits under the cell, including
   the darkest found-word colour. z-index outranks .grid-cell:hover (4). */
#game-grid .grid-cell:focus,
#game-grid .grid-cell:focus-visible {
    outline: 3px solid #212529;
    outline-offset: -3px;
    box-shadow: 0 0 0 3px #ffffff;
    z-index: 6;
}

/* 1.4.1 Use of Colour — found words carry an underline as well as a hue, and
   `, found` in their accessible name (js/wordsearch.js#_cellLabel). */
#game-grid .grid-cell[data-found="true"] {
    text-decoration-line: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 2px;
}

/* 1.4.3 / aesthetic restore — the original found-word treatment was the ten
   `.highlight-color-N` pastels washed to `opacity: 90%` over the white grid
   ground, with `color: white !important`. The wash is the look and is kept
   verbatim; the white letter is the only thing that changed, because white on
   these fills ran as low as 1.78:1.

   WHY THE FILL IS REPEATED HERE. `.highlight-color-N` on its own is (0,1,0)
   and loses to `#game-grid .grid-cell` (1,1,0), so the found cell kept the
   resting #f8f9fa fill while these rules still recoloured the letter — grey
   tiles with black underlined letters, and, when the resting fill was white
   and the letter was forced white, invisible letters. The fix is specificity,
   not `!important`: (1,3,0) beats the resting rule outright. Fill and letter
   are declared in the SAME rule so they can never diverge again, which is the
   invariant tests/a11y/legacy-grid-contrast.test.js now enforces on computed
   style.

   `opacity` composites the WHOLE cell over #game-grid's white ground, so the
   ratios below are between the *painted* colours (letter #000 at 90% over
   white = #1a1a1a; fill as noted), not the declared ones. All ten swatches are
   unchanged, and all ten clear 4.5:1 with black. */
#game-grid .grid-cell.word-found-highlight {
    opacity: 0.9;
    color: #000000;
}
#game-grid .grid-cell.word-found-highlight.highlight-color-0 {
    background-color: #FF6B6B;
    color: #000000;
    opacity: 0.9;
} /* 6.96:1 — #FF6B6B paints as #ff7a7a */
#game-grid .grid-cell.word-found-highlight.highlight-color-1 {
    background-color: #4ECDC4;
    color: #000000;
    opacity: 0.9;
} /* 9.69:1 — #4ECDC4 paints as #60d2ca */
#game-grid .grid-cell.word-found-highlight.highlight-color-2 {
    background-color: #45B7D1;
    color: #000000;
    opacity: 0.9;
} /* 8.16:1 — #45B7D1 paints as #58bed6 */
#game-grid .grid-cell.word-found-highlight.highlight-color-3 {
    background-color: #96CEB4;
    color: #000000;
    opacity: 0.9;
} /* 10.51:1 — #96CEB4 paints as #a1d3bc */
#game-grid .grid-cell.word-found-highlight.highlight-color-4 {
    background-color: #A163BA;
    color: #000000;
    opacity: 0.9;
} /* 4.97:1 — #A163BA paints as #aa73c0 (lightened from #9B59B6, see A3-7) */
#game-grid .grid-cell.word-found-highlight.highlight-color-5 {
    background-color: #3498DB;
    color: #000000;
    opacity: 0.9;
} /* 6.29:1 — #3498DB paints as #48a2df */
#game-grid .grid-cell.word-found-highlight.highlight-color-6 {
    background-color: #F1A94E;
    color: #000000;
    opacity: 0.9;
} /* 9.47:1 — #F1A94E paints as #f2b260 */
#game-grid .grid-cell.word-found-highlight.highlight-color-7 {
    background-color: #2ECC71;
    color: #000000;
    opacity: 0.9;
} /* 8.93:1 — #2ECC71 paints as #43d17f */
#game-grid .grid-cell.word-found-highlight.highlight-color-8 {
    background-color: #E74C3C;
    color: #000000;
    opacity: 0.9;
} /* 5.18:1 — #E74C3C paints as #e95e50 */
#game-grid .grid-cell.word-found-highlight.highlight-color-9 {
    background-color: #1ABC9C;
    color: #000000;
    opacity: 0.9;
} /* 7.94:1 — #1ABC9C paints as #31c3a6 */

/* ---------------------------------------------------------------- the band
   A found word should read as ONE highlighter stroke across the run with
   rounded ends, not as N separate coloured boxes. Two parts:

   1. Corners. The run's outer cells keep a pill end; interior cells go square,
      so the run reads as a single shape. `word-run-start` / `word-run-end` are
      applied geometrically (leftmost/topmost first) by js/wordsearch.js and
      js/updated/ui/WordSearchRenderer.js.
   2. The gap. Cells are separate elements with a grid gap between them, so the
      fill alone gives a striped run. Each non-terminal cell paints a ::after
      bridge across the gap toward its successor. The bridge takes
      `background-color: inherit`, so it is the cell's OWN painted fill by
      construction and cannot drift from it — and because it is a child of the
      cell, the 90% wash above composites it identically.

   Diagonals cannot be bridged this way without a diagonal element per gap and
   the joins still read as notches, so a diagonal run degrades to per-cell
   rounded pills. That is deliberate; see docs/ACCESSIBILITY-AUDIT.md.

   Every selector below is gated on `.word-found-highlight`, so the run classes
   are inert once js/multiplayer.js strips that class off a cell. */
/* SINGLE SOURCE OF TRUTH for the band geometry. Nothing below re-states 4px.
   --ws-run-gap    is the gap the grid containers lay out with, AND the gap the
                   bridge spans. #game-grid / .grid-row / .word-search-grid all
                   read it, so the two can no longer drift apart.
   --ws-cell-border is the cell's (transparent) border width. It matters because
                   an absolutely-positioned ::after is offset from its parent's
                   PADDING box, while the parent's fill paints out to its BORDER
                   box. That one-pixel difference is what left a white sliver in
                   every gap: `right: -4px` on a bordered cell stops 3px into a
                   4px gap. Every bridge offset below compensates for it. */
:root {
    --ws-run-gap: 4px;
    --ws-cell-border: 1px;
}

/* The diagonal fallback (and any single-cell find). A diagonal run cannot be
   bridged without a rotated element per gap, so its cells stay separate — but
   at 999px on a square cell they were full CIRCLES, which reads as a different
   visual language sitting next to the straight runs' band. --ws-found-radius is
   a generous rounded square instead: unmistakably the same family as the band's
   pill ends, while still admitting the cells are discrete. */
:root { --ws-found-radius: 14px; }

#game-grid .grid-cell.word-found-highlight {
    border-radius: var(--ws-found-radius);
}

/* Interior of a straight run: square, so neighbours fuse into one shape. */
#game-grid .grid-cell.word-found-highlight.word-run-h,
#game-grid .grid-cell.word-found-highlight.word-run-v {
    border-radius: 0;
}

#game-grid .grid-cell.word-found-highlight.word-run-h.word-run-start {
    border-top-left-radius: 999px;
    border-bottom-left-radius: 999px;
}
#game-grid .grid-cell.word-found-highlight.word-run-h.word-run-end {
    border-top-right-radius: 999px;
    border-bottom-right-radius: 999px;
}
#game-grid .grid-cell.word-found-highlight.word-run-v.word-run-start {
    border-top-left-radius: 999px;
    border-top-right-radius: 999px;
}
#game-grid .grid-cell.word-found-highlight.word-run-v.word-run-end {
    border-bottom-left-radius: 999px;
    border-bottom-right-radius: 999px;
}

/* The gap bridges. `inherit` binds the bridge to the cell's own fill. */
#game-grid .grid-cell.word-found-highlight.word-run-h:not(.word-run-end)::after,
#game-grid .grid-cell.word-found-highlight.word-run-v:not(.word-run-end)::after {
    content: "";
    position: absolute;
    background-color: inherit;
    pointer-events: none;
}
#game-grid .grid-cell.word-found-highlight.word-run-h:not(.word-run-end)::after {
    /* -border on each side so the bridge is as tall as the painted fill, which
       reaches the border box; the ::after box only starts at the padding box. */
    top: calc(-1 * var(--ws-cell-border));
    bottom: calc(-1 * var(--ws-cell-border));
    /* Reach the neighbour's border box: gap + this cell's own border. */
    right: calc(-1 * (var(--ws-run-gap) + var(--ws-cell-border)));
    /* Gap + both borders + 1px of overlap back into the cell, which kills the
       sub-pixel seam on fractional device-pixel ratios. */
    width: calc(var(--ws-run-gap) + 2 * var(--ws-cell-border) + 1px);
}
#game-grid .grid-cell.word-found-highlight.word-run-v:not(.word-run-end)::after {
    left: calc(-1 * var(--ws-cell-border));
    right: calc(-1 * var(--ws-cell-border));
    bottom: calc(-1 * (var(--ws-run-gap) + var(--ws-cell-border)));
    height: calc(var(--ws-run-gap) + 2 * var(--ws-cell-border) + 1px);
}

/* The modular renderer (demo/kitchen-sink.html) lays its rows out with a 2px
   gap, not 4px, and paints its fills inline. It re-points the SAME variable, so
   its container gap and its bridge width still come from one number.
   Corner radii are written inline there because _buildGrid writes
   `border-radius` inline and inline would otherwise win. */
.word-search-grid {
    --ws-run-gap: 2px;
    gap: var(--ws-run-gap);
}

.word-search-grid .grid-cell.word-found-highlight.word-run-h:not(.word-run-end)::after,
.word-search-grid .grid-cell.word-found-highlight.word-run-v:not(.word-run-end)::after {
    content: "";
    position: absolute;
    background-color: inherit;
    pointer-events: none;
}
.word-search-grid .grid-cell.word-found-highlight.word-run-h:not(.word-run-end)::after {
    top: calc(-1 * var(--ws-cell-border));
    bottom: calc(-1 * var(--ws-cell-border));
    right: calc(-1 * (var(--ws-run-gap) + var(--ws-cell-border)));
    width: calc(var(--ws-run-gap) + 2 * var(--ws-cell-border) + 1px);
}
.word-search-grid .grid-cell.word-found-highlight.word-run-v:not(.word-run-end)::after {
    left: calc(-1 * var(--ws-cell-border));
    right: calc(-1 * var(--ws-cell-border));
    bottom: calc(-1 * (var(--ws-run-gap) + var(--ws-cell-border)));
    height: calc(var(--ws-run-gap) + 2 * var(--ws-cell-border) + 1px);
}

/* 1.4.3 — the hint fill had no declared text colour and inherited whatever the
   page set. #212529 on #fff6c3 is 14.12:1. */
#game-grid .grid-cell.hint {
    color: #212529;
    background-color: #fff6c3;
}

/* =========================================================================
   Site navigation — the header nav and the category list in the sidebar.

   Both are new surfaces: the header used to hold one dead button and the
   sidebar one link, so Bootstrap's defaults were never really exercised
   there. They do not clear 1.4.3 at the sizes this nav uses.
   ========================================================================= */

/* 1.4.3 Contrast (Minimum) — Bootstrap's navbar link colour is
   rgba(255,255,255,.55), which flattens to about 2.2:1 on .bg-primary
   (#0d6efd). Full white is 4.50:1 there, and it is already the treatment the
   brand and the Search button use, so the whole bar reads as one thing. */
.navbar-dark .navbar-brand,
.navbar-dark .navbar-nav .nav-link {
    color: #ffffff;
}

.navbar-dark .navbar-nav .nav-link:hover,
.navbar-dark .navbar-nav .nav-link:focus {
    color: #ffffff;
    text-decoration: underline;
}

/* 1.4.1 Use of Colour — the current page is marked by weight and an underline
   as well as by `aria-current`, never by colour on its own. Every link in this
   bar is already full white, so colour could not carry it in any case. */
.navbar-dark .navbar-nav .nav-link[aria-current="page"] {
    font-weight: 700;
    text-decoration: underline;
}

/* The Categories rail. A LIST OF PLACES TO GO, NOT TWELVE CALLS TO ACTION.
   Every entry used to be full-strength link blue, so a column of twelve read
   as twelve things all shouting equally and the eye had nowhere to rest. The
   names are set in the page's own ink and the blue is spent on the one row the
   pointer or the keyboard is actually on — the same restraint the rest of this
   sheet applies to the browse screen, where blue means "this is the way into a
   puzzle" and nothing else.

   EACH ROW CARRIES ITS SIZE. The number comes from js/words.js via
   scripts/generate-catalogue-markup.js, right-aligned so the column of counts
   can be read straight down, and it is what makes the list worth reading
   before clicking: "Places 10" is a different promise from "Practice 2".

   1.4.3 — #212529 on the card's white background is 15.4:1, and 13.7:1 on the
   #eef2f7 the hovered row is tinted with. The count's #495057 is 8.2:1 on
   white and 7.4:1 on its #f1f3f5 pill. The hover blue is 5.7:1 on that tint.
   1.4.1 — hover and focus are carried by a background and an underline as well
   as by the colour, so neither state is colour alone.
   2.5.8 — the row is 36 CSS px of target, over the 24px floor, and the whole
   row is the target rather than the word in it. */
.category-nav .nav-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.category-nav .nav-link {
    /* The count is inside the anchor now, so the row IS the link: the
       highlight runs the full width instead of stopping short of a number it
       did not contain, and the number is no longer dead space between two
       targets. */
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    gap: 0.5rem;
    min-height: 36px;
    margin-left: -0.45rem;
    padding: 0.25rem 0.45rem;
    color: #212529;
    font-size: 0.9375rem;
    font-weight: 500;
    border-radius: 0.375rem;
}

.category-nav__title {
    flex: 1 1 auto;
}

.category-nav .nav-link:hover,
.category-nav .nav-link:focus {
    /* Deliberately NOT the blue link colour. Every row here is a link, so
       turning one blue on hover says nothing the highlight has not already
       said, and it read as though the row had become a different kind of
       thing from its neighbours. The title still underlines, so the cue does
       not rest on colour alone (1.4.1). */
    color: #212529;
    background-color: #eef2f7;
    text-decoration: none;
}

.category-nav .nav-link:hover .category-nav__title,
.category-nav .nav-link:focus .category-nav__title {
    text-decoration: underline;
}

.category-nav__count {
    flex: 0 0 auto;
    min-width: 1.75rem;
    padding: 0.05rem 0.4rem;
    color: #495057;
    font-size: 0.75rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: center;
    background-color: #f1f3f5;
    border-radius: 999px;
}

/* On a highlighted row the pill's own grey would sit on grey and disappear;
   white keeps its edge and keeps the count legible (#495057 on white is
   8.18:1). */
.category-nav .nav-link:hover .category-nav__count,
.category-nav .nav-link:focus .category-nav__count {
    background-color: #fff;
}

/* 1.4.3 Contrast (Minimum) — Bootstrap's <code> pink (#d63384) lands on exactly
   4.50:1 against white, which is the AA threshold with nothing to spare, at
   0.875em. The room-code examples on the site pages are the only <code> in the
   project, and they are the thing a reader has to copy accurately, so they get
   a darker ink: #a61e5a is 7.1:1 on white. */
:root {
    --bs-code-color: #a61e5a;
}

/* 1.4.3 Contrast (Minimum) — Bootstrap's link blue (#0d6efd) is 3.7:1 on white
   and 3.5:1 on the .bg-light page background, so prose links failed AA at body
   size. The site pages are the first surface with links inside running text,
   which is what surfaced it. #0a58ca is 6.4:1 on white and 6.1:1 on #f8f9fa.

   Set through Bootstrap's own variable rather than an `a { }` override, so
   every component that derives its link colour moves with it and nothing has
   to be chased individually. Footer links are unaffected: .text-light sets
   its own colour with !important. */
:root {
    --bs-link-color-rgb: 10, 88, 202;
    --bs-link-hover-color-rgb: 8, 66, 152;
}

/* ------------------------------------------------------------- promo slots
   Two fixed boxes carrying links to the site owner's OWN sites (index.html).
   They are NOT labelled "Advertisement" or "Sponsored", because nothing in
   them is a paid placement.

   THE SPACE IS RESERVED. Each slot is built at an IAB standard unit size, so a
   real ad unit could be dropped in later without a redesign, and so nothing on
   the page moves when the contents of a slot change:

     .promo-slot--leaderboard   728 x 90   (leaderboard)
     .promo-slot--skyscraper    160 x 600  (wide skyscraper)

   `min-height`, never `height`: 1.4.12 Text Spacing needs a text-bearing box
   to be able to grow when the reader increases line height, and a hard height
   would clip it. `max-width: 100%` keeps the 728px unit inside the column on a
   narrow screen; the reserved height, which is what prevents layout shift, is
   unaffected either way.

   THEY ROTATE (js/promoSlots.js), AND THERE IS NO ANIMATION HERE ON PURPOSE.
   The swap is instantaneous: a cross-fade would be MORE motion, not less, for
   exactly the readers most likely to mind it, and it would need chasing
   through the prefers-reduced-motion guard at the top of this file. Because
   the box is a reserved fixed size, swapping the text and the address inside
   it cannot move anything on the page.

   Auto-updating content needs a visible mechanism to stop it under WCAG 2.2
   2.2.2, and pausing on hover does not satisfy it. That mechanism is
   .promo-rotation-toggle below. */
.promo-slot {
    position: relative;   /* anchors the rotation control in the corner */
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    max-width: 100%;
    margin-inline: auto;
    padding: 0.75rem;
    text-align: center;
    background-color: #fff;
    border: 1px solid #dee2e6;
    border-radius: 0.375rem;
}

.promo-slot--leaderboard {
    width: 728px;
    min-height: 90px;
}

.promo-slot--skyscraper {
    width: 160px;
    min-height: 600px;
    flex-direction: column;
}

/* 1.4.3 — #495057 is 7.0:1 on the slot's white background. The heading is the
   only thing in the slot that is not a link, and it has to stay readable
   without competing with the site name next to it. */
.promo-slot__heading {
    color: #495057;
    font-weight: 600;
}

.promo-slot__link {
    font-weight: 600;
}

/* The external-link glyph sat flush against the last letter, and the link's
   underline ran straight under it, so the two read as one word ending in a
   box. A small gap separates them; `inline-block` keeps the icon out of the
   underline it would otherwise share with the text. `em` so the gap tracks
   the font size wherever a slot is rendered. */
.promo-slot__link .bi {
    display: inline-block;
    margin-left: 0.35em;
    text-decoration: none;
    vertical-align: -0.05em;
}

/* The control that stops the rotation. One beside each slot, both driving the
   same preference, and it is real markup rather than something script injects
   — it has to be there and focusable before anything else has loaded.

   SMALL, BUT NOT HIDDEN. 2.2.2's mechanism cannot be hover-only, so nothing
   here is display:none, opacity:0 or off-screen until hovered. It is simply a
   quiet chip that does not compete with the board.

   1.4.3 — #495057 is 7.0:1 on white and 6.6:1 on the .bg-light page. Bootstrap's
   own .btn-outline-secondary grey (#6c757d) is 4.47:1 on #f8f9fa, which is
   under the 4.5 threshold at this size, which is why this is not that button.
   1.4.11 — the #6c757d border is 4.7:1 on white, over the 3:1 a control edge
   needs.
   2.5.8 — the 24px minimum box is the target-size floor. */
/* QUIET AT REST, OBVIOUS WHEN REACHED. The owner asked for this to be
   "mostly unnoticeable visually" — it is a housekeeping control beside two
   promo boxes, not something that should compete with the game.

   What "quiet" is NOT allowed to mean, because 2.2.2 requires this control to
   exist and be usable:
     - not hover-only. It is in the layout at all times; a control that
       appears on hover does not exist for touch or keyboard.
     - not invisible. #6a7178 on #f8f9fa is 4.69:1, past the 4.5:1 its label
       needs; the #868e96 edge is 3.15:1, past the 3:1 a control boundary
       needs under 1.4.11. Both measured, not estimated — the first colour
       tried here was #6c757d, which LOOKS like a safe grey and computes to
       4.45:1, just under the bar.
     - not small. 24px minimum box, the 2.5.8 target-size floor.
   Subtle here means low-contrast-but-legible, never unreadable, and the
   floor is the standard rather than taste. See DECISIONS.md. */
.promo-rotation-toggle {
    position: absolute;
    top: 0.25rem;
    right: 0.25rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    min-height: 24px;
    padding: 0;
    color: #6a7178;
    background-color: transparent;
    border: 1px solid transparent;
    border-radius: 0.25rem;
    cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease,
                background-color 0.15s ease;
}

/* ICON-ONLY, SO THE ICON IS THE CONTROL. Its label is still in the markup for
   a screen reader (.visually-hidden) — removing it would leave an unnamed
   button. Because the glyph is now the only thing identifying this control,
   1.4.11 applies to the GLYPH rather than to a border: #6a7178 on the slot's
   white ground is 4.99:1, well past the 3:1 a non-text control needs. The
   resting border is transparent on purpose; the corner position and the icon
   carry it, and hover/focus paint the edge in. */
.promo-rotation-toggle__icon {
    font-size: 1rem;
    line-height: 1;
}

/* Motion is decoration here; a visitor who asked for less should not get it. */
@media (prefers-reduced-motion: reduce) {
    .promo-rotation-toggle {
        transition: none;
    }
}

.promo-rotation-toggle:hover,
.promo-rotation-toggle:focus-visible {
    color: #212529;
    border-color: #495057;
}



/* =========================================================================
   THE BROWSE SCREEN (puzzles.html)

   All of this styles markup that does not exist in the file — js/words.js is
   read at load and js/catalogBrowser.js draws the page from it — so every rule
   here has to hold for a catalogue of four games and for one of four hundred.
   That constraint is what the layout is for:

     .catalog-today   one band across the top, dark, with the day's puzzle in
                      it. A fixed amount of space whatever the catalogue size,
                      and the only place on the page with a strong focal point,
                      so there is somewhere obvious to look first.
     .catalog-wall    the categories as tiles in an auto-filling grid. Four
                      categories become one row rather than four screens; forty
                      become ten rows of the same tidy tile. `align-items:
                      start` so a tall tile never stretches its neighbours into
                      whitespace.
     .puzzle          a game as a ROW, not a card: name, size, four real words.
                      Cards look generous at four games and are unusable at a
                      hundred; a bare list of names scales but says nothing
                      about the puzzle. This is the middle, and it is the same
                      height at both sizes.

   COLOUR. Two families and nothing else: the site's existing #0a58ca blue for
   anything you can press, and a near-black navy for the Today band. Ratios are
   stated per rule and none of them is close to the line.

   MOTION. Nothing here animates or transforms. The reduced-motion guard higher
   up this file neutralises transitions globally, and a hover effect built from
   colour rather than movement needs nothing from it. */

/* ------------------------------------------------------------- the masthead
   ONE LOUD THING, then quiet. The page title used to be set at Bootstrap's
   default h1 and sat between a lead paragraph and a dark band that was twice
   its weight, so the page's own name read as a caption to the band under it.
   It is now the largest thing on the page by a clear margin, tracked in tight
   the way a display size wants to be, and everything below it steps down in
   one move rather than in three overlapping ones. */
.catalog-intro {
    max-width: 44rem;
    margin-bottom: 2.25rem;
}

.catalog-intro__title {
    margin-bottom: 0.6rem;
    font-size: clamp(2rem, 4.5vw, 2.75rem);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.03em;
}

/* 1.4.3 — #495057 is 8.2:1 on white and 7.8:1 on the .bg-light page. */
.catalog-intro__lead {
    margin-bottom: 0;
    color: #495057;
    font-size: 1.0625rem;
    line-height: 1.6;
}

/* Replaced, not hidden, by js/catalogBrowser.js — so if the script never runs
   this is what stands here, and it says so plainly. */
.catalog-fallback {
    padding: 1rem 1.25rem;
    background-color: #fff;
    border: 1px solid #6c757d;
    border-radius: 0.5rem;
}

/* -------------------------------------------------------------- letter chips
   A game's own words, shown the way they look in the grid: uppercase, spaced
   out, one to a tile. This is the page's signature and it is doing a job — a
   sample rendered as body copy reads as a description of the puzzle, and
   rendered as tiles it reads as the thing you will be hunting for.

   1.4.3 — #212529 on #eef2f7 is 13.7:1. The tiles are small text, so the ink is
   the darkest on the page rather than a grey.

   NO OUTLINE ON THE SMALL ONES, and this is the single biggest change on the
   page. Four samples on each of ninety-two rows is three hundred and seventy
   chips, and each one used to carry a 1px #ccd5e0 edge — three hundred and
   seventy hard rectangles competing with ninety-two puzzle names for the same
   eye. The fill alone separates a word from the tile it sits on perfectly
   well; the edge was only ever making them louder. The large variant on the
   dark band keeps its edge, because a translucent fill on navy has nothing
   else to hold its shape. */
.letter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.2rem 0.25rem;
    margin: 0 0 0.55rem;
    padding: 0;
    list-style: none;
}

.letter-chips__item {
    padding: 0.15rem 0.4rem;
    color: #212529;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    background-color: #eef2f7;
    border-radius: 0.25rem;
}

.letter-chips--large .letter-chips__item {
    padding: 0.3rem 0.55rem;
    font-size: 0.9rem;
    color: #f8fafc;
    background-color: rgba(255, 255, 255, 0.08);
    border-color: rgba(255, 255, 255, 0.35);
}

/* ------------------------------------------------------------- the Today band
   Dark on purpose. It is the one thing on the page that changes by itself, so
   it has to be legible as a different KIND of thing from the catalogue below
   it, and inverting is the cheapest way to say that without inventing a second
   accent colour.

   1.4.3 — on #152238: white is 15.9:1, #cbd5e1 is 10.7:1, #ffc857 is 10.4:1. */
.catalog-today {
    margin-bottom: 2.25rem;
    padding: 1.75rem 1.75rem 2rem;
    color: #fff;
    background-color: #152238;
    border-radius: 1rem;
}

/* The date line is a rule as well as a label: it separates the band's own
   heading from the two things underneath it without a second colour. */
.catalog-today__heading {
    margin-bottom: 1.4rem;
    padding-bottom: 0.9rem;
    color: #cbd5e1;
    font-size: 0.8125rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

.catalog-today__date {
    color: #fff;
}

.catalog-today__grid {
    display: grid;
    gap: 1.5rem;
    grid-template-columns: 1fr;
}

@media (min-width: 62em) {
    .catalog-today__grid {
        /* Closer to even than it was. At 3:2 the day's puzzle held sixty per
           cent of a very wide band for four short lines of text, so most of
           what the split bought was empty navy, while the shelf beside it —
           five rows, each with a name at one end and its size at the other —
           was the column actually short of room. */
        grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
        gap: 2rem;
        align-items: stretch;
    }
}

/* The day's puzzle is centred against the shelf beside it rather than pinned to
   the top of the band. Top-aligned, the two columns were different heights and
   the difference read as a hole in the middle of the page's focal point. */
.daily-pick {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
}

.daily-pick__eyebrow {
    margin-bottom: 0.35rem;
    color: #ffc857;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.14em;
}

.daily-pick__title {
    margin-bottom: 0.35rem;
    font-size: clamp(1.75rem, 4vw, 2.5rem);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.02em;
}

.daily-pick__meta {
    margin-bottom: 0.9rem;
    color: #cbd5e1;
    font-size: 0.9375rem;
}

/* 1.4.3 — #cbd5e1 is 10.7:1 on the band. Capped in width because a measure of
   forty characters is unreadable and one of a hundred and forty is worse. */
.daily-pick__description {
    max-width: 42rem;
    margin-bottom: 0.9rem;
    color: #cbd5e1;
    font-size: 0.9375rem;
    line-height: 1.55;
}

/* 1.4.3 — white on Bootstrap's #0d6efd .btn-primary is 3.7:1, which is under AA
   for a button label. The site's darker #0a58ca is 6.4:1 and is already the
   link colour everywhere else. */
.daily-pick__play {
    --bs-btn-bg: #0a58ca;
    --bs-btn-border-color: #0a58ca;
    --bs-btn-hover-bg: #084298;
    --bs-btn-hover-border-color: #084298;
    --bs-btn-active-bg: #084298;
    --bs-btn-active-border-color: #084298;
    font-weight: 600;
}

/* The seasonal shelf. It sits beside the daily pick rather than under it so the
   band stays one screen-band tall however many entries it carries. */
.in-season {
    padding: 1.15rem 1.25rem 1.25rem;
    background-color: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 0.75rem;
}

.in-season__title {
    margin-bottom: 0.25rem;
    font-size: 1.0625rem;
    font-weight: 700;
}

.in-season__blurb {
    margin-bottom: 0.75rem;
    color: #cbd5e1;
    font-size: 0.8125rem;
}

.in-season__list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.in-season__item + .in-season__item {
    border-top: 1px solid rgba(255, 255, 255, 0.14);
}

/* 2.5.8 — 44px of row is well over the 24px target-size floor, and the whole
   row is the target rather than the words in it. */
.in-season__link {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    min-height: 44px;
    padding: 0.5rem 0.25rem;
    color: #fff;
    text-decoration: none;
}

.in-season__link:hover,
.in-season__link:focus-visible {
    color: #ffc857;
    text-decoration: underline;
}

.in-season__name {
    font-weight: 600;
}

.in-season__meta {
    color: #cbd5e1;
    font-size: 0.8125rem;
}

/* ----------------------------------------------------------------- the filter
   Real radio buttons in a real fieldset (js/catalogBrowser.js). Bootstrap's
   .btn-check hides the input visually and paints the label, so everything below
   is about making the LABEL show the input's state — including its focus, which
   is why :focus-visible on the input styles the label beside it. */
.catalog-filter {
    margin-bottom: 0.7rem;
}

.catalog-filter__set {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    margin: 0;
    padding: 0;
    border: 0;
}

.catalog-filter__legend {
    width: auto;
    margin: 0 0.5rem 0 0;
    padding: 0;
    color: #495057;
    font-size: 0.8125rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
}

/* 1.4.11 — the #6c757d edge is 4.7:1 on white, over the 3:1 a control boundary
   needs. 1.4.3 — #212529 on white is 15.4:1, and white on #0a58ca is 6.4:1 when
   the chip is the chosen one. 2.5.8 — 36px clears the 24px floor. */
.catalog-filter__chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 34px;
    padding: 0.25rem 0.65rem;
    color: #212529;
    font-size: 0.8125rem;
    font-weight: 600;
    background-color: #fff;
    border: 1px solid #6c757d;
    border-radius: 999px;
}

.catalog-filter__chip:hover {
    border-color: #0a58ca;
}

.catalog-filter__input:checked + .catalog-filter__chip {
    color: #fff;
    background-color: #0a58ca;
    border-color: #0a58ca;
}

/* 2.4.7 — the input itself is visually hidden by .btn-check, so without this
   there is no focus indicator anywhere for a keyboard user. */
.catalog-filter__input:focus-visible + .catalog-filter__chip {
    outline: 3px solid #0a58ca;
    outline-offset: 2px;
}

.catalog-filter__count {
    padding: 0 0.4rem;
    color: #212529;
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    background-color: #e9ecef;
    border-radius: 999px;
}

.catalog-filter__input:checked + .catalog-filter__chip .catalog-filter__count {
    color: #0a58ca;
    background-color: #fff;
}

/* The count the filter announces. It is a live region, so it is read out when
   it changes; it is visible for everyone else.

   IT ALSO CLOSES THE HEADER. The filter chips used to float between the band
   and the wall with nothing to sit on, so the eye read them as part of the
   first row of tiles. The rule underneath this line turns the filter and its
   result into one control strip with a bottom edge, and gives the wall a top
   edge to start from — the cheapest way to say "this is the toolbar, that is
   the catalogue" without a box around either. */
.catalog-status {
    margin: 0 0 1.5rem;
    padding-bottom: 0.9rem;
    color: #495057;
    font-size: 0.8125rem;
    border-bottom: 1px solid #dee2e6;
}

/* ------------------------------------------------------------------- the wall
   TILE ACCENTS. Twelve identical white boxes are hard to keep your place in
   while scrolling, so each tile carries one of six accents on its top rule and
   on its count. The accent comes from the category's POSITION in the catalogue
   (js/catalogBrowser.js), so a new category gets one without anyone picking it.

   ONE USE OF COLOUR PER TILE, AND IT IS SHORT. The accent used to paint a
   full-width 3px rule across the top of the tile AND the uppercase count under
   the heading, which put twelve saturated bars and twelve pieces of coloured
   text on one screen: the wall read as a chart legend rather than as a
   catalogue, and the loudest thing in every tile was a decoration. The accent
   is now one 36px mark above the heading and nothing else — the count is set
   in the same grey as the rest of the tile's metadata. It still does the job
   the mark is there for, which is keeping your place while scrolling twelve
   near-identical tiles, and it no longer competes with the puzzle names.

   1.4.1 — it is never the only signal: every tile is headed with the category's
   own name, and the filter chip beside it carries the same name. Nothing is
   findable only by remembering a colour.
   1.4.3 / 1.4.11 — the six are 6.4, 6.5, 6.5, 8.8, 6.3 and 6.6 to one against
   white. The mark is now decoration rather than text, so 3:1 would do; they
   are held to the text threshold anyway so that this palette stays safe if a
   later change puts words back in it. */
:root {
    --ws-accent-0: #0a58ca;
    --ws-accent-1: #146c43;
    --ws-accent-2: #b02a37;
    --ws-accent-3: #59359a;
    --ws-accent-4: #8a5300;
    --ws-accent-5: #0f6674;
}

.catalog-group[data-accent="0"] { --ws-accent: var(--ws-accent-0); }
.catalog-group[data-accent="1"] { --ws-accent: var(--ws-accent-1); }
.catalog-group[data-accent="2"] { --ws-accent: var(--ws-accent-2); }
.catalog-group[data-accent="3"] { --ws-accent: var(--ws-accent-3); }
.catalog-group[data-accent="4"] { --ws-accent: var(--ws-accent-4); }
.catalog-group[data-accent="5"] { --ws-accent: var(--ws-accent-5); }

/* TILES IN A ROW SHARE A BOTTOM EDGE. `align-items: start` let every tile find
   its own height, which sounds tidy and is not: the cap means each tile shows
   the same four games, so the heights differed only by how a category's words
   happened to wrap, and the wall came out with a ragged edge under every row
   for no reason a reader could see. Stretching them and pushing the overflow
   control to the bottom (see .catalog-group__more) puts those controls on one
   line per row and gives the grid the horizontal rhythm it was missing. */
.catalog-wall {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
    align-items: stretch;
    gap: 1.25rem;
    margin-bottom: 3rem;
}

/* THE COMMITTED LAYER GETS THE SAME WALL. puzzles.html ships the category
   tiles as markup (scripts/generate-catalogue-markup.js) and
   js/catalogBrowser.js replaces them with the browse screen, .catalog-wall and
   all. Until it does — and for a reader with no scripting, for whom it never
   does — the tiles are loose children of #catalog, and without this they stack
   full-width in one long column. `:has()` is how the sheet tells the two apart;
   a browser without it simply gets that column, which is the old behaviour and
   is perfectly readable. */
.catalog:not(:has(.catalog-wall)) {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
    align-items: stretch;
    gap: 1.25rem;
    margin-bottom: 3rem;
}

/* THE TILE IS A FOUR-ROW GRID, and it is a grid rather than a stack so that the
   category's name and its size can share a line without either of them being
   wrapped in a div the generator would have to emit. The rows are:

     1  the accent mark, spanning
     2  the heading, and the count right-aligned beside it
     3  the games — 1fr, so it takes whatever slack the tile has
     4  the overflow control, which therefore sits on the tile's bottom edge

   Every child is placed explicitly. Auto-placement would put the count on its
   own line the moment a heading wrapped to two, which is exactly the layout
   this replaces.

   A HAIRLINE PLUS A SHADOW, NOT A HEAVIER BORDER. A white tile on the page's
   near-white #f8f9fa separated by a 1px #dee2e6 edge is about 1.2:1 against
   its own background, which is why twelve of them read as a faint mesh rather
   than as twelve objects. The shadow is what lifts them; the edge can then
   stay light instead of being darkened into a cage. Nothing here moves — the
   shadow is static, so the reduced-motion guard has nothing to switch off. */
.catalog-group {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto auto 1fr auto;
    column-gap: 0.75rem;
    padding: 1.15rem 1.25rem 1.25rem;
    background-color: #fff;
    border: 1px solid #e4e8ed;
    border-radius: 0.75rem;
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 4px 10px rgba(16, 24, 40, 0.04);
}

.catalog-group::before {
    content: "";
    grid-column: 1 / -1;
    grid-row: 1;
    width: 2.25rem;
    height: 3px;
    margin-bottom: 0.85rem;
    background-color: var(--ws-accent, #0a58ca);
    border-radius: 2px;
}

.catalog-group__title {
    grid-column: 1;
    grid-row: 2;
    margin-bottom: 0;
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -0.015em;
}

/* 1.4.3 — #495057 is 8.2:1 on the tile and 7.4:1 on the #f1f3f5 pill. It is
   the same grey as the size line inside every row below it, which is the
   point: the count is metadata about the category, not a label for it. */
.catalog-group__count {
    grid-column: 2;
    grid-row: 2;
    align-self: center;
    min-width: 1.75rem;
    margin-bottom: 0;
    padding: 0.1rem 0.45rem;
    color: #495057;
    font-size: 0.75rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: center;
    background-color: #f1f3f5;
    border-radius: 999px;
}

.catalog-group__list {
    grid-column: 1 / -1;
    grid-row: 3;
    margin: 0.85rem 0 0;
    padding: 0;
    list-style: none;
}

/* A game. The rule between rows carries the grouping, so nothing needs a box of
   its own — twelve boxes inside a box is where a dense page turns to noise. */
.puzzle {
    padding: 0.8rem 0;
}

/* The tile's own heading is the row above it, so the first game does not need
   a second gap of its own. */
.puzzle:first-child {
    padding-top: 0;
}

.puzzle + .puzzle {
    border-top: 1px solid #edf0f3;
}

.puzzle__title {
    margin-bottom: 0.15rem;
    font-size: 0.9375rem;
    font-weight: 700;
    letter-spacing: -0.005em;
}

/* 1.4.3 — #0a58ca is 6.4:1 on the tile's white background. The name is the
   link, so a screen reader's link list reads as a list of puzzles rather than a
   column of identical "Play" buttons. */
.puzzle__link {
    color: #0a58ca;
    text-decoration: none;
}

.puzzle__link:hover,
.puzzle__link:focus-visible {
    text-decoration: underline;
}

.puzzle__meta {
    margin-bottom: 0.45rem;
    color: #495057;
    font-size: 0.8125rem;
}

/* A game's own note about itself. Rendered inside the disclosure by
   js/catalogBrowser.js and in the row by the committed markup, so it is styled
   to read the same in either place. 1.4.3 — #495057 is 8.2:1 on the tile. */
.puzzle__description {
    margin: 0 0 0.4rem;
    color: #495057;
    font-size: 0.8125rem;
    line-height: 1.5;
}

.puzzle__words {
    font-size: 0.75rem;
}

/* Deliberately QUIETER than the game's own name above it, and quieter now than
   it was. In a wall of ninety rows the blue has to mean one thing — "this is
   the way into a puzzle" — so the second control in the row, which only opens
   a list in place, is grey. It also appears ninety-two times, once per row and
   always saying the same kind of thing, so it is set a step smaller than the
   size line above it: at the size the size line uses, "All 12 words" repeated
   ninety-two times was reading as a column of its own.
   1.4.3 — #495057 is 8.2:1 on the tile's white background, at any size.
   2.5.8 — a disclosure is a control; 24px is the floor, and it is kept. */
.puzzle__words-toggle {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    color: #495057;
    cursor: pointer;
}

.puzzle__words-toggle:hover,
.puzzle__words[open] > .puzzle__words-toggle {
    color: #212529;
}

.puzzle__word-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin: 0.4rem 0 0;
    padding: 0;
    list-style: none;
}

.puzzle__word-list .word-list__item {
    padding: 0.1rem 0.35rem;
    color: #212529;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    background-color: #f1f3f5;
    border-radius: 0.2rem;
}

/* The overflow control — "+209 More" made real. It opens the rest of the
   category in place and says how many are behind it. 2.5.8 — 32px tall.
   1.4.11 — the #6c757d edge is 4.7:1 on white.

   IT SITS ON THE TILE'S BOTTOM EDGE. Row 4 of the tile grid, under a row that
   takes the slack, so every "show N more" in a row of tiles lands on the same
   line instead of wherever that category's words happened to stop wrapping. */
.catalog-group__more {
    grid-column: 1 / -1;
    grid-row: 4;
    justify-self: start;
    display: inline-flex;
    align-items: center;
    min-height: 32px;
    margin-top: 0.9rem;
    padding: 0.25rem 0.7rem;
    color: #0a58ca;
    font-size: 0.8125rem;
    font-weight: 600;
    background-color: #fff;
    border: 1px solid #6c757d;
    border-radius: 999px;
    cursor: pointer;
}

.catalog-group__more:hover,
.catalog-group__more:focus-visible {
    border-color: #0a58ca;
}

/* NARROWED TO ONE CATEGORY, the wall is not a wall any more — it is one tile,
   and leaving it in the first column of a four-column grid puts a screenful of
   nothing beside it. So the tile takes the width and its games become the grid
   instead: ten rows in three columns rather than ten rows in one.

   The row rule moves with them. `.puzzle + .puzzle` draws a line between
   vertical neighbours, which is the wrong line once the rows are laid out in
   columns, so in this mode every row carries its own. */
.catalog-wall[data-filtered]:not([data-filtered="all"]) {
    display: block;
}

/* THE COUNT COMES BACK TO THE HEADING. Right-aligning it works while a tile is
   a tile: the eye reads "Places" and finds "10 puzzles" a few centimetres
   away on the same line. Across the full width of the page it is half a metre
   away and reads as an unrelated label stuck to the corner, so here the title
   column shrinks to the title and the count sits directly after it. */
.catalog-wall[data-filtered]:not([data-filtered="all"]) .catalog-group {
    grid-template-columns: auto minmax(0, 1fr);
}

.catalog-wall[data-filtered]:not([data-filtered="all"]) .catalog-group__count {
    justify-self: start;
}

.catalog-wall[data-filtered]:not([data-filtered="all"]) .catalog-group__list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
    align-items: start;
    column-gap: 2rem;
}

/* Every row carries its own rule here, including the first one in each column
   — so the top padding the first row gives up in a single-column tile has to
   come back, or the first row of each column would sit tight against its rule
   while the rest did not. */
.catalog-wall[data-filtered]:not([data-filtered="all"]) .puzzle,
.catalog-wall[data-filtered]:not([data-filtered="all"]) .puzzle:first-child {
    padding-top: 0.8rem;
    border-top: 1px solid #edf0f3;
}

/* ------------------------------------------------------------------- the notes
   The two explanatory panels under the wall are Bootstrap cards, and left
   alone they arrive wearing Bootstrap's grey .card-header bar and 0.375rem
   radius — a third kind of box on a page that had settled on one. They are
   restyled here rather than reclassed in the markup, so the page keeps using
   the component it already uses, and they sit side by side on a wide screen
   instead of stacking into a narrow column under a full-width wall. */
.catalog-notes {
    max-width: 46rem;
}

@media (min-width: 62em) {
    .catalog-notes {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        align-items: start;
        gap: 1.25rem;
        max-width: none;
    }

    /* The section heading and the trailing paragraph both belong to the whole
       section, not to one column of it. Without this the heading takes the
       first cell as an ordinary grid item, which pushes the first card into
       column two and leaves a column-height hole under the heading. */
    .catalog-notes > p,
    .catalog-notes > .catalog-notes__heading {
        grid-column: 1 / -1;
    }
}

.catalog-notes .card {
    margin-bottom: 1.25rem;
    border: 1px solid #e4e8ed;
    border-radius: 0.75rem;
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 4px 10px rgba(16, 24, 40, 0.04);
}

@media (min-width: 62em) {
    .catalog-notes .card {
        margin-bottom: 0;
    }
}

.catalog-notes .card-header {
    padding: 1.15rem 1.25rem 0;
    background-color: transparent;
    border-bottom: 0;
}

.catalog-notes .card-body {
    padding: 0.65rem 1.25rem 1.25rem;
}

.catalog-notes .card-title {
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -0.015em;
}

/* The Categories sidebar on index.html is generated too (js/catalogMenu.js), so
   its length is the number of categories that exist. A cap with a scroll keeps
   it from pushing the board down the page once there are twenty of them. */
#category-nav-list {
    max-height: 60vh;
    overflow-y: auto;
}


/* The home page's opening block and its five lines. Deliberately plain: this
   is a landing page, not a brochure, and the strongest thing on the screen
   should stay the puzzle you can play. */
.catalog-offer {
    margin: 1.5rem 0 2rem;
}

.catalog-offer__heading {
    margin-bottom: 0.5rem;
    color: #212529;
}

.catalog-offer__list {
    margin: 0;
    padding-left: 1.15rem;
    color: #343a40;          /* 10.91:1 on the page ground */
    line-height: 1.7;
}

.catalog-offer__list li + li {
    margin-top: 0.25rem;
}

/* One line under the puzzle's name. #cbd5e1 on the band's own #152238 is
   10.73:1 — measured against the colour the band actually paints, not the one
   it looked like. */
.daily-pick__shared {
    margin: 0.15rem 0 0.35rem;
    color: #cbd5e1;
    font-size: 0.9375rem;
}

.catalog-notes__heading {
    margin: 2rem 0 0.75rem;
    color: #212529;
}
