/* =========================================================================
   3D PROTAGON PUZZLES — SITE CHROME
   -------------------------------------------------------------------------
   Header, navigation, user menu, breadcrumbs, progress strips, template action
   buttons, toasts, streak banners and the favourites control.

   This was previously emitted as a ~30 KB <style> block from
   Navigation::injectNavStyles() on EVERY page load — it could not be cached, it
   inflated every HTML response (the homepage was 63 KB, of which 31 KB was this
   block), and it meant editing CSS inside a PHP heredoc. It is now a normal
   stylesheet, fetched once and cached like every other asset.

   It loads from Navigation::criticalStylesheets(), before the page stylesheets.
   That is safe: every rule here is either more specific than its counterpart in
   the page sheets (e.g. `.site-header .theme-toggle` vs `.theme-toggle`) or
   carries !important, so the cascade result is unchanged.

   Colours come from the design system (public/css/tokens.css) via the --nav-*
   tokens, so switching theme or rebranding does not require touching this file.
   ========================================================================= */

/* Chrome colours come from the design system (public/css/tokens.css), which
   defines --nav-bg / --nav-border / --nav-text / --nav-muted / --nav-hover-bg /
   --nav-active-bg / --nav-active-text / --nav-highlight per theme. This block
   used to redeclare all of them, and to repeat a second hard-coded burgundy
   palette under html[data-theme='light'], which meant changing the brand colour
   required editing PHP. Anything theme-dependent now lives in the stylesheet. */
.site-header {
    /* Was `position: fixed`, which took the header out of flow and forced a
       DOMContentLoaded script to add body padding-top to stop content hiding
       underneath it. That script ran AFTER the first paint, so the entire page
       (body > .page) jumped down by the height of the header on every load —
       measured as CLS 0.2718, the single largest layout shift on the site.
       Sticky keeps the header in normal flow (so its space is reserved during
       the initial layout, with no JS and no shift) while behaving identically
       once the page is scrolled. */
    position: sticky;
    top: 0;
    z-index: 9999;
    width: 100%;
    -webkit-backdrop-filter: blur(10px) saturate(130%);
    backdrop-filter: blur(10px) saturate(130%);
    background: var(--nav-bg);
    border-bottom: 1px solid var(--nav-border);
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
}
.site-header__bar {
    max-width: 1200px;
    margin: 0 auto;
    padding: 12px 24px 6px;
    display: flex;
    align-items: center;
    gap: 18px;
    justify-content: space-between;
}
.site-header__brand .brand-mark {
    color: var(--nav-text);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 1.18rem;
    letter-spacing: 0;
    text-decoration: none;
    background-image: var(--nav-highlight);
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}
.site-header__nav {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    align-items: center;
    /* Stated explicitly rather than inherited from a bare `nav {}` rule in
       style.css: this nav is ~40px wider than its links, so the centring is
       load-bearing. That rule has been deleted — see the note there. */
    justify-content: center;
}
.nav-links-group {
    display: flex;
    /* 18px matches .site-header__bar's own gap, and is the value the header's
       rhythm implies. It was 14px, which was NOT the spacing anyone had been
       looking at: the legacy bare `nav a { margin: 0 20px }` in style.css added
       40px per link, so adjacent links actually sat 54px apart. Deleting that rule
       (it was painting .legal-links white on a cream background) left 14px and
       visibly tightened the header, so the gap is now stated deliberately here.
       Note the links live in this group, NOT directly in .site-header__nav, so
       the nav's own `gap` does nothing — it has a single child. */
    gap: 18px;
    flex-wrap: wrap;
    align-items: center;
}
.site-header__nav .nav-link {
    color: var(--nav-text);
    text-decoration: none;
    font-weight: 700;
    padding: 8px 12px;
    border-radius: 10px;
    border: 1px solid transparent;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.site-header__nav .nav-link:hover {
    background: rgba(255, 255, 255, 0.08);
    border-color: color-mix(in srgb, var(--accent) 25%, transparent);
    transform: translateY(-1px);
}
.site-header__auth {
    display: inline-flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    /* Must wrap. With justify-content: flex-end a row that does not fit spills
       to the LEFT, off the edge of the viewport — at 390px the theme toggle and
       the Settings button were pushed to negative coordinates and clipped to a
       sliver. Wrapping keeps every control on screen at any width. */
    flex-wrap: wrap;
}
.site-header .theme-toggle {
    border: 1px solid var(--accent) !important;
    background: var(--accent) !important;
    color: var(--accent-contrast) !important;
    border-radius: 999px;
    padding: 8px 14px;
    font-family: var(--font-body);
    font-weight: 700;
    font-size: 0.85rem;
    cursor: pointer;
    transition: filter 0.2s ease, transform 0.2s ease;
}
.site-header .theme-toggle:hover {
    filter: brightness(0.92);
    transform: translateY(-1px);
}

/* The icon shows the theme you would GET, which is what the label beside it
   already says ("Dark Mode" while light is active). Both glyphs are in the DOM
   and CSS picks one, so no JS has to keep them in step. */
.site-header .theme-toggle__icon {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
}
.site-header .theme-toggle__icon svg {
    width: 1.05em;
    height: 1.05em;
}
/* Light theme is active → offer the moon. */
.site-header .theme-toggle__icon svg + svg {
    display: none;
}
/* Dark theme is active → offer the sun. */
html[data-theme="dark"] .site-header .theme-toggle__icon svg {
    display: none;
}
html[data-theme="dark"] .site-header .theme-toggle__icon svg + svg {
    display: inline-block;
}

/* Settings was a filled accent pill with no id, classed `theme-toggle` to borrow
   its styling, which left the header showing two visually identical teal pills
   with no way to tell a state toggle from a plain action. It is a secondary
   action, so it now reads like Login beside it.

   WHY THE !important HERE
   -----------------------
   public/css/style.css carries a bare `button { ... }` rule with six
   `!important` declarations that paint EVERY button in the application as a
   solid accent pill:
       background: var(--accent) !important;  color: var(--accent-contrast) !important;
       box-shadow: var(--shadow-md) !important;  ...
   Nothing without an `!important` of its own can opt out, which is exactly why
   this control could not stop looking like the toggle even once its class had
   changed — and why the theme toggle above needs `!important` for the same
   properties. Restating them here with `!important` is the targeted fix; the
   global rule itself is left alone because an unknown number of class-less
   buttons elsewhere in the app depend on it.

   The `padding` deliberately has no `!important`: the selector is written
   `.site-header .site-header__settings` (0,2,0) so it already outranks the
   `html[data-ui-mobile] body button` rule (0,1,3) that would otherwise force
   44px touch padding on desktop, while the collapsible-menu media query further
   down — same specificity, later in the file — still overrides it on phones. */
.site-header .site-header__settings {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 12px;
    border: 1px solid transparent !important;
    border-radius: 10px !important;
    background: transparent !important;
    box-shadow: none !important;
    color: var(--nav-text) !important;
    font-family: var(--font-body);
    font-weight: 700 !important;
    font-size: 0.9rem;
    line-height: 1.6;
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.site-header .site-header__settings:hover {
    /* Neutralise the global `button:hover`, which lifts and brightens every
       button in the app — wrong for a quiet text control. */
    transform: none;
    filter: none;
    background: rgba(255, 255, 255, 0.08) !important;
    color: var(--accent) !important;
}

/* Gap between the icon and the label on the toggle. */
.site-header .theme-toggle {
    display: inline-flex;
    align-items: center;
    gap: 7px;
}
.nav-user-link {
    color: var(--nav-text);
    text-decoration: none;
    font-weight: 700;
    padding: 8px 12px;
    border-radius: 10px;
    border: 1px solid transparent;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.nav-user-link:hover {
    background: rgba(255, 255, 255, 0.08);
    border-color: color-mix(in srgb, var(--accent) 25%, transparent);
    transform: translateY(-1px);
}
.nav-user-menu {
    position: relative;
}
.nav-user-menu summary {
    list-style: none;
}
.nav-user-menu summary::-webkit-details-marker {
    display: none;
}
.nav-user-menu__trigger {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--nav-text);
    padding: 8px 12px;
    border-radius: 10px;
    border: 1px solid transparent;
    cursor: pointer;
    font-weight: 700;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.nav-user-menu__trigger:hover {
    transform: translateY(-1px);
}
.nav-user-menu__trigger-label {
    max-width: 180px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.nav-user-menu__caret {
    font-size: 0.75rem;
    color: var(--nav-muted);
    transition: transform 0.2s ease, color 0.2s ease;
}
.nav-user-menu[open] .nav-user-menu__caret {
    transform: rotate(180deg);
    color: var(--nav-text);
}
.nav-user-menu--left .nav-user-menu__panel {
    left: 0;
    right: auto;
}
.nav-user-menu__panel {
    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    min-width: 220px;
    display: grid;
    gap: 4px;
    padding: 10px;
    background: rgba(12, 22, 29, 0.97);
    border: 1px solid var(--nav-border);
    border-radius: 14px;
    box-shadow: 0 18px 36px rgba(0, 0, 0, 0.36);
}
.nav-user-menu__link {
    display: block;
    width: 100%;
    color: var(--nav-text);
    text-decoration: none;
    font-weight: 700;
    padding: 10px;
    text-align: left;
    border-radius: 10px;
    border: 1px solid transparent;
    transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.nav-user-menu__link:hover {
    background: rgba(255, 255, 255, 0.08);
    border-color: color-mix(in srgb, var(--accent) 25%, transparent);
    transform: translateY(-1px);
}
/* Reset the logout <button> so it renders like the menu links. */
.nav-user-menu__logout-form {
    margin: 0;
    padding: 0;
}
.nav-user-menu__logout-button {
    background: none;
    cursor: pointer;
    font: inherit;
    width: 100%;
}
.nav-user-menu__action {
    display: block;
    width: 100%;
    padding: 10px !important;
    color: var(--nav-text) !important;
    text-decoration: none !important;
    font: inherit;
    font-weight: 700 !important;
    text-align: left;
    background: transparent !important;
    border: 1px solid transparent !important;
    border-radius: 10px !important;
    box-shadow: none !important;
    cursor: pointer;
}
.nav-user-menu__action:hover {
    background: rgba(255, 255, 255, 0.08) !important;
    border-color: color-mix(in srgb, var(--accent) 25%, transparent) !important;
    color: var(--nav-text) !important;
    filter: none !important;
}
.status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #8aa4b2;
    box-shadow: 0 0 8px rgba(0,0,0,0.25);
}
.status-dot.is-online { background: var(--ok); box-shadow: 0 0 10px color-mix(in srgb, var(--ok) 80%, transparent); }
.status-dot.is-offline { background: var(--warn); box-shadow: 0 0 10px color-mix(in srgb, var(--warn) 70%, transparent); }

.breadcrumb-row {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 24px 12px;
    display: flex;
    align-items: center;
    gap: 16px;
    justify-content: space-between;
    flex-wrap: wrap;
}
.breadcrumb-trail {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    color: var(--nav-muted);
    font-size: 0.8125rem;
}
.breadcrumb-trail a {
    color: #8be8ff;
    font-weight: 700;
    text-decoration: none;
}
.breadcrumb-trail .crumb-separator { color: var(--nav-muted); }
.breadcrumb-trail [aria-current="page"] { color: var(--nav-text); font-weight: 700; }

.breadcrumb-progress {
    display: grid;
    grid-template-columns: 1fr;
    gap: 6px;
    min-width: 230px;
    background: rgba(20, 34, 44, 0.78);
    border: 1px solid var(--nav-border);
    border-radius: 12px;
    padding: 10px 12px;
    box-shadow: 0 6px 16px rgba(0,0,0,0.18);
}
.breadcrumb-progress.is-hidden { display: none; }
.breadcrumb-progress .progress-label { color: var(--nav-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; }
.breadcrumb-progress .progress-meta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 0.75rem;
    color: var(--nav-muted);
}
.breadcrumb-progress .progress-value { color: var(--nav-text); font-weight: 700; }

@media (max-width: 720px) {
    .site-header__bar { flex-direction: column; align-items: flex-start; }
    .site-header__nav { width: 100%; }
    .nav-links-group,
    .nav-inline-actions {
        flex-basis: 100%;
        justify-content: center;
    }
    .nav-inline-actions {
        margin-left: 0;
        margin-top: 4px;
    }
    .site-header__bar.has-inline-actions .nav-auth-desktop {
        display: none;
    }
    .site-header__bar.has-inline-actions .nav-auth-mobile {
        display: inline-flex;
    }
    .site-header__auth { width: 100%; }
    .nav-user-link { width: 100%; text-align: center; }
    .nav-user-menu { width: 100%; }
    .nav-user-menu__trigger { width: 100%; justify-content: space-between; }
    .nav-user-menu__panel { position: static; margin-top: 8px; width: 100%; }
    .breadcrumb-row { flex-direction: column; align-items: flex-start; }
    .breadcrumb-progress { display: none; }
}

@media (max-width: 400px) {
    .site-header__bar { padding: 8px 14px 4px; }
    .breadcrumb-row { padding: 0 14px 8px; }
    .site-header__nav { gap: 6px; }
    .nav-links-group { gap: 6px; }
    .site-header__nav .nav-link { padding: 6px 8px; font-size: 0.88rem; }
    .template-actions { left: 8px; right: 8px; }
}

[data-progress-slug] .progress-bar { background: rgba(255,255,255,0.1); height: 8px; border-radius: 8px; overflow: hidden; }
[data-progress-slug] [data-progress-fill] { display: block; height: 100%; background: var(--nav-highlight); border-radius: 8px; width: 0; }
[data-progress-slug] .progress-meta { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--nav-muted); }
[data-progress-slug] .progress-value { font-weight: 700; color: var(--nav-text); }

/* UI Kit shared elements */
.ui-loading-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(5, 12, 18, 0.75);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 12000;
}
.ui-loading-backdrop.is-visible { display: flex; }
.ui-loading-card {
    min-width: 220px;
    padding: 18px;
    border-radius: 14px;
    background: rgba(12, 22, 29, 0.96);
    border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
    box-shadow: 0 14px 38px rgba(0,0,0,0.45);
    color: #e8edf7;
    display: grid;
    gap: 10px;
    text-align: center;
}
.ui-spinner {
    width: 32px;
    height: 32px;
    border: 4px solid rgba(255,255,255,0.15);
    border-top-color: #ff8a3d;
    border-radius: 50%;
    animation: ui-spin 1s linear infinite;
    margin: 0 auto;
}
@keyframes ui-spin { to { transform: rotate(360deg);} }

.toast-stack {
    position: fixed;
    right: 18px;
    bottom: 18px;
    display: grid;
    gap: 8px;
    z-index: 12050;
}
.toast {
    min-width: 240px;
    max-width: 360px;
    padding: 12px 14px;
    border-radius: 12px;
    background: rgba(12, 21, 29, 0.96);
    border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
    color: #e8edf7;
    box-shadow: 0 10px 24px rgba(0,0,0,0.35);
    display: flex;
    justify-content: space-between;
    gap: 12px;
    align-items: flex-start;
}
.toast.success { border-color: rgba(52,211,153,0.4); }
.toast.error { border-color: rgba(248,113,113,0.5); }
.toast.info { border-color: rgba(125,211,252,0.5); }
.toast button { background: none; border: none; color: #fff; cursor: pointer; font-weight: 700; }

.ui-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(5, 12, 18, 0.78);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 12040;
}
.ui-modal-backdrop.is-visible { display: flex; }
.ui-modal {
    width: min(680px, 90vw);
    max-height: 88vh;
    overflow-y: auto;
    background: var(--surface-grad, linear-gradient(160deg, color-mix(in srgb, var(--accent) 8%, transparent), color-mix(in srgb, var(--accent) 5%, transparent) 46%, rgba(0, 0, 0, 0.12))), var(--panel, rgba(14, 22, 28, 0.86));
    border: 1px solid var(--line, color-mix(in srgb, var(--accent) 25%, transparent));
    border-radius: var(--radius, 16px);
    box-shadow: var(--shadow-lg, 0 22px 45px rgba(0, 0, 0, 0.34)), inset 0 1px 0 rgba(255, 255, 255, 0.06);
    color: var(--text, #eaf4fa);
    padding: 1.4rem 1.5rem 1.5rem;
}
html[data-theme="light"] .ui-modal {
    background: var(--surface-grad, linear-gradient(160deg, rgba(255, 179, 71, 0.05), rgba(86, 182, 255, 0.04) 46%, rgba(0, 0, 0, 0.01))), var(--panel, rgba(255, 255, 255, 0.94));
    border-color: var(--line, rgba(68, 100, 140, 0.18));
    box-shadow: var(--shadow-lg, 0 18px 42px rgba(68, 104, 145, 0.1)), inset 0 1px 0 rgba(255, 255, 255, 0.55);
    color: var(--text, #5a5a5a);
}
.ui-modal header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.ui-modal h3 {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 800;
    /* Was a four-stop rainbow gradient clipped to the text (#fff2d9 -> #ffbc79
       -> #7de6ff -> #ffffff, and a burgundy/olive/blue twin in light). The same
       treatment was removed elsewhere in this codebase for making headings
       unreadable; it survived here on the Settings and Help dialog titles. */
    color: var(--text-strong);
}
.ui-modal .ui-modal-body { display: grid; gap: 0.65rem; }
.ui-modal .pill-row { display: flex; gap: 8px; flex-wrap: wrap; }
.ui-modal .pill-chip {
    padding: 6px 12px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
    color: var(--text-muted);
    font-weight: 600;
    font-size: 0.85rem;
}
html[data-theme="light"] .ui-modal .pill-chip {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border-color: color-mix(in srgb, var(--accent) 25%, transparent);
    color: var(--accent);
}
.ui-modal .actions { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-top: 1.2rem; }
.ui-modal .actions:has(> button:only-child) { justify-content: center; }

.ui-modal-compact {
    width: fit-content;
    max-width: min(520px, 92vw);
    min-width: 280px;
    padding: 1rem 1.25rem 1.15rem;
}
.ui-modal-compact header {
    margin-bottom: 0.55rem;
}
.ui-modal-compact .ui-modal-body {
    gap: 0.4rem;
}
.ui-modal-compact .pill-row {
    gap: 6px;
}
.ui-modal-compact .pill-chip {
    padding: 5px 11px;
    font-size: 0.82rem;
}
.ui-modal-compact .actions {
    gap: 0.55rem;
    margin-top: 0.75rem;
}
.ui-modal-compact p {
    margin: 0.15rem 0 0;
    font-size: 0.95rem;
}

/* Modal close (×) button — used by UI kit modals (settings/help/completion).
   Previously only styled inside competitions.css, so it was unstyled elsewhere. */
.ui-modal .modal-close {
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.16);
    color: #cdd7f5;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1;
    width: 34px;
    height: 34px;
    flex: 0 0 34px;
    border-radius: 10px;
    padding: 0;
    cursor: pointer;
    box-shadow: none;
    transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease;
}
html[data-theme="light"] .ui-modal .modal-close {
    background: color-mix(in srgb, var(--text) 6%, transparent);
    border-color: color-mix(in srgb, var(--text) 18%, transparent);
    color: #5a5a5a;
}
html[data-theme="light"] .ui-modal .modal-close:hover {
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
    color: var(--danger);
}
.ui-modal .modal-close:hover {
    background: color-mix(in srgb, var(--danger) 20%, transparent);
    border-color: color-mix(in srgb, var(--danger) 45%, transparent);
    color: #ffd7dc;
}

.ui-button {
    border: 1px solid rgba(255, 255, 255, 0.18);
    /* Was an orange gradient (#d8622d -> #ff8a3d -> #ffb564) with white text.
       That is the previous brand, and it measured 1.74:1 at the light end — a
       hard WCAG failure on the primary action of the Settings and Help dialogs.
       The accent pair is 9.93:1 light / 9.39:1 dark. */
    background: var(--accent);
    color: var(--accent-contrast);
    border-radius: 12px;
    padding: 10px 16px;
    font-weight: 700;
    cursor: pointer;
    box-shadow: var(--shadow-md, 0 12px 24px rgba(0, 0, 0, 0.24));
    transition: transform 0.16s ease, filter 0.16s ease;
}
.ui-button:hover { transform: translateY(-1px); filter: brightness(1.07); }
.ui-button.secondary {
    background: linear-gradient(125deg, color-mix(in srgb, var(--accent) 25%, transparent), rgba(42, 84, 104, 0.36));
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
    color: var(--text);
}
html[data-theme="light"] .ui-button.secondary {
    /* Was an olive tint (#4a6237) that exists in no token. */
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border-color: color-mix(in srgb, var(--accent) 35%, transparent);
    color: var(--accent);
}
.ui-button.danger {
    background: linear-gradient(125deg, color-mix(in srgb, var(--danger) 30%, transparent), color-mix(in srgb, var(--danger) 42%, transparent));
    border-color: color-mix(in srgb, var(--danger) 50%, transparent);
    /* Was var(--danger) on a danger tint: 3.17:1 in dark. The surface carries the
       meaning; the label needs ordinary ink. */
    color: var(--text);
}
html[data-theme="light"] .ui-button.danger {
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    border-color: color-mix(in srgb, var(--danger) 40%, transparent);
    color: var(--text);
}

/* === Settings & Help panels ===
   Settings is a <ul class="ui-settings-list"> where <li>s alternate between
   dropdown rows (label + select) and toggle rows (label wrap around checkbox).
   We lay them out as bordered rows so each setting reads as its own field. */
.ui-settings-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.6rem;
}
.ui-help-list {
    list-style: disc;
    margin: 0;
    padding-left: 1.2rem;
    display: grid;
    gap: 0.5rem;
}
.ui-settings-list li {
    color: var(--text, #eaf4fa);
    background: var(--panel-soft, rgba(21, 33, 42, 0.78));
    border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
    border-radius: 12px;
    padding: 0.7rem 0.85rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}
html[data-theme="light"] .ui-settings-list li {
    color: var(--text, #5a5a5a);
    background: var(--panel-soft, rgba(245, 250, 255, 0.92));
}
.ui-help-list li { color: var(--text-muted, #adc4d2); line-height: 1.5; }
html[data-theme="light"] .ui-help-list li { color: var(--text-muted, #8a8279); }

/* Sound row: toggle label on the left, fanfare preview button on the right. */
.ui-setting-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex: 1;
    min-width: 0;
}
.ui-preview-sound {
    padding: 0.35rem 0.85rem;
    font-size: 0.85rem;
    flex: none;
}

/* === Structured puzzle help (Goal / Controls / Keys / Difficulty / Tips) ===
   Rendered both inline on puzzle pages (sidebar-howto / classic-clues-section)
   and inside the UI Kit Help (?) dialog. The block adapts to its container. */
.puzzle-help { display: grid; gap: 0.85rem; }
.puzzle-help__goal,
.puzzle-help__section { display: grid; gap: 0.35rem; }
.puzzle-help__label {
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent);
}
html[data-theme="light"] .puzzle-help__label { color: var(--accent); }
.puzzle-help__goal p,
.puzzle-help__text {
    margin: 0;
    line-height: 1.55;
    color: var(--text, #eaf4fa);
}
html[data-theme="light"] .puzzle-help__goal p,
html[data-theme="light"] .puzzle-help__text { color: var(--text, #5a5a5a); }
.puzzle-help__list {
    list-style: disc;
    margin: 0;
    padding-left: 1.15rem;
    display: grid;
    gap: 0.35rem;
    color: var(--text-muted, #adc4d2);
    line-height: 1.5;
}
html[data-theme="light"] .puzzle-help__list { color: var(--text-muted, #8a8279); }
.puzzle-help__list li::marker { color: var(--accent); }
.puzzle-help__keys {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.3rem 0.8rem;
    margin: 0;
    align-items: center;
}
.puzzle-help__keys dt { margin: 0; }
.puzzle-help__keys dd {
    margin: 0;
    color: var(--text-muted, #adc4d2);
    font-size: 0.9rem;
    line-height: 1.4;
}
html[data-theme="light"] .puzzle-help__keys dd { color: var(--text-muted, #8a8279); }
.puzzle-help kbd {
    display: inline-block;
    min-width: 1.4em;
    padding: 2px 7px;
    border-radius: 6px;
    
    /* --font-mono, not --font-family-mono: the latter is defined only by the
       per-game sheets, which are not loaded on every page. The previous
       `var(--font-family-mono, ui-monospace, monospace)` therefore resolved to a
       bare system stack here and skipped the metric-matched fallback, so this
       reflowed when JetBrains Mono swapped in — measured as the +97px growth of
       .puzzle-help that produced CLS 0.144 on /play/wordsearch. */
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 700;
    text-align: center;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.2);
    color: #eaf4fa;
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);
}
html[data-theme="light"] .puzzle-help kbd {
    background: color-mix(in srgb, var(--text) 6%, transparent);
    border-color: color-mix(in srgb, var(--text) 18%, transparent);
    color: #3d3d3d;
}
/* When the structured block is dropped into the modal body, give it a touch
   more breathing room. */
.ui-modal .puzzle-help { gap: 1rem; }
.ui-modal .puzzle-help__list { font-size: 0.92rem; }

.ui-select-label {
    color: var(--text);
    font-weight: 700;
    font-size: 0.92rem;
}
html[data-theme="light"] .ui-select-label {
    color: #3d3d3d;
}

.ui-inline-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    cursor: pointer;
    color: var(--text, #eaf4fa);
    font-weight: 500;
    font-size: 0.92rem;
    flex: 1;
}
html[data-theme="light"] .ui-inline-toggle {
    color: var(--text, #5a5a5a);
}
.ui-inline-toggle input[type='checkbox'] {
    appearance: none;
    -webkit-appearance: none;
    width: 42px;
    height: 24px;
    flex: 0 0 42px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.1);
    border: 1px solid rgba(255, 255, 255, 0.22);
    position: relative;
    cursor: pointer;
    margin: 0;
    transition: background 0.2s ease, border-color 0.2s ease;
    accent-color: var(--accent);
}
.ui-inline-toggle input[type='checkbox']::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: #e8edf7;
    transition: transform 0.2s ease, background 0.2s ease;
}
.ui-inline-toggle input[type='checkbox']:checked {
    background: linear-gradient(120deg, var(--brand, #ff8a3d), var(--brand-2, #51d6ff));
    border-color: color-mix(in srgb, var(--accent) 50%, transparent);
}
.ui-inline-toggle input[type='checkbox']:checked::after {
    transform: translateX(18px);
    background: #fff;
}

/* Selects inside settings rows */
.ui-settings-list select {
    min-width: 150px;
    width: auto;
}

/* Streak badge in nav header */
.streak-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 5px 10px;
    border-radius: 999px;
    background: rgba(255, 160, 50, 0.15);
    border: 1px solid rgba(255, 160, 50, 0.35);
    color: var(--accent);
    font-weight: 700;
    font-size: 0.85rem;
    text-decoration: none;
    margin-right: 8px;
    transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.streak-badge:hover {
    background: rgba(255, 160, 50, 0.25);
    border-color: rgba(255, 160, 50, 0.55);
    transform: translateY(-1px);
}
.streak-badge.streak-at-risk {
    animation: streak-pulse 1.5s ease-in-out infinite;
}
.streak-icon { font-size: 1rem; line-height: 1; }
.streak-count { line-height: 1; }
@keyframes streak-pulse {
    0%, 100% { border-color: rgba(255, 160, 50, 0.35); }
    50% { border-color: rgba(255, 80, 50, 0.7); box-shadow: 0 0 10px rgba(255, 80, 50, 0.3); }
}
html[data-theme='light'] .streak-badge {
    /* #9a7a2e measured 3.40:1 on this tint. --warn is the darker light-theme gold
       defined for exactly this surface. */
    background: color-mix(in srgb, var(--warn) 15%, transparent);
    border-color: color-mix(in srgb, var(--warn) 45%, transparent);
    color: var(--warn);
}
html[data-theme='light'] .streak-badge:hover {
    background: rgba(196, 154, 77, 0.22);
    border-color: rgba(196, 154, 77, 0.6);
}

/* Streak-at-risk warning banner */
.streak-warning-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 10000;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 20px;
    /* Was white on an orange gradient: 2.64:1 at the light end, on a role="alert"
       banner. The surface is now the danger token and the ink is chosen per
       theme: #fff on #9C3A32 is 6.85:1, and --bg-0 on #E8705C is 5.89:1. */
    background: var(--danger);
    color: #fff;
    font-weight: 600;
    font-size: 0.88rem;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    animation: streak-banner-slide 0.4s ease-out;
}
@keyframes streak-banner-slide {
    from { transform: translateY(-100%); }
    to { transform: translateY(0); }
}
.streak-warning-icon { font-size: 1.2rem; }
.streak-warning-close {
    margin-left: auto;
    background: none;
    border: none;
    color: #fff;
    cursor: pointer;
    font-size: 1rem;
    padding: 4px 8px;
    border-radius: 6px;
    opacity: 0.8;
    transition: opacity 0.2s;
}
.streak-warning-close:hover { opacity: 1; }
html[data-theme='light'] .streak-warning-banner {
    /* The dark rule sets a solid --danger with #fff ink (6.85:1 light / 5.89:1
       dark). This override replaced the background with a red GRADIENT but left
       the ink at #fff, and the gradient's lighter stop composites to ~4.29:1 —
       just under AA for 0.88rem text, on an alert. Solid --danger keeps the
       measured 6.85:1. */
    background: var(--danger);
}

.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0; }
.ui-reduce-motion *, .ui-reduce-motion *::before, .ui-reduce-motion *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
.ui-high-contrast { filter: saturate(1.1); }
.ui-high-contrast .card, .ui-high-contrast .ui-modal, .ui-high-contrast .card-progress, .ui-high-contrast .toast { border-color: rgba(255,255,255,0.35) !important; box-shadow: 0 0 0 1px rgba(255,255,255,0.22) inset, 0 10px 26px rgba(0,0,0,0.4); }

/* Shared template action rail */
.template-actions {
    position: fixed;
    right: 18px;
    top: 86px;
    z-index: 12020;
    pointer-events: none;
}
.template-actions__inner {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: flex-end;
    pointer-events: auto;
}
.template-action {
    border: 1px solid rgba(255,255,255,0.12);
    background: linear-gradient(124deg, color-mix(in srgb, var(--accent) 95%, transparent), color-mix(in srgb, var(--accent) 95%, transparent));
    color: #f8fafc;
    padding: 9px 14px;
    border-radius: 12px;
    font-weight: 700;
    box-shadow: 0 10px 24px rgba(0,0,0,0.35);
    cursor: pointer;
    transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.template-action.secondary {
    background: linear-gradient(124deg, color-mix(in srgb, var(--accent) 24%, transparent), color-mix(in srgb, var(--accent) 36%, transparent));
    border-color: color-mix(in srgb, var(--accent) 36%, transparent);
}
.template-action:hover { transform: translateY(-1px); box-shadow: 0 16px 34px rgba(0,0,0,0.4); }
.template-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (max-width: 720px) {
    .template-actions { left: 18px; right: 18px; top: 74px; }
    .template-actions__inner { justify-content: flex-start; }
    .template-action { width: 100%; text-align: center; }
}

/* Favourite toggle button in breadcrumb row */
.fav-btn {
    background: none;
    border: 1px solid rgba(255,255,255,0.16);
    border-radius: 10px;
    color: var(--nav-muted);
    cursor: pointer;
    font-size: 1.25rem;
    line-height: 1;
    padding: 6px 10px;
    transition: color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
    flex-shrink: 0;
}
.fav-btn:hover {
    color: #ff7e7e;
    border-color: rgba(255,100,100,0.4);
    transform: scale(1.1);
}
html[data-theme='light'] .fav-btn { color: #8a8279; border-color: color-mix(in srgb, var(--text) 18%, transparent); }
html[data-theme='light'] .fav-btn.is-fav { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent); }

/* Inline settings/help actions shown in the nav bar on selected pages (e.g. About, Leaderboard). */
.nav-inline-actions {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-left: 4px;
}
.nav-auth-mobile {
    display: none;
}
.site-header__nav button.nav-link {
    background: transparent !important;
    border: 1px solid transparent !important;
    border-radius: 10px !important;
    box-shadow: none !important;
    color: var(--nav-text) !important;
    font-family: inherit;
    cursor: pointer;
    filter: none !important;
}
.site-header__nav button.nav-link:hover {
    background: rgba(255, 255, 255, 0.08) !important;
    border-color: color-mix(in srgb, var(--accent) 25%, transparent) !important;
    color: var(--nav-text) !important;
    filter: none !important;
}
html[data-theme='light'] .site-header__nav button.nav-link {
    color: #3d3d3d !important;
}
html[data-theme='light'] .site-header__nav button.nav-link:hover {
    background: color-mix(in srgb, var(--accent) 8%, transparent) !important;
    border-color: color-mix(in srgb, var(--accent) 30%, transparent) !important;
}

/* ───────────────────────────────────────────────────────────────────
   CROSSWORD PAPER re-skin — nav overrides (appended last so they win).
   Neutralizes legacy orange/cyan brand tints in favour of the new
   vermilion + ink palette. Presentational only.
   ─────────────────────────────────────────────────────────────────── */
.site-header { box-shadow: 0 1px 0 var(--nav-border); }
.site-header__nav .nav-link:hover,
.nav-user-link:hover,
.nav-user-menu__link:hover,
.nav-user-menu__action:hover,
.site-header__nav button.nav-link:hover {
    border-color: var(--nav-border) !important;
}
.site-header__nav .nav-link.is-active,
.nav-user-menu[open] .nav-user-menu__trigger,
.nav-user-menu.is-active .nav-user-menu__trigger,
.nav-user-menu__trigger:hover {
    background: var(--accent) !important;
    color: var(--accent-contrast) !important;
    border-color: transparent !important;
    box-shadow: none !important;
}
.nav-user-menu__panel { background: var(--panel) !important; }
.breadcrumb-progress { background: var(--panel-soft) !important; }
.breadcrumb-trail a { color: var(--brand) !important; }
.breadcrumb-trail a:hover { color: var(--accent) !important; }
.template-action {
    background: var(--panel) !important;
    border-color: var(--nav-border) !important;
    color: var(--nav-text) !important;
}
.template-action:hover { border-color: var(--accent) !important; box-shadow: var(--shadow-md) !important; }
.template-action:focus-visible { outline-color: var(--accent) !important; }
.fav-btn.is-fav { color: var(--accent) !important; border-color: var(--accent) !important; }

/* =========================================================================
   SITE FOOTER
   -------------------------------------------------------------------------
   Moved here from public/css/style.css: the footer is chrome, and /3d does not
   load style.css at all, so the footer rendered unstyled there.

   Rewritten on tokens. The previous version used a backdrop-filter glass panel
   over an off-palette taupe gradient. Its hover rule was also dead code —
   `.footer-links a:hover` has a space before `hover`, which makes the selector
   invalid, so the hover colour never applied.

   Each link carries vertical padding to clear the WCAG 2.2 SC 2.5.8 minimum
   24x24 CSS px touch target, which the Lighthouse target-size audit checks.
   ========================================================================= */
.site-footer {
    border-top: 1px solid var(--line);
    background: var(--bg-1);
    color: var(--text-muted);
    padding: var(--space-4) var(--space-4);
    margin-top: var(--space-6);
}

.footer-inner {
    width: min(1200px, 100%);
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-4);
    font-size: var(--fs-sm);
}

.footer-links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--space-4);
    /* As above: was inherited from the bare `nav {}` rule. */
    justify-content: center;
}

.footer-links a {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    padding: var(--space-1) 2px;
    color: var(--accent);
    font-weight: 600;
    text-decoration: none;
    transition: color var(--dur-fast) linear;
}

.footer-links a:hover {
    color: var(--accent-hover);
    text-decoration: underline;
}

@media (max-width: 600px) {
    .footer-inner {
        flex-direction: column;
        justify-content: center;
        text-align: center;
    }
    .footer-links {
        justify-content: center;
    }
}

/* =========================================================================
   COOKIE CONSENT — non-blocking bottom bar
   -------------------------------------------------------------------------
   This used to be a centred modal with aria-modal="true" over a full-viewport
   backdrop: rgba(0,0,0,.55) plus backdrop-filter: blur(3px). Two problems:

   1. It dimmed AND blurred the entire page — including the hero and the primary
      call to action — on the very first thing a visitor ever sees. That is
      hostile, and heavy glassmorphism is explicitly called out as a look to
      avoid in the redesign brief.
   2. It announced itself as a modal while the rest of the page stayed
      interactive, and the card was hard-coded `background: #ffffff` with a
      dark-grey text colour, so dark mode was never actually dark.

   Consent is now a bar anchored to the bottom of the viewport. Nothing is
   covered, nothing is blurred, the page is readable and scrollable immediately,
   and every colour comes from the theme tokens so both themes work.

   "Accept all cookies" and "Necessary only" are given the same size, shape and
   row. Styling one as a loud filled button and the other as a barely-visible
   link nudges people toward the permissive option — a dark pattern that several
   EU data-protection authorities treat as invalid consent.

   The .cookie-modal__backdrop element is kept in the markup (the JS still
   queries it) but is now inert.
   ========================================================================= */
.cookie-modal {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 9000;
    display: flex;
    justify-content: center;
    padding: var(--space-3);
    /* Without this the full-width wrapper would swallow clicks on the page
       behind it even though nothing is drawn there. */
    pointer-events: none;
    animation: cookie-rise var(--dur-slow) var(--ease-out);
}

.cookie-modal[hidden],
.cookie-modal[data-hidden='true'],
.cookie-modal__actions[hidden],
.cookie-modal__manage[hidden] {
    display: none !important;
}

.cookie-modal__backdrop {
    display: none;
}

.cookie-modal__card {
    pointer-events: auto;
    position: relative;
    width: min(760px, 100%);
    max-height: min(72vh, 34rem);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    background: var(--bg-1);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: var(--shadow-lg);
    animation: cookie-rise var(--dur-slow) var(--ease-out);
}

.cookie-modal__title {
    margin: 0;
    font-size: 1.2rem;
    font-weight: 600;
    line-height: 1.25;
    color: var(--text-strong);
}

.cookie-modal__text {
    margin: 0;
    font-size: var(--fs-sm);
    line-height: 1.55;
    color: var(--text);
}

.cookie-modal__text + .cookie-modal__text {
    margin-top: calc(-1 * var(--space-3) + var(--space-2));
}

.cookie-modal__text a {
    color: var(--accent);
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.cookie-modal__text a:hover {
    color: var(--accent-hover);
}

.cookie-modal__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-1);
}

.cookie-modal__btn {
    flex: 1 1 12rem;
    font-family: var(--font-body) !important;
    font-weight: 600 !important;
    font-size: var(--fs-sm) !important;
    line-height: 1.2 !important;
    padding: 0.7rem 1.1rem !important;
    border-radius: var(--radius-pill) !important;
    cursor: pointer !important;
    border: 1px solid transparent !important;
    transition: background-color var(--dur-fast) linear,
                border-color var(--dur-fast) linear,
                color var(--dur-fast) linear !important;
    box-shadow: none !important;
    text-align: center;
}

.cookie-modal__btn:active {
    transform: scale(0.99);
}

/* Both consent choices: same size, same shape, same row — one filled, one
   outlined, so neither is hidden. */
.cookie-modal__btn--primary,
.cookie-modal__btn--primary:hover {
    background: var(--accent) !important;
    color: var(--accent-contrast) !important;
    border-color: var(--accent) !important;
}

.cookie-modal__btn--primary:hover {
    background: var(--accent-hover) !important;
    border-color: var(--accent-hover) !important;
}

.cookie-modal__btn--secondary,
.cookie-modal__btn--secondary:hover {
    background: transparent !important;
    color: var(--text) !important;
    border-color: var(--line) !important;
}

.cookie-modal__btn--secondary:hover {
    border-color: var(--accent) !important;
    color: var(--accent) !important;
}

.cookie-modal__link {
    align-self: flex-start;
    background: none;
    border: none;
    padding: var(--space-1) 0;
    margin: 0;
    font: inherit;
    font-weight: 600;
    color: var(--accent);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.cookie-modal__link:hover {
    color: var(--accent-hover);
}

.cookie-modal__manage {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding-top: var(--space-1);
    animation: cookie-rise var(--dur) var(--ease-out);
}

.cookie-modal__categories {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.cookie-modal__category {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3);
    background: var(--bg-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}

.cookie-modal__category-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.cookie-modal__category-name {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: 600;
    font-size: var(--fs-sm);
    color: var(--text-strong);
}

.cookie-modal__category-required {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    padding: 1px 7px;
}

.cookie-modal__category-desc {
    font-size: var(--fs-sm);
    line-height: 1.45;
    color: var(--text-muted);
}

.cookie-modal__switch {
    flex: 0 0 auto;
    min-width: 4.5rem;
    min-height: 32px;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: var(--fs-sm);
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: background-color var(--dur-fast) linear,
                border-color var(--dur-fast) linear,
                color var(--dur-fast) linear;
}

.cookie-modal__switch:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.cookie-modal__switch--on,
.cookie-modal__switch--on:hover {
    background: var(--accent) !important;
    color: var(--accent-contrast) !important;
    border-color: var(--accent) !important;
}

/* The non-interactive "Essential: always on" badge */
.cookie-modal__switch--on[aria-label] {
    cursor: default !important;
}

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

@media (prefers-reduced-motion: reduce) {
    .cookie-modal,
    .cookie-modal__card,
    .cookie-modal__manage { animation: none; }
}

@media (max-width: 560px) {
    .cookie-modal { padding: var(--space-2); }
    .cookie-modal__card { padding: var(--space-3); }
    .cookie-modal__btn { flex: 1 1 100%; }
}

/* =========================================================================
   HEADER TEXT METRICS — keep the header height independent of the font swap
   -------------------------------------------------------------------------
   Measured on /leaderboard with a LayoutShift observer installed before load:

     t=147ms  fonts:loading  header 103px  bar 55px  nav 37px
     t=200ms  fonts:loaded   header 110px  bar 62px  nav 44px

   With `line-height: normal` the line box is derived from the font's own
   ascent/descent, so replacing the fallback with the webfont grew the nav by
   7px. Because the header is sticky and in normal flow, that pushed the entire
   page down by 7px after first paint — CLS 0.157 on /leaderboard, the largest
   remaining shift on the site.

   An explicit unitless line-height makes the box height depend only on
   font-size, so it cannot change when the font loads. 1.6 was chosen because it
   reproduces the post-swap height exactly (1rem * 1.6 = 25.6px, + 16px padding
   + 2px border = 43.6px, matching the observed 44px), so the rendered header is
   unchanged — it is simply now decided up front instead of after the swap.
   ========================================================================= */
.site-header__brand .brand-mark,
.site-header__nav .nav-link,
.site-header__auth .nav-user-link,
.nav-user-menu__trigger,
.nav-user-menu__link,
.site-header .theme-toggle,
.site-header__settings {
    line-height: 1.6;
}

/* =========================================================================
   HEADER TEXT MUST NOT BE SQUEEZED
   -------------------------------------------------------------------------
   The header is a wrapping flex row, so at narrow widths its items were being
   shrunk below their own content width. At 390px the "Settings" pill computed to
   57px wide while its label needed more, and the text was clipped mid-word
   ("Setting"), with the button overlapping "Login" beside it.

   `white-space: nowrap` plus a disabled shrink factor means a label is either
   shown whole or the row wraps to the next line — never truncated. The header is
   already allowed to wrap, so this costs nothing.
   ========================================================================= */
.site-header__nav .nav-link,
.site-header__auth .nav-link,
.site-header__auth button,
.site-header .theme-toggle,
.site-header .site-header__settings,
.site-header .nav-user-link,
.site-header .nav-user-menu__trigger {
    white-space: nowrap;
    flex-shrink: 0;
}

/* =========================================================================
   COLLAPSIBLE MOBILE MENU
   -------------------------------------------------------------------------
   Below 768px the nav (4-5 links) plus the auth controls (theme toggle,
   settings, streak, account/login) wrapped into five stacked rows and made the
   header ~240px tall on an 844px-tall screen — a third of the viewport before
   any content. They now live behind a Menu button.

   `.site-header__panel` uses `display: contents` above the breakpoint, which
   makes the wrapper transparent to layout: the nav and the auth group remain
   direct flex children of the bar, so the desktop header is byte-for-byte the
   same as before this wrapper existed.
   ========================================================================= */
.site-header__panel {
    display: contents;
}

.site-header__menu {
    display: none;          /* desktop: the panel is always visible */
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border: 1px solid var(--nav-border);
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--nav-text);
    font-family: var(--font-body);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    flex-shrink: 0;
}

.site-header__menu:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* Three stacked bars, drawn with gradients so there is no icon font or image. */
.site-header__menu-bars {
    position: relative;
    display: block;
    width: 16px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
    box-shadow: 0 -5px 0 currentColor, 0 5px 0 currentColor;
    transition: transform var(--dur-fast) var(--ease-out);
}

.site-header[data-menu-open="true"] .site-header__menu-bars {
    background: transparent;
    box-shadow: none;
}

.site-header[data-menu-open="true"] .site-header__menu-bars::before,
.site-header[data-menu-open="true"] .site-header__menu-bars::after {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 16px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
}

.site-header[data-menu-open="true"] .site-header__menu-bars::before { transform: rotate(45deg); }
.site-header[data-menu-open="true"] .site-header__menu-bars::after  { transform: rotate(-45deg); }

@media (max-width: 768px) {
    .site-header__bar {
        padding: 8px 10px;
        gap: 8px;
    }

    .site-header__menu {
        display: inline-flex;
        /* Push the toggle to the far right of the brand row. */
        margin-left: auto;
    }

    .site-header__brand .brand-mark {
        font-size: 1rem;
    }

    /* Collapsed by default; the panel only exists while the menu is open. */
    .site-header__panel {
        display: none;
    }

    .site-header[data-menu-open="true"] .site-header__panel {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        width: 100%;
        gap: 6px;
        padding-top: 6px;
        margin-top: 2px;
        border-top: 1px solid var(--nav-border);
        animation: site-menu-open var(--dur) var(--ease-out);
        /* Very short screens must still be able to reach every item. */
        max-height: calc(100vh - 5.5rem);
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    .site-header__nav,
    .site-header__auth {
        flex-direction: column;
        align-items: stretch;
        width: 100%;
        gap: 4px;
    }

    .nav-links-group {
        flex-direction: column;
        align-items: stretch;
        gap: 2px;
    }

    /* Full-width rows read as a menu rather than a wrapped toolbar. */
    .site-header__nav .nav-link,
    .site-header__auth .nav-user-link,
    .site-header .theme-toggle,
    .site-header .site-header__settings,
    .site-header .nav-user-menu__trigger {
        width: 100%;
        justify-content: flex-start;
        padding: 11px 12px;
        font-size: 0.95rem;
        text-align: left;
    }

    /* The account/admin dropdowns become plain stacked lists inside the panel:
       a <details> popover inside a collapsing panel is a confusing double
       disclosure on a phone. */
    .site-header__panel .nav-user-menu__panel {
        position: static;
        box-shadow: none;
        border: 0;
        background: transparent;
        padding: 0 0 4px 12px;
        min-width: 0;
        width: 100%;
    }

    .site-header__auth .theme-toggle {
        justify-content: flex-start;
    }
}

@keyframes site-menu-open {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    .site-header[data-menu-open="true"] .site-header__panel { animation: none; }
    .site-header__menu-bars { transition: none; }
}

/* =========================================================================
   BRAND-CHROME ICONS
   -------------------------------------------------------------------------
   See src/common/Icon.php. Sized in `em` so an icon scales with the text it
   labels exactly as the emoji it replaced did, and inherits currentColor so it
   is correct in both themes with no second asset.
   ========================================================================= */
.icon {
    display: inline-block;
    width: 1.1em;
    height: 1.1em;
    vertical-align: -0.16em;
    flex-shrink: 0;
}

.icon--lg {
    width: 1.5em;
    height: 1.5em;
    vertical-align: -0.3em;
}

html[data-theme="dark"] .streak-warning-banner { color: var(--bg-0); }

/* =========================================================================
   SKIP LINK
   -------------------------------------------------------------------------
   First focusable element on every page, hidden until it is focused. Positioned
   rather than `display:none`/`visibility:hidden` so it remains focusable — a
   skipped-out element cannot receive focus and the link would never appear.
   ========================================================================= */
.skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1000;
    padding: 10px 16px;
    background: var(--accent);
    color: var(--accent-contrast);
    font-family: var(--font-body);
    font-weight: 700;
    font-size: 0.9rem;
    text-decoration: none;
    border-radius: 0 0 var(--radius-sm) 0;
    box-shadow: var(--shadow-md);
    /* Moved out of the viewport instead of hidden. */
    transform: translateY(-120%);
    transition: transform var(--dur) var(--ease-out);
}
.skip-link:focus,
.skip-link:focus-visible {
    transform: translateY(0);
}

/* Mobile optimizations. The <html>[data-ui-*] variants let the state be applied
   during <head> parsing (see Navigation::criticalStylesheets) so the 12px body
   padding and the 44px touch targets are settled BEFORE the first paint. They
   used to be applied by ui-kit.js after load, which shifted the entire document
   by 12px on every page — the largest remaining CLS source on /leaderboard. */
body.ui-touch,
body.ui-mobile-optimized,
html[data-ui-touch] body,
html[data-ui-mobile] body {
    padding: 12px;
}

body.ui-touch .button,
body.ui-touch button,
body.ui-mobile-optimized .button,
body.ui-mobile-optimized button,
html[data-ui-touch] body .button,
html[data-ui-touch] body button,
html[data-ui-mobile] body .button,
html[data-ui-mobile] body button {
    min-height: 44px;
    min-width: 44px;
    font-size: 1rem;
    padding: 10px 14px;
}

/* =========================================================================
   BIG ACTION BUTTON — the 2D/3D collection buttons
   -------------------------------------------------------------------------
   Moved here from css/index2D/styles.css, which is the HOMEPAGE sheet. This
   component is used on four pages that between them load three different sets of
   stylesheets, and on the pages that did not load the homepage sheet it silently
   degraded to a plain inline link. The 404's primary "Go Home" call to action was
   an unstyled <a> held together by an inline `style="display:inline-flex"` patch
   somebody had added to compensate for it.

   chrome.css is emitted by Navigation::criticalStylesheets() on every page, which
   is where a component that appears on every kind of page belongs. The rules are
   unchanged; only their home is.
   ========================================================================= */

.big-btn {
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 18px 24px;
    border-radius: var(--radius);
    text-decoration: none;
    position: relative;
    overflow: hidden;
    border: 1.5px solid transparent;
    cursor: pointer;
    transition: transform 0.22s cubic-bezier(.22, .68, 0, 1.2),
                box-shadow 0.22s ease,
                border-color 0.22s ease,
                background 0.22s ease,
                color 0.22s ease;
}

/* One unmistakable primary CTA (2D) — solid vermilion. */
.big-btn.primary {
    background: var(--accent);
    color: var(--accent-contrast);
    box-shadow: var(--shadow-md);
}
.big-btn.primary:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-lg);
    filter: brightness(1.04);
}

/* Lower-commitment secondary (3D) — quiet outline that fills toward accent. */
.big-btn.secondary {
    background: transparent;
    color: var(--text);
    border-color: var(--line);
    box-shadow: none;
}
.big-btn.secondary:hover {
    transform: translateY(-3px);
    border-color: var(--accent);
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.btn-icon {
    font-size: 38px;
    line-height: 1;
    flex-shrink: 0;
    transition: transform 0.25s ease;
}
.big-btn:hover .btn-icon { transform: scale(1.12) rotate(-5deg); }

.btn-text {
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex: 1;
}
.btn-text strong {
    font-family: var(--font-display);
    font-size: 1.6rem;
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.01em;
    display: block;
}
.btn-text small {
    font-family: var(--font-body);
    font-size: 0.72rem;
    font-weight: 600;
    /* This sits inside two different buttons, so it must inherit their colour:
       white on the vermilion primary, and --text on the cream secondary. A fixed
       colour token cannot serve both (a dark token would be ~1.3:1 on vermilion).
       The original 0.7 opacity composited the secondary label down to #898786 on
       cream — only 3.15:1, failing WCAG AA. 0.9 keeps the softened look while
       measuring 4.84:1 on the secondary and 6.47:1 on the primary. */
    opacity: 0.9;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    display: block;
}

.btn-arrow {
    font-size: 24px;
    font-weight: 700;
    opacity: 0.6;
    flex-shrink: 0;
    transition: transform 0.2s ease, opacity 0.2s ease;
}
.big-btn:hover .btn-arrow { transform: translateX(6px); opacity: 1; }

.big-btn .btn-icon svg {
    display: block;
    width: 30px;
    height: 30px;
}

.big-btn .btn-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    font-size: 0;
}

@media (max-width: 400px) {
    .big-btn .btn-icon,
    .big-btn .btn-icon svg { width: 24px; height: 24px; }
}

/* ============================================================================
   STICKY FOOTER
   Short pages (login, 404, reset links) used to leave the footer floating
   mid-viewport above a slab of dead background. Anchoring it costs two
   declarations: the body is a column flex container and the footer absorbs
   the free space above it. Pages that already manage body layout keep
   working — their own class selectors out-rank the bare element selector
   (e.g. body.game-3d-wrapper sets its own height/overflow).
   ============================================================================ */
html body {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

/* Specificity note: style.css also declares html, body { min-height: 100% }
   and loads after this sheet on pages that pull both. min-height:100% on a
   body under a height:auto html resolves to nothing, which let the footer
   float mid-page; html body (0,0,2) outranks it so the viewport floor and
   the auto margin below actually hold. */
html body > .site-footer {
    margin-top: auto;
}
