/* ===========================
   GLOBAL STYLES
   =========================== */

/* The house UI face ('Mono' Regular, Rony Setya Siswadi, via dafont). Everything
   that used to hard-code Arial now points at --game-font instead, so this is
   the one place a typeface swap happens. The wiki article pane is exempt on
   purpose: it keeps its own sans-serif/serif stack (see .wiki-article,
   .wiki-title and friends) so a race still looks and reads like real
   Wikipedia rather than the game's own chrome.
   LICENCE NOTE: unlike every other bundled font here, this one ships with no
   readme/licence file and its dafont listing could not be confirmed as
   commercial-safe (the same author's Bold weight is tagged personal-use-only
   elsewhere). No licence text exists to put in public/fonts/licenses/. Used
   at keko's explicit direction despite that; revisit before this game takes
   real payments off the back of it. */
@font-face {
    font-family: 'Mono';
    src: url('/fonts/Mono-Regular.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

/* Pretendard Bold, SIL OFL 1.1 (licence in public/fonts/licenses/). Member-list names and
   levels only; loaded on first use, so no other screen pays for the file. */
@font-face {
    font-family: 'Pretendard';
    src: url('/fonts/Pretendard-Bold.woff2') format('woff2');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* The chat face, SIL OFL 1.1 (see public/fonts/licenses/) — free to bundle
   and redistribute with the game as long as the font itself is never sold
   standalone and the licence text travels with each copy, which is what
   that folder is for. */
@font-face {
    font-family: 'Syne Mono';
    src: url('/fonts/SyneMono-Regular.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

/* The name on the profile polaroid. 1001Fonts Free For Commercial Use
   licence: embedding in a web app is allowed, offering the font as a
   download is not, and the glyphs may not be modified. The EULA is in
   public/fonts/licenses/. */
@font-face {
    font-family: 'Embolism Spark';
    src: url('/fonts/EmbolismSpark.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

/* Default UI colours. Accent defaults to the stock gold so every accent part
   (header/footer trim, checker flags, card goldframe) matches out of the box;
   ColorTheme.apply() overrides these inline on <html> when a colour is chosen
   and removes them (falling back to gold) when it's cleared. Theme (the card
   body/wash) stays white/neutral by default, so those vars are left unset. */
:root {
    --game-font: 'Mono', Consolas, monospace;
    --chat-font: 'Mono', Consolas, monospace;

    --accent: #d0d20d;
    --accent-deep: #ac8f0d;
    --accent-soft: #eaeb92;

    /* Height of the top strip the floating chrome sits in: wordmark, back
       button, profile card. It used to be the height of the header and footer
       bands, which were removed 2026-09-28; the name stayed because a lot of
       layout clears it. Overridden in the height/width breakpoints at the
       bottom of this file. */
    --chrome-h: calc(90px * var(--wdm-k, 1));

    /* How far in from the left edge the fixed wordmark reaches, plus a little
       air. Measured, not guessed: .site-logo is "WikiDerby.io" at 72px italic
       squashed to 0.63 of its width, sitting at left: 26px, so it ends at 299.
       Anything anchored to the top of the screen has to start after this or it
       runs underneath the mark, which stays put for the whole race. Overridden
       wherever the wordmark is resized — the same breakpoints as --chrome-h. */
    --logo-clear: 314px;

    /* Surfaces and ink. The defaults are exactly the literals these replaced,
       so the classic theme renders as it always did; midnight repaints them
       below and every panel follows without a filter. */
    --surface: #fff;
    /* The two steps below white. Half a dozen near-identical lavenders
       (#f2f4ff, #f4f6ff, #eef1ff …) collapsed into one token each: they were
       within a few units of one another, close enough that no one could pick
       them apart, and far enough apart to make a dark palette impossible to
       define. Classic shifts by at most 4/255 on a channel as a result. */
    --surface-2: #f2f4ff;
    --surface-3: #e8e8f0;
    --ink: #111;
    --ink-strong: #000;
    /* The neutral text scale, darkest to faintest. Near-identical values were
       folded together the same way the surfaces were (#333/#37424f/#445 into
       one step); coloured inks — the blue links, the red warnings, the gold
       prices — are left alone, because their hue is the message. */
    --ink-navy: #17233b;
    --ink-mid: #333;
    --ink-muted: #5b6474;
    --ink-faint: #888;

    /* The standard panel gradient: settings rows, dialogs, list rows, cards.
       It was written out thirteen times with alphas between 0.95 and 0.99 that
       nobody could tell apart, which is the same reason the surfaces above were
       folded into tokens — a literal repeated that many times cannot be
       repainted for a dark theme, and every one of those panels carries
       var(--ink) text that midnight turns near-white. White on white. */
    --panel-top: rgba(255, 255, 255, 0.97);
    --panel-bottom: rgba(236, 240, 255, 0.97);

    /* The brand blue, as INK. Only the text uses are tokenised: the same hex is
       also a fill under white text in 32 places, and those want no theming at
       all. On white it is the link colour it always was; midnight needs it
       lifted, because #0032ff on a dark panel is a navy smudge. */
    --ink-link: #0032ff;

    /* Warning/danger red for UI chrome (ban chips, error text) — distinct from
       --wiki-banned, which is scoped to the wiki article theme, not the UI
       theme. Needs the same midnight lift as --ink-link: this red on
       midnight's #4d5163 panel is a ~1.4:1 contrast, effectively invisible. */
    --danger: #c92a2a;
    --bad: #c92a2a;

    /* Theme slots. ColorTheme.apply() overrides these inline on <html> when the
       player picks a theme colour, and REMOVES them when they clear it — at
       which point these are what shows through. They used to exist only as
       hardcoded white fallbacks inside each var() call, so clearing the theme
       colour under midnight put white cards back under white text. */
    --theme-card: rgba(255, 255, 255, 0.72);
    --theme-tint: #ffffff;
    --theme-tint2: #eef0f4;
    --theme-wash: #fefefe;
    --theme-wash2: #f2f2f2;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    /* Sharp corners everywhere, no exceptions — including the 50% circles
       (avatars, dots), which is the point: a circle is round too. !important
       because this has to outrank every specific border-radius below it in
       the cascade (buttons, cards, panels, pills) without hunting down and
       zeroing all 220 of them by hand — one line here is the whole policy,
       and a component that wants a radius back would be fighting the design
       language, not a stylesheet bug. gfx-potato's own reset (further down)
       already did this for its one mode; this makes it the house style. */
    border-radius: 0 !important;
}

body {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    overflow-x: clip;
    overflow-y: auto;
    /* House style, same policy as border-radius above: every UI label reads in
       caps unless something opts out. Real Wikipedia content opts out below
       (.wiki-article etc.) so a race still reads like an actual article; a
       handful of existing rules already did their own text-transform: none
       for the same reason (room-code name input, wiki side-panel titles) and
       keep working unchanged. */
    text-transform: uppercase;
}

/* Wikipedia-authentic text: the raced article itself, its extracts/snippets
   shown elsewhere (WikiChannel, the racer feed), and the theme-preview mockup
   of a wiki page. These stay in real sentence case regardless of the house
   uppercase rule above — the whole point is that they read like Wikipedia. */
.wiki-article,
.wiki-toc,
.mp-spy-skin,
.ch-intro-desc,
.theme-preview-page,
.mp-app-lab,
.mp-app-radio {
    text-transform: none;
}

/* Scrollbars, everywhere a scrollable box in the app can appear — the page
   itself, the wiki article stage, chat, dialogs. The thumb is NOT the player's
   accent colour: it is neutral grey until an equipped shop theme sets
   --scroll-thumb to that theme's own base colour (customThemeCss in script.js). */
:root {
    --scroll-thumb: #6e6e6e;
    --scroll-thumb-hover: color-mix(in srgb, var(--scroll-thumb) 70%, #ffffff);
}

* {
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) var(--scroll-track, var(--surface-2));
}

::-webkit-scrollbar {
    width: 12px;
    height: 12px;
}

::-webkit-scrollbar-track {
    background: var(--scroll-track, var(--surface-2));
}

::-webkit-scrollbar-thumb {
    background: var(--scroll-thumb);
    border: 2px solid var(--scroll-track, var(--surface-2));
}

::-webkit-scrollbar-thumb:hover {
    background: var(--scroll-thumb-hover);
}

/* Child-scoped on purpose. An article is rendered as its own <main class="wiki-page">
   inside the stage (see WikiSkin.pageHtml), so a bare `main` selector hits it too and
   turns the article into a centred flex ROW: the title, the CC BY-SA footer and the
   "Target reached!" panel get squeezed to ~140px and centred against a 12,000px-tall
   .wiki-content, i.e. thousands of pixels off screen, while the content itself
   overhangs both edges on a phone. Every rule meant for the app shell is `body > main`. */
body > main {
    flex: 1;
    width: min(100%, 2200px);
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

/* ===========================
   GRID LAYOUT
   =========================== */

.menu-stage {
    width: 100%;
    overflow: visible;
    padding: 120px 0;
}

.grid-container {
    display: grid;
    grid-template-columns: repeat(3, minmax(420px, 504px));
    gap: 50px 170px;
    width: 100%;
    max-width: 2100px;
    justify-content: center;
}

/* Cards are things you click, not things you read, so a double-click on one
   should not leave its title highlighted the way it would on a web page.
   Scoped to the card class rather than to .grid-container: #menuGrid carries
   .grid-container on EVERY screen (see renderScreen), so a rule there is
   inherited by all of them, which is how the legal text, the About page, the
   shop and the settings all ended up unselectable.
   Keyed on .glass rather than on a direct child of the grid so the training
   difficulty bars — cards that sit two levels down — behave the same way. */
.glass {
    user-select: none;
}

/* ── Card menus never scroll ──────────────────────────────────────────────
   A menu is meant to be taken in at a glance, so the stage is exactly the
   space between the fixed header and footer and the page does not scroll.
   When the cards do not fit, MenuFit in script.js shrinks them via
   --menu-fit rather than letting the page grow.

   Only card screens opt in. The shop, the lobby lists and a race's article
   are lists that are supposed to scroll, so they never get this class — and
   :not(.wiki-reading) is belt-and-braces for a race started from here.

   The stage has to clip: a transform does not affect layout, so the grid's
   unscaled box still overflows even while it looks like it fits. That is the
   same property that lets MenuFit measure the natural size at any time
   without first undoing the scale. */
body.menu-fit:not(.wiki-reading) {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
}

body.menu-fit:not(.wiki-reading) main {
    min-height: 0;
    overflow: hidden;
}

body.menu-fit:not(.wiki-reading) .menu-stage {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

body.menu-fit:not(.wiki-reading) .grid-container {
    transform: scale(var(--menu-fit, 1));
    transform-origin: center center;
}

/* Safety valve: below the floor in MenuFit the cards would be unreadable, so
   the page is allowed to scroll again rather than clipping them out of reach. */
body.menu-fit.menu-fit-overflow:not(.wiki-reading),
body.menu-fit.menu-fit-overflow:not(.wiki-reading) main,
body.menu-fit.menu-fit-overflow:not(.wiki-reading) .menu-stage {
    height: auto;
    overflow: visible;
}

.grid-container.is-exiting,
.grid-container.is-entering,
.grid-container.is-exiting-back,
.grid-container.is-entering-back {
    pointer-events: none;
}

.grid-container.is-exiting .glass {
    animation: cardSlideOutLeft 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
    animation-delay: var(--row-delay);
}

.grid-container.is-entering .glass {
    animation: cardSlideInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
    animation-delay: var(--row-delay);
}

.grid-container.is-exiting-back .glass {
    animation: cardSlideOutRight 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
    animation-delay: var(--row-delay);
}

.grid-container.is-entering-back .glass {
    animation: cardSlideInLeft 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
    animation-delay: var(--row-delay);
}

.training-container.is-exiting .training-screen {
    animation: trainingScreenOutLeft 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.training-container.is-entering .training-screen {
    animation: trainingScreenInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

.training-container.is-exiting-back .training-screen {
    animation: trainingScreenOutRight 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.training-container.is-entering-back .training-screen {
    animation: trainingScreenInLeft 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

@keyframes cardSlideOutLeft {
    from {
        transform: translateX(0);
        opacity: 1;
    }

    68% {
        opacity: 1;
    }

    to {
        transform: translateX(-135vw);
        opacity: 0;
    }
}

@keyframes cardSlideInRight {
    from {
        transform: translateX(135vw);
        opacity: 0;
    }

    30% {
        opacity: 1;
    }

    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@keyframes cardSlideOutRight {
    from {
        transform: translateX(0);
        opacity: 1;
    }

    68% {
        opacity: 1;
    }

    to {
        transform: translateX(135vw);
        opacity: 0;
    }
}

@keyframes cardSlideInLeft {
    from {
        transform: translateX(-135vw);
        opacity: 0;
    }

    30% {
        opacity: 1;
    }

    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@keyframes trainingScreenOutLeft {
    from {
        transform: translateX(0);
        opacity: 1;
    }

    68% {
        opacity: 1;
    }

    to {
        transform: translateX(-115vw);
        opacity: 0;
    }
}

@keyframes trainingScreenInRight {
    from {
        transform: translateX(115vw);
        opacity: 0;
    }

    30% {
        opacity: 1;
    }

    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@keyframes trainingScreenOutRight {
    from {
        transform: translateX(0);
        opacity: 1;
    }

    68% {
        opacity: 1;
    }

    to {
        transform: translateX(115vw);
        opacity: 0;
    }
}

@keyframes trainingScreenInLeft {
    from {
        transform: translateX(-115vw);
        opacity: 0;
    }

    30% {
        opacity: 1;
    }

    to {
        transform: translateX(0);
        opacity: 1;
    }
}

/* ===========================
   GLASS CARD COMPONENTS
   =========================== */

.glass {
    appearance: none;
    font: inherit;
    width: 100%;
    aspect-ratio: 13 / 8;
    background: rgba(255, 255, 255, 0.0);
    border-radius: 7px;
    padding: 10px 15px;
    box-shadow:
        inset 0 0 2px 3px rgba(255, 255, 255, 0.5),
        inset 0 0 12px 9px rgba(255, 255, 255, 0.25),
        inset 0 0 30px 15px rgba(255, 255, 255, 0.15);
    position: relative;
    border-bottom: rgba(0, 0, 0, 0.267) 4px solid;
    transition: transform 0.2s ease;
    cursor: pointer;
    border: 0;
    will-change: transform, opacity;
    container-type: inline-size;
}

.glass:hover {
    transform: scale(1.1);
    z-index: 10;
}

.blur {
    position: absolute;
    top: 10px;
    left: 15px;
    right: 15px;
    bottom: 10px;
    /* --theme-card: the player's theme colour (white by default) */
    background: var(--theme-card, rgba(255, 255, 255, 0.6));
    border-radius: 16px;
    box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
    backdrop-filter: blur(15.1px);
    -webkit-backdrop-filter: blur(15.1px);
    border: 1px solid rgba(255, 255, 255, 0.3);
    z-index: -1;
}

.blue-card {
    width: 100%;
    height: 100%;
    padding: 7px 10px;
    border-radius: 7px;
    border: 2px solid black;
    position: relative;
}

.blue-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background:
        radial-gradient(circle at 0% 0%, var(--theme-tint, #ffffff) 0%, transparent 50%),
        radial-gradient(circle at 100% 0%, var(--theme-tint2, #eef0f4) 0%, transparent 50%),
        radial-gradient(circle at 0% 100%, var(--theme-tint2, #eef0f4) 0%, transparent 50%),
        radial-gradient(circle at 100% 100%, var(--theme-tint, #ffffff) 0%, transparent 50%),
        linear-gradient(135deg, var(--theme-tint, #ffffff), var(--theme-tint2, #eef0f4));
    background-blend-mode: normal;
    border-radius: 5px;
    z-index: -1;
    -webkit-mask:
        linear-gradient(#fff 0 0) exclude,
        linear-gradient(#fff 0 0) 50% 90% / 93% 65% no-repeat;
    mask:
        linear-gradient(#fff 0 0) exclude,
        linear-gradient(#fff 0 0) 50% 87% / 93% 64% no-repeat;
    -webkit-mask-composite: xor;
    mask-composite: exclude;
}

.inner {
    height: 100%;
    background: rgba(255, 255, 255, 0.0);
    border-radius: 7px;
    border: 1.5px solid rgba(63, 63, 63, 0.671);
    box-shadow:
        inset 0 0 2px 3px rgba(255, 255, 255, 0.5),
        inset 0 0 12px 9px rgba(255, 255, 255, 0.25),
        inset 0 0 30px 15px rgba(255, 255, 255, 0.15);
    padding: 1px 1px;
}

.square {
    position: absolute;
    background-color: #0033ff00;
    width: 97%;
    height: 65%;
    top: 65%;
    left: 50%;
    transform: translate(-50%, -50%);
    box-shadow:
        0 0 2px 3px rgba(255, 255, 255, 0.5),
        0 0 12px 9px rgba(255, 255, 255, 0.25),
        0 0 30px 15px rgba(255, 255, 255, 0.15);
    z-index: 2;
}

/* ===========================
   CARD PATTERNS
   =========================== */

.chess {
    position: relative;
    width: 100%;
    height: 30%;
    border-radius: 3px;
    border: 1px solid rgba(0, 0, 0, 0.171);
    background: repeating-conic-gradient(#ffffff27 0 25%, #0000 0 50%) 50% / 30px 30px;
}

/* ===========================
   GOLD FRAME COMPONENTS
   =========================== */

.goldframe {
    width: 100%;
    height: 70%;
    padding: 5px 10px 10px 10px;
    transition: transform 0.2s ease;
}

.glass:hover .goldframe {
    transform: scale(1.09) translateY(5px);
}

.goldframe2 {
    width: 100%;
    height: 100%;
    /* --accent-deep/--accent: the player's accent colour (ColorTheme.apply);
       unset, the frame keeps its stock gold. */
    background:
        radial-gradient(circle at 0% 0%, var(--accent-deep, #ac8f0d) 0%, transparent 50%),
        radial-gradient(circle at 100% 0%, var(--accent, #d0d20d) 0%, transparent 50%),
        radial-gradient(circle at 0% 100%, var(--accent, #d0d20d) 0%, transparent 50%),
        radial-gradient(circle at 100% 100%, var(--accent-deep, #ac8f0d) 0%, transparent 50%),
        linear-gradient(135deg, var(--accent-deep, #ac8f0d), var(--accent, #d0d20d));
    border-radius: 1px;
    border-left: 4px solid black;
    border-right: 4px solid black;
    border-top: 2px solid black;
    border-bottom: 2px solid black;
    padding: 4px 0px;
    box-shadow: 0 6px 0 0 rgba(0, 0, 0, 0.247);
}

.goldframe3 {
    width: 100%;
    height: 100%;
    /* black → accent colour → black (stock gold when no colour is set) */
    background: linear-gradient(to bottom,
            black 0%,
            var(--accent-deep, #ac8f0d) 10%,
            var(--accent-deep, #ac8f0d) 30%,
            var(--accent, #d0d20d) 70%,
            var(--accent, #d0d20d) 90%,
            black 100%);
    padding-left: 4px;
    padding-right: 4px;
    border-radius: 1px;
}

/* `contain`, not `cover`. The frame is wide and short while the card art is
   square, so cover crops the subject's head and feet off — which is fatal when
   the subject is a single figure. Contain shows the whole thing and lets the
   white studio background of the art meet the frame, so the letterboxing is
   invisible rather than looking like a mistake. */
/* <picture> is an inline wrapper with no box of its own, so without this the
   img inside it sizes against the line box instead of the frame. */
.goldframe3 picture {
    display: block;
    width: 100%;
    height: 100%;
}

.goldframe3 img {
    background-color: var(--surface);
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* ===========================
   TEXT ON SURFACES
   ===========================

   One face throughout. What changes is the fill and the colour of the edge
   around it, and both are read off whatever the text is standing on — the
   same trick the Mario Kart Wii menus play, where a card title and the "Main
   Menu" label share a typeface and share nothing else.

   Over card art the text is white with a hard black edge, because the art
   underneath is arbitrary and only black is guaranteed to separate from it.
   On the flat panels of the multiplayer screens it inverts: dark ink with a
   white edge, which lifts the letters off a panel that is itself translucent
   over the page background, without shouting the way white-on-black would in
   a place with no photo to compete with.

   Set --edge and --edge-w per surface; the recipe below does the rest.
   Eight directions rather than -webkit-text-stroke, which is centred on the
   glyph path and eats into the letterform — this sits entirely outside it. */

.cardtitle,
.cardtitle2,
.mp-title,
.mp-lobby-title,
.mp-row-title {
    text-shadow:
        calc(var(--edge-w) * -1) calc(var(--edge-w) * -1) 0 var(--edge),
        var(--edge-w) calc(var(--edge-w) * -1) 0 var(--edge),
        calc(var(--edge-w) * -1) var(--edge-w) 0 var(--edge),
        var(--edge-w) var(--edge-w) 0 var(--edge),
        calc(var(--edge-w) * -1) 0 0 var(--edge),
        var(--edge-w) 0 0 var(--edge),
        0 calc(var(--edge-w) * -1) 0 var(--edge),
        0 var(--edge-w) 0 var(--edge);
}

/* ===========================
   CARD TITLES
   =========================== */

.cardtitle {
    /* Over card art: white on hard black, the heaviest treatment there is. */
    --edge: #000;
    --edge-w: 3px;
    font-family: var(--game-font);
    font-weight: bold;
    font-size: clamp(25.6px, 18.4cqw, 88px);
    color: rgb(255, 255, 255);
    text-align: center;
    border-radius: 7px;
    position: absolute;
    top: -7px;
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    z-index: 3;
}

.cardtitle2 {
    /* The echo behind the title: same colours, wider edge, so it reads as the
       same words further away rather than as a second label. */
    --edge: #000;
    --edge-w: 5px;
    font-family: var(--game-font);
    font-weight: bold;
    font-size: clamp(22.4px, 16cqw, 76.8px);
    color: rgba(255, 255, 255, 0.5);
    text-align: center;
    border-radius: 7px;
    opacity: 0.3;
    white-space: nowrap;
    position: absolute;
    bottom: -5px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1;
}

/* The six main-menu cards (Single Player, Multiplayer, Settings, WikiChannel,
   Shop, About) keep their original face and size rather than following
   --game-font: every other card screen (mode-select, singleplayer bars) uses
   the same .cardtitle class (the bars now re-apply Arial themselves, see
   .card-bars .cardtitle), so the revert is scoped to .main-menu
   specifically — see the isMainMenu flag in renderCardScreen(), script.js. */
.main-menu .cardtitle,
.main-menu .cardtitle2 {
    font-family: Arial, sans-serif;
}
.main-menu .cardtitle { font-size: clamp(16px, 11.5cqw, 55px); }
.main-menu .cardtitle2 { font-size: clamp(14px, 10cqw, 48px); }

/* ===========================
   CARD BARS
   ===========================

   The stacked variant of the same card: a menu laid down a column instead of
   across a grid, with the art half taken out. Used by the training
   difficulties and by every card menu below the main one — only the main menu
   keeps the card shape, because only its cards carry art for that shape to be
   built around.

   Everything here overrides the shared card styling above rather than forking
   it, so the colours and the title treatment keep working as they change.
   createCard() in script.js drops .square, .cardtitle2 and .goldframe from the
   markup; what is left is layout. */

.card-bars {
    display: flex;
    flex-direction: column;
    gap: 16px;
    width: min(720px, 92vw);
    margin: 0 auto;
}

.card-bars .glass {
    /* .glass is a 13/8 card by default; these are bars, so the ratio goes and
       a height takes over. */
    aspect-ratio: auto;
    height: 88px;
    padding: 6px 10px;
}

/* The card lift is scale(1.1). On a bar that is 720px wide that throws the
   edges outside the column and under the neighbours; a smaller lift reads the
   same at this width. */
.card-bars .glass:hover {
    transform: scale(1.03);
}

/* The gradient's mask carves a window out of the middle so the colour reads as
   a frame around the card art. There is no art here, so the mask would just be
   a hole down the middle of the bar - drop it and let the colour fill. */
.card-bars .blue-card::before {
    -webkit-mask: none;
    mask: none;
}

.card-bars .chess {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* .cardtitle is sized in cqw - a percentage of the container's own width - and
   pinned to the top edge. A bar is two to three times wider than a card, so
   the card's multiplier runs straight into the clamp ceiling and every title
   comes out at the maximum; a smaller one lands back inside the range. The
   pinning goes too, and the flex centring above places it instead. */
.card-bars .cardtitle {
    position: static;
    transform: none;
    /* Arial like the main-menu cards and the Multiplayer rows (.mp-row-title),
       not --game-font: the bars are reached from those, so the face should
       carry through rather than flip to Mono one screen in. */
    font-family: Arial, sans-serif;
    font-size: clamp(32px, 6.72cqw, 54.4px);
}

@media (max-width: 560px) {
    .card-bars {
        gap: 12px;
    }

    .card-bars .glass {
        height: 72px;
    }
}

/* ===========================
   SITE LOGO (TEMPORARY)
   =========================== */

/* Placeholder wordmark until a real one exists — "WikiDerby.io" set nearly the
   full height of the header bar it sits in. Mixed case, not uppercased: the
   camel hump in WikiDerby is what makes the two words readable at a glance.
   Same face and weight as .cardtitle (the menu tiles), just italic, so it reads
   as part of the same set rather than as a pasted-on logo.

   --logo-sx / --logo-sy squash the wordmark without changing its type size, so
   it can be made to fit a band it would otherwise overflow while staying just
   as legible on a stream. Tune them live with ?logotune=1 (public/logo-tuner.js)
   and paste the numbers back here — they are plain multipliers, 1 = untouched. */
/* Fixed to the viewport rather than parented to the header, because the header
   leaves during a race and would take the wordmark with it. The element spans
   the header band (same height, so `align-items: center` reproduces the old
   `top: 50%` centring) and every media query that resizes the bar resizes this
   to match. z-index sits above the article stage (300) and the retreating
   chrome (340), but far below the modal range (900+) so dialogs still cover it. */
.site-logo {
    position: fixed;
    top: 0;
    left: 26px;
    height: 90px;
    display: flex;
    align-items: center;
    /* The bar used to clip this; keep doing so now that it stands alone. */
    overflow: hidden;
    z-index: 341;
    pointer-events: none;
    user-select: none;
    white-space: nowrap;
    /* Arial, not --game-font: the wordmark keeps its original face, sized
       back to the 72px --logo-sx/--logo-sy were measured against (see the
       comment on --logo-clear above). It also opts out of the house
       uppercase rule — unlike the cards/row buttons, which get that for
       free from the UA stylesheet's button { text-transform: none }, this
       is a plain div, so "WikiDerby.io" needs it spelled out here. */
    font-family: Arial, sans-serif;
    font-weight: bold;
    font-style: italic;
    font-size: 72px;
    text-transform: none;
    line-height: 1;
    /* Squash factors, overridable at runtime by the tuner. Tuned against a live
       race rather than the menu, which is the view that has to hold up. */
    --logo-sx: 0.63;
    --logo-sy: 0.81;
    /* Slid sideways by the back-button rule below rather than by overwriting
       `transform`, so the squash and the slide can coexist. */
    --logo-shift: 0px;
    transform: translateX(var(--logo-shift)) scale(var(--logo-sx), var(--logo-sy));
    /* Squash toward the left edge so the wordmark stays pinned to its margin
       instead of drifting inward as it narrows. */
    transform-origin: left center;
    color: var(--accent, #d0d20d);
    /* Eight-direction shadow, the same outline recipe as .cardtitle. Preferred
       over -webkit-text-stroke here: stroke is centred on the path and eats
       into thin cursive strokes, while this sits entirely outside the glyph.

       The outline colour is NOT fixed at black. The glyphs are painted in the
       player's accent, and a black accent made the fill and the outline the
       same colour: the wordmark collapsed into a silhouette, and on midnight's
       near-black page it vanished completely. ColorTheme.apply() flips this to
       a light outline when the accent is too dark to carry a black one. */
    text-shadow:
        -3px -3px 0 var(--logo-outline, #000),
        3px -3px 0 var(--logo-outline, #000),
        -3px 3px 0 var(--logo-outline, #000),
        3px 3px 0 var(--logo-outline, #000),
        -3px 0 0 var(--logo-outline, #000),
        3px 0 0 var(--logo-outline, #000),
        0 -3px 0 var(--logo-outline, #000),
        0 3px 0 var(--logo-outline, #000);
    transition: transform 120ms ease;
}

/* Back button occupies the same corner once there's history to go back to.
   Slide rather than hide — a wordmark that vanishes on navigation reads as a
   bug. 78px clears the button's 26px offset plus its 64px width.
   Matched via :has() rather than the old sibling combinator, because the two
   no longer share a parent — the wordmark moved out of the header.
   Excluded during a race: the button stays flagged visible while the header
   carries it off-screen, and without the guard the wordmark would shuffle
   sideways to clear something the player can no longer see. */
body:not(.wiki-reading):has(.back-button.is-visible) .site-logo {
    --logo-shift: 78px;
}

/* Race start: the chrome leaves, the wordmark stays — at full size, through
   the part of the session anyone actually broadcasts. It deliberately does NOT
   shrink here: a mark that resizes the moment the race begins reads as smaller
   in every frame that gets streamed, which is backwards. Use --logo-sx /
   --logo-sy to fit it instead; squashing keeps the type size, and with it the
   legibility, that shrinking throws away.
   The route is anchored to y=0 and would run straight under it, so the route
   keeps its hands off this corner — see --logo-clear below. */

/* Rally is the one mode whose route is long enough to want the whole top band
   (a start, up to six stops and a target, wrapped into rows), so there the
   wordmark leaves with the header instead of staying as the streaming credit,
   and the route takes back the corner it was keeping clear. Keyed off the
   route's own snake class rather than a body flag, so it can never outlive the
   route: no cleanup to forget on any of the ways out of a match. */
body.wiki-reading:has(.training-route--snake) .site-logo { display: none; }
body.wiki-reading:has(.training-route--snake) { --logo-clear: 18px; }

/* And it takes the top band back: no wordmark row to sit under below 1100px,
   and only a hairline of padding, so the article starts as high as it can.
   The single row (start, stops, target) shares the width instead of wrapping,
   each pill shrinking to its share and clipping long titles with an ellipsis. */
body.wiki-reading .training-route.training-route--snake {
    margin-top: 0;
    padding: 3px 10px 2px;
}
body.wiki-reading .training-route.training-route--snake .route-snake-row--one {
    width: 100%;
    min-width: 0;
    gap: 6px;
    align-items: flex-start;
}
/* Elastic: each node starts at the width of its own title (flex-basis auto)
   and, when the row is too wide, gives up width in proportion to that — so
   short titles stay snug and long ones wrap onto a second line instead of
   the row ever scrolling sideways. */
body.wiki-reading .route-snake-row--one > .route-pill,
body.wiki-reading .route-snake-row--one > .route-target-wrap {
    flex: 0 1 auto;
    min-width: auto;
}

body.wiki-reading .route-snake-row--one .route-pill {
    min-width: 0;
    max-width: none;
    /* Overrides the shared .route-pill/.route-target-wrap { flex: 1 1 0 }
       above — that rule is for balancing the two sides' widths, but letting
       the pill itself shrink squeezes a long nowrap title past its own
       border instead of the row just scrolling (.training-route--snake is
       the scroll container). */
    /* Titles keep their own width (up to 18em, then they wrap) so that when
       the row is tight it is the descriptions that give up width, not the
       titles. */
    flex-shrink: 1;
    white-space: normal;
    width: max-content;
    max-width: 18em;
    overflow-wrap: anywhere;
    padding: 3px 8px;
    font-size: clamp(12px, 1.35vw, 22px);
}
body.wiki-reading .route-snake-row--one .route-target-desc {
    /* Three lines at most: setRouteDesc (multiplayer-screen.js) measures the
       narrowest width that fits and leaves it in --desc-w. The box above
       stretches to match (see .route-pill below), so a long description makes
       its box wider rather than taller. */
    width: auto;
    min-width: 0;
    max-width: var(--desc-w, 26em);
    font-size: clamp(10px, 0.9vw, 13px);
}
/* The start has no description under it and nothing to read from it, so it is
   the one that gives up width first: it wraps to two lines before any
   description has to squeeze taller. */
body.wiki-reading .route-snake-row--one > .route-pill {
    flex-shrink: 2;
    width: auto;
    min-width: auto;
    overflow-wrap: normal;
}
/* Fills its wrap, which is as wide as the wider of title and description. */
body.wiki-reading .route-snake-row--one .route-target-wrap > .route-pill {
    min-width: 100%;
}
/* Too narrow even for the titles alone: now they give up width and wrap too,
   rather than the row scrolling. */
@media (max-width: 900px) {
    body.wiki-reading .route-snake-row--one > .route-pill,
    body.wiki-reading .route-snake-row--one > .route-target-wrap {
        flex-shrink: 1;
        min-width: 0;
    }
    body.wiki-reading .route-snake-row--one .route-pill {
        width: auto;
        max-width: 100%;
    }
}
body.wiki-reading .route-snake-row--one .route-to {
    flex: 0 0 auto;
    padding-top: 2px;
    font-size: clamp(16px, 1.8vw, 26px);
}

@media (max-height: 820px) {
    .site-logo { height: 60px; font-size: 48px; }
}

@media (max-height: 620px) {
    .site-logo { height: 46px; font-size: 36px; }
}

@media (max-width: 480px) {
    .site-logo { height: 50px; font-size: 30px; left: 16px; }
}

/* Floats top-left on its own since the header band went (2026-09-28), level
   with the wordmark. z-index 101 is what the band used to give it. */
.back-button {
    appearance: none;
    position: fixed;
    top: calc(var(--chrome-h) / 2);
    left: 26px;
    width: 64px;
    height: 54px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transform: translateY(-50%) translateX(-18px);
    transition: opacity 120ms ease, transform 120ms ease;
    z-index: 101;
}

/* A race has no back button: it used to leave with the header, and the
   article owns the top of the screen until the race ends. */
body.wiki-reading .back-button { display: none; }

.back-button:disabled {
    cursor: default;
}

.back-button::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 22px;
    width: 24px;
    height: 24px;
    border-left: 7px solid #1f1f1f;
    border-bottom: 7px solid #1f1f1f;
    transform: translateY(-50%) rotate(45deg);
    
}

.back-button.is-visible {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(-50%) translateX(0);
}

.back-button:hover::before {
    border-color: #0032ff;
}

/* ===========================
   PROFILE BUTTON
   =========================== */

/* Profile: a polaroid pinned to the top-right corner, the prototype's
   player card (version3). Offsets are fractions of img/pfp-polaroid.png
   (1029x1176 before the downscale to 480 wide): the photo window is
   x 84..946, y 84..921 and is see-through, so the frame is drawn OVER the
   picture; the white strip under it runs to y 1172. */
.profile-btn {
    appearance: none;
    font: inherit;
    position: fixed;
    top: 16px;
    right: 16px;
    width: 150px;
    aspect-ratio: 1029 / 1176;
    container-type: inline-size;
    background: none;
    
    padding: 0;
    border: 0;
    border-radius: 0;
    cursor: pointer;
    z-index: 9999;
    will-change: transform;
    transition: transform 0.2s ease;
    transform-origin: center;
}

.profile-btn:hover {
    z-index: 10000;
    transform: scale(1.08);
}

@media (max-width: 540px), (max-height: 540px) {
    .profile-btn { width: 100px; }
}

.profile-card {
    position: absolute;
    inset: 0;
}

.profile-card::after {
    content: "";
    position: absolute;
    inset: 0;
    background: url('/img/pfp-polaroid.png') center / 100% 100% no-repeat;
    pointer-events: none;
}

/* The picture sits under the photo window, a hair bigger so no gap shows
   at the frame's inner edge. */
.profile-btn .profile-avatar {
    position: absolute;
    left: 7.9%;
    top: 6.9%;
    width: 84.3%;
    height: 71.7%;
    border-radius: 0;
    background-color: #000;
    background-size: cover;
    background-position: center;
    box-shadow: none;
}

/* Handwritten on the strip under the photo. */
.profile-card-name {
    position: absolute;
    z-index: 1;
    left: 7.9%;
    right: 7.9%;
    top: 79%;
    height: 19.5%;
    display: grid;
    place-items: center;
    overflow: hidden;
    white-space: nowrap;
    font: 18cqw/1 'Embolism Spark', cursive;
    color: #1b1520;
}

/* The profile modal still uses the avatar square this card used to have. */
.profile-avatar {
    width: auto;
    height: 100%;
    aspect-ratio: 1 / 1;
    flex-shrink: 0;
    border-radius: 9px;
    display: block;
    background-color: var(--surface);
    background-size: cover;
    background-position: center;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}

.profile-card-info {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 3px;
    min-width: 0;
    overflow: hidden;
}

/* ===========================
   DISCORD BUTTON
   =========================== */

/* The profile card's twin in the opposite corner: same mini-card language
   (radius, accent border, blur, hover lift), smaller because it is a secondary
   action and shouldn't compete with the profile for attention.
   Sized/placed here rather than in JS — unlike .profile-btn there is nothing
   dynamic about it, so it costs no script and can't flash unpositioned. */
.discord-btn {
    position: fixed;
    width: 64px;
    height: 64px;
    right: 44px;
    /* Clears the 90px fixed footer. Both values track the footer's own
       height breakpoints below — change one, change the other. */
    bottom: 108px;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    z-index: 9999;
    will-change: transform;
    transition: transform 0.2s ease;
    transform-origin: center;
    -webkit-tap-highlight-color: transparent;
}

.discord-btn:hover {
    z-index: 10000;
    transform: scale(1.08);
}

.discord-btn:focus-visible {
    outline: 3px solid var(--accent, #d0d20d);
    outline-offset: 4px;
    border-radius: 14px;
}

.discord-card {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    padding: 12px;
    border-radius: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--theme-card, rgba(255, 255, 255, 0.72));
    border: 3px solid var(--accent, #d0d20d);
    box-shadow:
        0 4px 16px rgba(0, 0, 0, 0.28),
        inset 0 0 0 2px rgba(255, 255, 255, 0.65);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}

.discord-logo {
    width: 100%;
    height: auto;
    display: block;
    /* Takes the theme accent rather than Discord blurple, so the mark recolours
       with the rest of the UI. The <path> is fill="currentColor", which is what
       lets a single colour property drive it. */
    color: var(--accent, #d0d20d);
}

@media (max-height: 820px) {
    .discord-btn { bottom: 78px; }
    .notif-layer { bottom: 158px; }
}

@media (max-height: 620px) {
    .discord-btn {
        bottom: 60px;
        width: 54px;
        height: 54px;
    }
    /* Clears the shrunken button (60 + 54 + air) — and the cards themselves get
       shorter here, because at this height a 300px column would fill the screen. */
    .notif-layer { bottom: 130px; }
    .notif-card { width: 260px; }
}

/* The footer also shrinks to 50px on narrow screens — a WIDTH query, so it can
   apply on a tall phone where neither height query above does. Must stay last
   of the three: same specificity, so source order is what decides when a small
   phone matches both this and a max-height rule. */
@media (max-width: 480px) {
    .discord-btn { bottom: 68px; }
    /* The Discord button is already hidden by the 768px rule below, so this
       only has to clear the 50px footer. */
    .notif-layer { right: 16px; bottom: 66px; }
}

/* ===========================
   FRIENDS BUTTON + PANEL
   =========================== */

/* ── The panel ──
   Friends used to be its own corner button beside the profile card; it is now
   one more item in the profile dropdown (see .profile-menu-item in the
   PROFILE MENU & BADGES section, and ProfileMenu.toggle() in script.js), so
   only the panel itself is left to style here. */

.friends-panel {
    position: fixed;
    /* Below the PROFILE card (y 56-156), which is the only thing left in this
       corner and the only thing this panel is ever opened from now. */
    top: 164px;
    right: 44px;
    width: 300px;
    max-height: min(60vh, 460px);
    display: flex;
    flex-direction: column;
    z-index: 10001;
    box-sizing: border-box;
    padding: 14px;
    border-radius: 16px;
    background: var(--theme-card, rgba(255, 255, 255, 0.92));
    border: 3px solid var(--accent, #d0d20d);
    box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    color: var(--ink, #16161d);
}

.friends-panel[hidden] { display: none; }

.friends-panel-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 10px;
}

.friends-panel-title { font-size: 15px; font-weight: 800; letter-spacing: 0.02em; }
.friends-panel-sub { font-size: 11px; opacity: 0.68; }

.friends-add { display: flex; gap: 6px; margin-bottom: 10px; }

.friends-add input {
    flex: 1 1 auto;
    min-width: 0;
    font: inherit;
    font-size: 13px;
    padding: 6px 8px;
    border-radius: 9px;
    border: 2px solid rgba(0, 0, 0, 0.18);
    background: var(--surface, #fff);
    color: inherit;
}

.friends-add input:focus { outline: 2px solid var(--accent, #d0d20d); outline-offset: 1px; }

.friends-btn-sm {
    font: inherit;
    font-size: 12px;
    font-weight: 700;
    padding: 6px 10px;
    border-radius: 9px;
    border: 2px solid var(--accent, #d0d20d);
    background: var(--accent, #d0d20d);
    color: var(--ink, #16161d);
    cursor: pointer;
    white-space: nowrap;
}

.friends-btn-sm.is-quiet { background: transparent; border-color: rgba(0, 0, 0, 0.2); }
.friends-btn-sm:hover { filter: brightness(1.06); }
.friends-btn-sm:disabled { opacity: 0.5; cursor: default; }

/* The list is the only part that scrolls — the add box and the heading stay put
   however many friends there are. */
.friends-scroll { overflow-y: auto; overflow-x: hidden; margin: 0 -4px; padding: 0 4px; }

.friends-section {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    opacity: 0.6;
    margin: 8px 0 4px;
}

.friends-row {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 5px 0;
    border-top: 1px solid rgba(0, 0, 0, 0.08);
}

.friends-row:first-of-type { border-top: 0; }

.friends-dot {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.22);
}

.friends-dot.is-online { background: #3fbf5f; box-shadow: 0 0 0 2px rgba(63, 191, 95, 0.25); }

.friends-name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 13px;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.friends-row .friends-btn-sm { font-size: 11px; padding: 4px 8px; }

.friends-empty { font-size: 12px; opacity: 0.7; line-height: 1.45; padding: 6px 0; }

.friends-note { font-size: 11.5px; line-height: 1.45; opacity: 0.8; }

.friends-error { font-size: 12px; color: #c0392b; font-weight: 600; padding: 4px 0 0; }

/* Narrow screens have no room for a 300px dropdown-anchored panel, so it
   becomes a sheet instead — pinned to the bottom edge rather than hung off the
   profile card. The trigger (the profile menu's Friends item) does not move;
   only where the panel it opens lands does. */
@media (max-width: 720px) {
    .friends-panel {
        top: auto;
        right: 12px;
        left: 12px;
        bottom: 12px;
        width: auto;
        max-height: min(70vh, 460px);
    }
}

/* A race takes the whole screen and the corner furniture gets out of the way —
   the same rule the profile and Discord buttons follow. */
body.wiki-reading .friends-panel { display: none; }

/* ===========================
   TRAINING MODE
   =========================== */

/* The user-select: auto that used to be here is gone with the blanket rule it
   was fighting — training was the one screen that opted back into selectable
   text, and now every screen is selectable by default. */
.training-container {
    display: block;
    max-width: min(1180px, calc(100vw - 40px));
    margin: 0 auto;
}

.training-screen {
    width: 100%;
    padding: 16px 0 120px;
    color: var(--ink);
    will-change: transform, opacity;
}

.training-route {
    position: sticky;
    top: 106px;
    z-index: 20;
    /* Grid rather than a centered flex row: the "TO" is the one thing that
       has to sit on the container's true midpoint, whatever width the start
       and target pills end up at (a long title on one side alone used to
       drag the whole centered block, "TO" included, off to that side). Bare
       `1fr` tracks still take their minimum from their content, so a wide
       target (a long title, or its gloss underneath) would out-grow the
       start column and pull "auto" off-centre again — minmax(0, ) drops that
       floor so both tracks split the leftover space exactly evenly and a
       pill wider than its half just overflows into the gap (route-pill is
       overflow: visible for exactly this) instead of resizing the track. */
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: 18px;
    width: min(100%, 980px);
    min-height: 74px;
    margin: 0 auto 24px;
    padding: 10px 18px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow:
        0 6px 0 rgba(0, 0, 0, 0.24),
        inset 0 0 0 3px rgba(176, 176, 255, 0.55);
    /* Fully Random can draw a title wide enough that the row no longer fits
       between the panel edges. The pills stay one line and grow sideways
       (see .route-pill) instead of cropping, so the row itself has to be
       able to scroll sideways to still fit at the top of the screen. */
    overflow-x: auto;
    overflow-y: visible;
}

/* Anchored to the row's top edge rather than centered: the target's gloss
   underneath it makes .route-target-wrap taller than the bare start pill, and
   centering both by height sat the target pill higher than the start pill —
   they only land level with each other when both start from the same edge. */
.training-route > .route-pill {
    justify-self: end;
    align-self: start;
}

.training-route > .route-target-wrap {
    justify-self: start;
    align-self: start;
}

.training-route--snake {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    width: min(100%, 1100px);
    overflow-x: auto;
    overflow-y: visible;
}

.route-snake-row {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 10px;
    min-width: max-content;
}

.route-snake-row .route-pill {
    min-width: 180px;
    font-size: 24px;
}

.route-snake-row .route-target-desc {
    max-width: 260px;
    font-size: 15px;
}

.route-snake-row .route-to {
    padding-top: 8px;
    font-size: 34px;
    line-height: 1;
}

.route-snake-row .route-to--down {
    padding-top: 6px;
    color: #0032ff;
}

.route-pill {
    min-width: 180px;
    padding: 8px 14px;
    border: 2px solid #101010;
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink-link);
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: bold;
    line-height: 1.1;
    text-align: center;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.16);
    /* Fully Random draws whatever the feed returns, dead-ends and all, so
       titles here have no length cap unlike the curated tiers. No max-width
       and no ellipsis: the pill grows sideways to fit the title on one line
       instead of cropping it. .training-route scrolls horizontally (see
       below) for when that line is wider than the row. */
    flex-shrink: 0;
    overflow: visible;
    white-space: nowrap;
}

.route-pill.is-empty {
    color: rgba(0, 0, 0, 0.38);
}

/* ── Reveal: the multiplayer article roulette ──
   The start/target pills used to just appear with the room's draw already
   written in them. Now they spin through filler titles first (one swap per
   .is-spinning class re-add, driven from JS — see spinMpPill) and pop when
   the real title lands, so "the room is about to race THIS" reads as an
   event instead of a fact you had to notice on your own. */
.route-pill.is-spinning {
    animation: mpPillTick 55ms linear;
    color: var(--ink-link);
}

.route-pill.is-locked {
    animation: mpPillLock 420ms ease-out;
}

@keyframes mpPillTick {
    from { transform: translateY(-6px); opacity: 0.4; }
    to { transform: translateY(0); opacity: 1; }
}

@keyframes mpPillLock {
    0% { transform: scale(1); }
    45% { transform: scale(1.08); }
    100% { transform: scale(1); }
}

.route-cursor-dock {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 4px;
    width: 100%;
    min-height: 28px;
}

.route-cursor-mark {
    display: inline-flex;
    width: 24px;
    height: 24px;
    align-items: center;
    justify-content: center;
}

.route-cursor-mark .mp-rival-cursor {
    margin: 0;
}

/* The plain-arrow fallback cosmeticCursorIcon draws for an unresolved cursor
   claim (see script.js's defaultCursorIconSvg) — every guest races with this
   by default, wherever a racer's cursor is drawn. */
.mp-default-cursor {
    display: block;
    
}

/* ── Rally: the waypoint ──
   Smaller than the two ends, because it is a place you pass through rather
   than a place the race is between — and it goes green once stamped, which is
   the only feedback the header ever gives. */
.route-pill--via {
    min-width: 180px;
    font-size: 20px;
    padding: 6px 12px;
}

.route-pill--via.is-stamped {
    border-color: #2e7d32;
    color: #2e7d32;
    background: color-mix(in srgb, #2e7d32 10%, var(--surface));
}

/* The target while the waypoint is still unstamped. Dimmed rather than hidden:
   you are allowed to walk over it, it just will not end anything yet. */
.route-pill.is-shut {
    opacity: 0.5;
    border-style: dashed;
}

.route-target-wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}

.route-target-desc {
    max-width: 360px;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-style: italic;
    color: var(--ink-muted);
    text-align: center;
    line-height: 1.35;
}

.route-target-desc--via {
    max-width: 260px;
    font-size: 15px;
}

.route-to {
    color: var(--ink-strong);
    font-family: var(--game-font);
    font-size: 34px;
    font-weight: bold;
    letter-spacing: 0;
    text-shadow: 2px 2px 0 rgba(176, 176, 255, 0.9);
}

/* ── Fitting an oversized title (see TrainingUI.fitRoute in script.js) ──
   A title long enough to draw a pill wider than its half of the row used to
   just make the row scroll sideways, no matter how it got there. These three
   escalate through what the row can give up before that ever happens again:
   the wordmark's corner, then the "TO" arrow and its two gaps, then finally
   letting the title itself wrap to a second line. */
body.wiki-reading:has(.training-route.is-crowded) .site-logo {
    display: none;
}

.training-route.is-tight {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

.training-route.is-tight > .route-to {
    display: none;
}

.training-route.is-wrapped .route-pill {
    white-space: normal;
    overflow-wrap: anywhere;
}

.roulette-area {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 280px;
    margin: 16px auto 24px;
}

.roulette-area[hidden],
.article-stage[hidden] {
    display: none;
}

.roulette-card {
    display: grid;
    place-items: center;
    width: min(760px, 100%);
    min-height: 240px;
    padding: 28px;
    border: 5px solid #f6d83f;
    border-radius: 8px;
    background:
        linear-gradient(135deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0)),
        #55129a;
    box-shadow:
        0 8px 0 rgba(0, 0, 0, 0.36),
        inset 0 0 0 4px rgba(0, 0, 0, 0.22);
    color: #ffe45c;
    font-family: var(--game-font);
    text-align: center;
}

.roulette-label {
    margin-bottom: 18px;
    color: #fff;
    font-size: 24px;
    font-weight: bold;
    text-shadow: 2px 2px 0 #000;
}

.roulette-title {
    min-height: 76px;
    color: #ffe45c;
    font-size: 58px;
    font-weight: bold;
    line-height: 1.05;
    text-shadow:
        -3px -3px 0 #000,
        3px -3px 0 #000,
        -3px 3px 0 #000,
        3px 3px 0 #000;
}

.roulette-title.is-ticking {
    animation: rouletteTick 55ms linear;
}

.roulette-title.is-selected {
    animation: rouletteSelected 420ms ease-out;
}

@keyframes rouletteTick {
    from {
        transform: translateY(-10px);
        opacity: 0.35;
    }

    to {
        transform: translateY(0);
        opacity: 1;
    }
}

@keyframes rouletteSelected {
    0% {
        transform: scale(1);
    }

    45% {
        transform: scale(1.09);
    }

    100% {
        transform: scale(1);
    }
}

/* The only way out of a race once the header has retreated. Kept quiet: it is a
   permanent fixture on screen during every race, including the frames a
   streamer broadcasts, so it reads as chrome rather than as a call to action. */
.training-quit {
    padding: 5px 12px;
    border: 2px solid rgba(0, 0, 0, 0.55);
    border-radius: 6px;
    background: color-mix(in srgb, var(--surface) 70%, transparent);
    color: var(--ink);
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: 700;
    cursor: pointer;
}

.training-quit:hover {
    background: #ffd9d9;
    border-color: #000;
}

.training-progress {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    width: min(100%, 980px);
    min-height: 52px;
    margin: 0 auto 14px;
    padding: 10px 14px;
    border: 2px solid rgba(0, 0, 0, 0.34);
    border-radius: 7px;
    background: color-mix(in srgb, var(--surface) 88%, transparent);
    font-family: var(--game-font);
    font-size: 28.8px;
}

.progress-trail {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
    color: var(--ink-link);
    font-weight: bold;
}

.progress-step {
    white-space: nowrap;
}

.article-stage {
    width: min(100%, 980px);
    margin: 0 auto;
}

/* Colour theme tokens (light default; dark + auto below). */
.article-stage,
.rc-scroll {
    --wiki-bg: #fff;
    --wiki-fg: #202122;          /* --color-base */
    --wiki-link: #36c;           /* --color-progressive */
    --wiki-border: #a2a9b1;      /* --border-color-base */
    --wiki-input-border: #72777d;
    --wiki-soft: #f8f9fa;        /* --background-color-neutral-subtle */
    --wiki-th: #eaecf0;          /* --background-color-neutral */
    --wiki-muted: #54595d;       /* --color-subtle */
    --wiki-burnt: #3a2f27;       /* charred link (Arson) */
    --wiki-banned: #c92a2a;      /* room-banned link */
}

/* Wikipedia "night" (skin-theme-clientpref-night) tokens. */
.article-stage[data-wiki-color="dark"],
.rc-scroll[data-wiki-color="dark"] {
    --wiki-bg: #101418;
    --wiki-fg: #eaecf0;
    --wiki-link: #88a3e8;
    --wiki-border: #54595d;
    --wiki-input-border: #72777d;
    --wiki-soft: #27292d;
    --wiki-th: #2a2d31;
    --wiki-muted: #a2a9b1;
    --wiki-burnt: #8d7f72;
    --wiki-banned: #ff8787;
}

@media (prefers-color-scheme: dark) {
    .article-stage[data-wiki-color="auto"],
    .rc-scroll[data-wiki-color="auto"] {
        --wiki-bg: #101418;
        --wiki-fg: #eaecf0;
        --wiki-link: #88a3e8;
        --wiki-border: #54595d;
        --wiki-input-border: #72777d;
        --wiki-soft: #27292d;
        --wiki-th: #2a2d31;
        --wiki-muted: #a2a9b1;
        --wiki-burnt: #8d7f72;
        --wiki-banned: #ff8787;
    }
}

/* ── Reading an article: it becomes the whole page, in every mode ──
   Fixed layer below the game header (so the header's Back button still works);
   the game HUD (route, racers, moves) floats on top. */

body.wiki-reading .profile-btn,
body.wiki-reading .notif-layer,
body.wiki-reading .discord-btn { display: none; }

/* Drop the will-change containing block (it would trap the fixed article layer
   inside the mode screen) and strip the width caps between screen and article. */
body.wiki-reading .multiplayer-screen,
body.wiki-reading .training-screen,
body.wiki-reading .custom-screen,
body.wiki-reading .challenges-screen { will-change: auto; }
body.wiki-reading .training-container,
body.wiki-reading .grid-container { transform: none; }
body.wiki-reading .multiplayer-container { max-width: none; margin: 0; }
body.wiki-reading > main { max-width: none; width: 100%; padding: 0; }
body.wiki-reading .menu-stage { padding: 0; }
body.wiki-reading .mp-game { position: static; width: 100%; max-width: none; margin: 0; padding: 0; }
body.wiki-reading .mp-game-body { display: block; }

body.wiki-reading .article-stage {
    position: fixed;
    /* Full-bleed: the header and footer retreat when a race starts (see
       "Race start" below), so the article owns the whole viewport. */
    inset: 0;
    z-index: 300;
    width: 100vw;
    max-width: none;
    margin: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--wiki-bg);
}

/* The race HUD: two article pills and the arrow between them, flush to the top
   of the screen, and nothing else.

   Flush to y=0 — that band belongs to the route, which is why .site-logo now
   leaves for the duration of a race.

   No banner: the frame the pill wears everywhere else — border, gradient fill,
   inset outline, drop shadow — and its 74px floor all come off, so what sits on
   the article is the three things the player is actually reading.

   This is deliberately written for BOTH of the route's homes, so the two are
   indistinguishable. The element is stowed out of the stage and re-adopted on
   every single move (its parent's innerHTML is replaced each time), and any
   difference between the two states shows up as a flicker on every click. */
body.wiki-reading .training-route {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 320;
    box-sizing: border-box;
    width: 100%;
    max-width: none;
    min-height: 0;
    margin: 0;
    /* Symmetric, not padding-left: var(--logo-clear) — the grid below centers
       "TO" on this box, so reserving asymmetric space for the wordmark's
       corner here dragged that center off the screen's actual midpoint. The
       start pill is justify-self: end (hugs the arrow, empty track to its
       left), so on any width wide enough to share this row with the wordmark
       at all it clears the corner without help; the mark is hidden below
       ~1100px, where the route drops to its own line instead (see below). */
    padding: 8px 18px 12px;
    transform: none;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    overflow-x: auto;
    overflow-y: hidden;
}

/* Under ~1100px there is no longer room for both across one line: the pills
   need about 670px between them and the wordmark is eating 314 of it. Below
   that the route stops sharing the row and drops under the wordmark's band
   instead — still the top of the article, just the second line of it. */
@media (max-width: 1100px) {
    body.wiki-reading .training-route {
        margin-top: var(--chrome-h);
    }
}

/* Adopted into the stage, it drops out of the viewport and joins the article's
   flow, so it scrolls away instead of covering the text. Pinned, it sat over
   the first ~190px of every article — exactly where the lead paragraph and its
   first links are — and players said so. The stage is the scroll container (JS
   resets its scrollTop on every move), which is why "scrolls away" has to mean
   "lives in the stage's flow" rather than "let the page scroll". Everything
   else is inherited from the shared rule above. */
body.wiki-reading .article-stage > .training-route {
    position: static;
}

/* The target's one-line gloss ("Sport of shooting arrows"), under the target
   pill. It follows the article's own muted ink rather than the menu's, since
   with the banner gone it sits straight on the page; the fallback covers the
   stowed state, where --wiki-* is out of scope. */
body.wiki-reading .training-route .route-target-desc {
    color: var(--wiki-muted, #54595d);
}

/* The target pill and its gloss are a COLUMN inside the route's row, and that
   is a trap: the narrow-width `.route-pill { flex: 1 1 0 }` — written for the
   row — also matches the pill in here, where the main axis is vertical, so the
   basis resolved against its HEIGHT and collapsed the target to 20px, cutting
   the article name in half. (Live on phones since 8abe22e, under the banner.)
   Pinning the pill to its own content height is what stops that; stretch makes
   it span the column so the gloss centres under a full-width pill instead of a
   text-width one. */
body.wiki-reading .training-route .route-target-wrap {
    align-items: stretch;
}

body.wiki-reading .training-route .route-target-wrap .route-pill {
    flex: 0 0 auto;
}

/* The arrow has no pill behind it, so it sits straight on the article and has
   to follow the article's theme: near-black #000 on Wikipedia's night palette
   was all but invisible. --wiki-fg is the same token the article body uses, so
   the arrow flips with light/dark/auto for free. The lavender drop shadow goes
   with it — it read as an outline against the banner and as mush against a
   dark page. The fallback covers the stowed state, where the route sits outside
   the stage and --wiki-fg is not in scope to inherit. */
body.wiki-reading .training-route .route-to {
    color: var(--wiki-fg, #202122);
    text-shadow: none;
}

body.wiki-reading .training-progress {
    position: fixed;
    bottom: 12px;
    left: 12px;
    /* Shrink to its contents while racing. At the base 980px it stretched most
       of the way across the foot of the article for the sake of one "Moves: 3",
       sitting on top of the wallpaper attribution. */
    width: auto;
    margin: 0;
    padding: 6px 14px;
    border: 2px solid #000;
    border-radius: 8px;
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.2);
    z-index: 320;
}

body.wiki-reading .progress-trail,
body.wiki-reading .roulette-area { display: none; }

body.wiki-reading .mp-side {
    position: fixed;
    bottom: 12px;
    right: 12px;
    width: 220px;
    max-height: 48vh;
    overflow-y: auto;
    z-index: 320;
}

/* Once WikiSkin.placeSide() grafts it into the skin's own grid, it stops being
   a floating card over the article and becomes the rail Appearance used to
   be: sticky, full height, no fixed positioning of its own. Min-width so the
   phone layout below — which takes it out of the grid's flow entirely — still
   wins there regardless of the extra specificity `.wiki-skin >` adds. */
@media (min-width: 769px) {
    body.wiki-reading .wiki-skin > .mp-side {
        position: sticky;
        top: 12px;
        right: auto;
        bottom: auto;
        width: auto;
        max-height: calc(100vh - 120px);
        overflow-y: auto;
        z-index: auto;
    }

    /* The race rail matches the chat column on the left: pinned where the
       skin's top padding rests and exactly one screen tall, so its bottom
       edge sits 16px off the screen. Racers takes whatever the blocks above
       it leave and scrolls its own list. */
    body.wiki-reading .wiki-skin > .mp-side--race {
        top: 74px;
        height: calc(100dvh - 74px - 16px);
        max-height: none;
        overflow: hidden;
    }

    .mp-side--race .mp-side-block--racers {
        flex: 1 1 0;
    }

    .mp-side--race .mp-side-block--racers .mp-rivals {
        flex: 1 1 0;
    }
}

/* The race rail itself never scrolls: Appearance / Item / Backtrack stay put
   and only the Racers list scrolls when it runs out of room. The rail keeps
   its max-height (a flex column), every block but Racers refuses to shrink,
   and Racers gives up the difference to its own list. Plain grey scrollbar,
   not the app's accent-coloured one, since this one sits inside the article
   skin. Below 769px the rail is in normal flow with no cap, so the list
   just grows there. */
body.wiki-reading .mp-side--race,
body.wiki-reading .wiki-skin > .mp-side--race {
    overflow-y: visible;
}

.mp-side--race > .mp-side-block:not(.mp-side-block--racers) { flex-shrink: 0; }

.mp-side--race .mp-side-block--racers {
    display: flex;
    flex-direction: column;
    flex-shrink: 1;
    min-height: 0;
}

.mp-side--race .mp-side-block--racers .mp-rivals {
    min-height: 96px;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: rgba(128, 128, 128, 0.55) transparent;
}

/* ── Wikipedia skin: Contents | article, plus a third rail in multiplayer ──
   Two columns by default (solo reading has nothing to put on the right, now
   that Appearance lives in Settings > Customization). `.has-side` is added by
   WikiSkin.placeSide() the moment it grafts the live Racers/Item/Feed panel
   on — see script.js — which is the only thing that ever fills that column. */
.wiki-skin {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    gap: 0 26px;
    align-items: start;
    min-height: 100%;
    box-sizing: border-box;
    padding: 74px 26px 90px;
    background: var(--wiki-bg);
    color: var(--wiki-fg);
    font-family: sans-serif;
}

.wiki-skin.has-side {
    grid-template-columns: 220px minmax(0, 1fr) 232px;
}

/* The loading/error placeholder between moves: no TOC (there is nothing to
   list yet), so just the one column, plus the side rail when multiplayer has
   one — see WikiSkin.swap() in script.js. Rules come after the ones above so
   they win the tie against `.has-side` alone. */
.wiki-skin--bare {
    grid-template-columns: minmax(0, 1fr);
}

.wiki-skin--bare.has-side {
    grid-template-columns: minmax(0, 1fr) 232px;
}

.wiki-toc {
    position: sticky;
    top: 12px;
    align-self: start;
    font-size: 13px;
    color: var(--wiki-fg);
}

/* The chat rail, grafted under Contents by WikiSkin.placeTocChat — see the
   comment there for why it's a DOM child of .wiki-toc rather than a grid
   position. Docked only where Contents itself shows (desktop, not while
   spectating); everywhere else it keeps the floating bottom-left position,
   which is why the phone and spectating rules further down turn .wiki-toc
   into display: contents instead of hiding it outright. It stays the dark
   box in both places (the look is the floating rule with the race HUD
   below); docking only takes it out of fixed positioning. */
@media (min-width: 769px) {
    /* While a race is on the TOC becomes a column exactly one screen tall
       (the skin's 74px top padding is where it rests, so it pins there instead
       of 12px: the bottom edge then sits 16px off the screen at every scroll
       position), and the chat takes whatever Contents leaves. Contents itself
       scrolls if a long article's headings would squeeze the chat out. */
    body:not(.mp-spying) .wiki-toc:has(> .mp-toc-chat) {
        top: 74px;
        display: flex;
        flex-direction: column;
        height: calc(100dvh - 74px - 16px);
    }

    body:not(.mp-spying) .wiki-toc:has(> .mp-toc-chat) .wiki-toc-body {
        flex: 0 1 auto;
        min-height: 60px;
        overflow-y: auto;
    }

    body:not(.mp-spying) .wiki-toc .mp-toc-chat {
        position: static;
        z-index: auto;
        width: auto;
        max-height: none;
        margin-top: 20px;
        flex: 1 1 0;
        min-height: 140px;
    }

    body:not(.mp-spying) .wiki-toc .mp-toc-chat .mp-chat-log {
        flex: 1 1 0;
        height: auto;
        min-height: 60px;
    }
}

.wiki-toc-head {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--wiki-border);
    font-weight: bold;
}

.wiki-toc-toggle {
    margin-left: auto;
    background: none;
    border: 0;
    padding: 0;
    color: var(--wiki-link);
    font-size: 12px;
    cursor: pointer;
}

.wiki-toc-list,
.wiki-toc-sublist { list-style: none; margin: 0; padding: 0; }

.wiki-toc-row { display: flex; align-items: center; gap: 2px; }

.wiki-toc-link {
    display: block;
    flex: 1 1 auto;
    min-width: 0;
    padding: 4px 8px;
    border: 0;
    border-left: 2px solid transparent;
    background: none;
    color: var(--wiki-link);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.wiki-toc-link:hover {
    border-left-color: var(--wiki-link);
    background: rgba(128, 128, 128, 0.12);
}

.wiki-toc-item.is-top > .wiki-toc-row .wiki-toc-link { font-weight: bold; }
.wiki-toc-item.is-sub .wiki-toc-link { padding-left: 22px; font-size: 12.5px; }

/* The disclosure arrow — Wikipedia's own ">" that opens a section's
   subheadings without navigating anywhere. Collapsed by default: a long
   article's TOC would otherwise run well past the screen, which is what
   used to force a scrollbar onto this column. */
.wiki-toc-arrow {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    border: 0;
    padding: 0;
    background: none;
    color: var(--wiki-muted);
    font-size: 10px;
    line-height: 1;
    cursor: pointer;
    transition: transform 120ms ease;
}

.wiki-toc-arrow::before { content: "▶"; }

.wiki-toc-item.has-children:not(.is-collapsed) > .wiki-toc-row .wiki-toc-arrow {
    transform: rotate(90deg);
}

.wiki-toc-item.is-collapsed > .wiki-toc-sublist { display: none; }

.wiki-skin.is-toc-collapsed .wiki-toc-body { display: none; }

.wiki-page { min-width: 0; }

/* CC BY-SA attribution + non-affiliation line under every article. */
.wiki-attrib {
    margin-top: 34px;
    padding-top: 10px;
    border-top: 1px solid var(--wiki-border);
    font-size: 12px;
    line-height: 1.6;
    color: color-mix(in srgb, var(--wiki-fg) 62%, transparent);
}

.wiki-attrib a {
    color: var(--wiki-link);
    text-decoration: none;
}
/* Width setting, done the way Vector 2022 does it: "Standard" caps and centres
   the WHOLE column layout, so the rail(s) sit next to the article.

   It used to cap only the middle column (max-width: 820px) while the grid track
   itself stayed 1fr — so on a wide screen the track grew to ~1360px, the article
   used 820 of it, and the leftover opened as dead space between the article and
   the rail. Capping the container instead means the middle track is exactly the
   space that's left: no slack. Two figures because the multiplayer side panel
   (`.has-side`) claims a third 232px column that solo reading has no use for. */
.wiki-skin[data-width="standard"] {
    max-width: 1262px;   /* 26 + 220 + 26 + ~964 + 26 */
    margin-inline: auto;
}

.wiki-skin[data-width="standard"].has-side {
    max-width: 1520px;   /* 26 + 220 + 26 + ~964 + 26 + 232 + 26 */
}

.wiki-skin[data-width="standard"] .wiki-page,
.wiki-skin[data-width="wide"] .wiki-page { max-width: none; }

.wiki-skin[data-text="small"] .wiki-content { font-size: 13px; }
.wiki-skin[data-text="standard"] .wiki-content { font-size: 14.4px; }
.wiki-skin[data-text="large"] .wiki-content { font-size: 16.5px; }

/* ── Airstrike: split-screen view of a rival's article ─────────────────────
   #mpArticleStage and #mpSpyStage both match the full-bleed `.article-stage`
   rule above (position: fixed; inset: 0) — on its own that means each one
   tries to cover the entire viewport. Airstrike pins them to a half each
   instead: the width comes from the shared rule below, and #mpSpyStage's own
   `left` (higher specificity, being an id) is the only thing that moves it
   into the right half rather than stacking on top of the left one.

   Contents and Appearance both disappear while the two articles are side by
   side, and there is no width left over for
   either once the screen is halved. */
body.wiki-reading.mp-spying .article-stage {
    width: 50vw;
}

body.wiki-reading.mp-spying #mpSpyStage {
    left: 50vw;
    border-left: 3px solid #000;
}

/* The rival's whole feed reads as a captured camera/scope video, not a plain
   wiki page: a dark green tint plus horizontal scan lines over the entire
   pane. `position: fixed` with the same bounds as #mpSpyStage itself (see
   .article-stage above) rather than `absolute`, so the tint stays put as a
   lens over the viewport instead of scrolling away with the article text. */
body.wiki-reading.mp-spying #mpSpyStage::before {
    content: '';
    position: fixed;
    top: 0;
    left: 50vw;
    right: 0;
    bottom: 0;
    z-index: 400;
    pointer-events: none;
    background:
        repeating-linear-gradient(
            to bottom,
            rgba(0, 0, 0, 0.18) 0px,
            rgba(0, 0, 0, 0.18) 1px,
            transparent 1px,
            transparent 4px
        ),
        linear-gradient(rgba(10, 70, 30, 0.4), rgba(10, 70, 30, 0.4));
}

/* A brighter band sweeps down over the static scanlines on a loop, so the
   tint reads as a live feed refreshing rather than a still filter. The tile
   (background-size) and the distance travelled in one cycle are the same
   260px, so the loop point is seamless. */
body.wiki-reading.mp-spying #mpSpyStage::after {
    content: '';
    position: fixed;
    top: 0;
    left: 50vw;
    right: 0;
    bottom: 0;
    z-index: 401;
    pointer-events: none;
    background: linear-gradient(
        to bottom,
        transparent 0%,
        rgba(67, 245, 119, 0.16) 45%,
        rgba(67, 245, 119, 0.26) 50%,
        rgba(67, 245, 119, 0.16) 55%,
        transparent 100%
    );
    background-size: 100% 260px;
    background-repeat: repeat-y;
    animation: mp-airstrike-scan 3.2s linear infinite;
}

@keyframes mp-airstrike-scan {
    from { background-position: 0 0; }
    to { background-position: 0 260px; }
}

@media (prefers-reduced-motion: reduce) {
    body.wiki-reading.mp-spying #mpSpyStage::after { animation: none; }
}

/* Viewfinder corners framing the rival's whole pane — the fourth cue (with
   the tint, scan lines and reticle) that this is a targeting feed, not the
   plain article view the attacker gets on their own side. Anchored the same
   way as the tint above rather than to #mpSpyStage's own box, so they sit
   fixed to the viewport instead of scrolling with the article. */
.mp-airstrike-frame {
    position: fixed;
    top: 0;
    left: 50vw;
    right: 0;
    bottom: 0;
    z-index: 402;
    pointer-events: none;
}

.mp-airstrike-corner {
    position: absolute;
    width: 34px;
    height: 34px;
    border: 3px solid #43f577;
}

.mp-airstrike-corner--tl { top: 16px; left: 16px; border-right: none; border-bottom: none; }
.mp-airstrike-corner--tr { top: 16px; right: 16px; border-left: none; border-bottom: none; }
.mp-airstrike-corner--bl { bottom: 16px; left: 16px; border-right: none; border-top: none; }
.mp-airstrike-corner--br { bottom: 16px; right: 16px; border-left: none; border-top: none; }

body.mp-spying .mp-side-block--appearance {
    display: none;
}

/* Contents goes, the chat docked in it does not: display: contents drops the
   TOC's own box but leaves the chat as a (floating) child of the grid. */
body.mp-spying .wiki-toc { display: contents; }
body.mp-spying .wiki-toc > :not(.mp-toc-chat) { display: none; }

/* Same move .wiki-skin--bare already makes for the loading/error state:
   dropping the Contents column entirely rather than leaving its 220px track
   reserved and empty. */
body.mp-spying .wiki-skin {
    grid-template-columns: minmax(0, 1fr);
}

body.mp-spying .wiki-skin.has-side {
    grid-template-columns: minmax(0, 1fr) 232px;
}

body.mp-spying .wiki-skin[data-width="standard"],
body.mp-spying .wiki-skin[data-width="standard"].has-side {
    max-width: none;
}

.mp-spy-banner {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 14px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--wiki-border);
    font-weight: bold;
    color: var(--wiki-muted);
}

.mp-spy-banner svg { width: 16px; height: 16px; flex: 0 0 auto; }
.mp-spy-count {
    margin-left: auto;
    color: var(--wiki-link);
    font-family: var(--game-font);
    font-size: 12px;
}

#mpSpyStage[hidden],
.mp-airstrike-reticle[hidden] { display: none; }

#mpSpyStage,
#mpSpyStage .mp-spy-skin,
#mpSpyStage .mp-spy-skin * { cursor: none !important; }

#mpSpyStage [data-spy-close] { cursor: pointer !important; margin-left: 8px; }

.mp-airstrike-reticle {
    position: fixed;
    overflow: hidden;
    /* Belt-and-braces with the overflow above: this box starts `hidden` and
       only gets its real size from JS on the first pointermove (see
       startAirstrikeView), and revealing an `overflow: hidden` box for the
       first time like that can leave Chrome's compositor holding a stale
       clip — the crosshair lines then paint past the box's own edges,
       straight over the attacker's own article. `contain: paint` forces a
       hard clip boundary at layout time regardless of that reveal timing. */
    contain: paint;
    pointer-events: none;
    z-index: 1000;
    color: #43f577;
}

.mp-airstrike-ring,
.mp-airstrike-line {
    position: absolute;
    display: block;
    box-sizing: border-box;
    background: currentColor;
}

.mp-airstrike-ring {
    left: var(--target-x);
    top: var(--target-y);
    border: 2px solid currentColor;
    border-radius: 50%;
    background: transparent;
    transform: translate(-50%, -50%);
}

.mp-airstrike-ring--outer { width: 76px; height: 76px; }
.mp-airstrike-ring--inner { width: 30px; height: 30px; }

/* Crosshairs run edge-to-edge through the target point, cutting across both
   rings rather than stopping short of them — a scope reticle, not a gapped
   compass rose. Lines come after the rings in the markup, so they paint on
   top of the lens tint below. */
.mp-airstrike-line--top {
    width: 2px;
    left: calc(var(--target-x) - 1px);
    top: 0;
    height: var(--target-y);
}
.mp-airstrike-line--right {
    height: 2px;
    top: calc(var(--target-y) - 1px);
    left: var(--target-x);
    right: 0;
}
.mp-airstrike-line--bottom {
    width: 2px;
    left: calc(var(--target-x) - 1px);
    top: var(--target-y);
    bottom: 0;
}
.mp-airstrike-line--left {
    height: 2px;
    top: calc(var(--target-y) - 1px);
    left: 0;
    width: var(--target-x);
}

/* The rival's article is the Airstrike picker: clicking a link bombs that
   article for the selected victim rather than navigating the attacker. */
.mp-spy-skin button[data-article-link] {
    pointer-events: auto;
}

/* ── Phone layout ─────────────────────────────────────────────────────────
   Wikipedia serves the Minerva skin at this width, which has neither Vector
   rail. Ours kept the desktop 220px | 1fr | 232px grid at every width, so on
   a ~390px phone the fixed tracks alone (220 + 232 + 52 gap + 52 padding =
   556px) outran the viewport and `minmax(0, 1fr)` collapsed the article
   column — the one part the player actually has to read — to a sliver.

   The stage stays the scroll container: JS resets `stage.scrollTop` on every
   move, so letting the document scroll instead would break the jump-to-top
   between articles. That means the HUD is placed relative to the stage, not
   the page. The route now scrolls away at every width (see the .article-stage >
   .training-route rule above); here the item slot is the one thing still pinned
   to the viewport. */
@media (max-width: 768px) {
    .wiki-toc { display: contents; }
    .wiki-toc > :not(.mp-toc-chat) { display: none; }

    /* `.has-side` repeated here so its higher specificity (it wins on desktop
       on purpose, to widen for the multiplayer panel) can't outrank the phone
       collapse to a single column. */
    .wiki-skin,
    .wiki-skin.has-side {
        grid-template-columns: minmax(0, 1fr);
        gap: 0;
        /* The bottom pad clears the pinned item bar. Without it the article's
           last links sit underneath it and can't be tapped. */
        padding: 12px 14px 104px;
    }

    .wiki-skin .wiki-page { padding: 0; }

    .wiki-title { font-size: 26px; }

    /* Tighter side padding for the route; the rest of it is shared. Both of its
       homes, so a stowed loading state matches the article that follows. */
    body.wiki-reading .training-route { padding: 8px 12px 10px; }

    /* The target column takes the row space its pill would have taken, so the
       two sides stay symmetric once .route-pill starts flexing at this width. */
    body.wiki-reading .training-route .route-target-wrap {
        flex: 1 1 0;
        min-width: 0;
    }

    /* The side rail stops floating and just follows the article. */
    body.wiki-reading .mp-side {
        position: static;
        width: auto;
        max-height: none;
        margin: 0 14px 104px;
        overflow-y: visible;
    }

    /* …all except the item, which is the one control that has to stay in
       reach no matter how far down the article you are. */
    body.wiki-reading .mp-side-block--item {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        margin: 0;
        padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
        border-top: 3px solid #000;
        background: var(--surface);
        z-index: 330;
    }

    /* A 50vw split is unreadable at phone width — stack the two articles
       instead of sitting them side by side. */
    body.wiki-reading.mp-spying .article-stage {
        width: 100vw;
        height: 50vh;
    }

    body.wiki-reading.mp-spying #mpSpyStage {
        left: 0;
        top: 50vh;
        border-left: 0;
        border-top: 3px solid #000;
    }
}

/* ── The article body (page) ── */
.wiki-article {
    color: var(--wiki-fg);
    font-family: sans-serif;
    font-size: 14.4px;
    line-height: 1.6;
    text-align: left;
}

.wiki-mt-badge {
    position: relative;
    display: inline-block;
    width: 26px;
    height: 26px;
    margin-right: 10px;
    vertical-align: -3px;
    cursor: help;
    outline: none;
}

.wiki-mt-badge img {
    display: block;
    width: 100%;
    height: 100%;
}

/* The explanation, on hover or on tap (a tap focuses the span). */
.wiki-mt-badge::after {
    content: attr(data-tip);
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 20;
    width: max-content;
    max-width: 260px;
    padding: 6px 9px;
    border-radius: 5px;
    background: #202122;
    color: #fff;
    font-family: system-ui, sans-serif;
    font-size: 13px;
    line-height: 1.35;
    white-space: normal;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.12s;
}

.wiki-mt-badge:hover::after,
.wiki-mt-badge:focus::after {
    opacity: 1;
    visibility: visible;
}

.wiki-mt-badge--link {
    width: 0.95em;
    height: 0.95em;
    margin: 0 0.25em 0 0;
    vertical-align: -0.1em;
}

.wiki-title {
    margin: 0 0 4px;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--wiki-border);
    font-family: "Linux Libertine", "Georgia", "Times", serif;
    font-size: 28px;
    font-weight: normal;
    line-height: 1.3;
}

.training-complete {
    display: grid;
    gap: 4px;
    margin-bottom: 14px;
    padding: 14px 18px;
    border: 3px solid #000;
    border-radius: 7px;
    background: #ffe45c;
    color: var(--ink);
    font-family: var(--game-font);
    font-size: 32px;
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.22);
}

.training-complete strong {
    font-size: 26px;
}

.tm-best-line {
    font-size: 18px;
    color: #4a3b00;
}

/* ── Speedrun ── */

/* Tabular digits so the clock does not jitter sideways as it ticks. */
.sr-clock {
    min-width: 7ch;
    justify-content: center;
    font-size: 30px;
    font-variant-numeric: tabular-nums;
}

.sr-clock.is-pb {
    background: #ffe45c;
}

.sr-hud-best {
    font-size: 18px;
    opacity: 0.85;
}

.sr-result {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px 16px;
}

.sr-result-time {
    font-size: 40px;
    font-weight: bold;
    font-variant-numeric: tabular-nums;
}

.sr-result-verdict {
    font-size: 20px;
    color: #4a3b00;
}

.sr-result-verdict.is-pb {
    color: #1b7a1b;
    font-weight: bold;
}

.sr-result-btns {
    display: flex;
    gap: 10px;
    flex-basis: 100%;
}

.sr-swap {
    appearance: none;
    align-self: center;
    width: 52px;
    height: 40px;
    margin: 2px auto;
    border: 3px solid #000;
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    font-size: 24px;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.22);
}

.sr-swap:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.22);
}

.sr-routes {
    display: grid;
    gap: 6px;
    max-height: 320px;
    overflow-y: auto;
}

.sr-route {
    display: flex;
    gap: 6px;
}

.sr-route-pick {
    appearance: none;
    flex: 1;
    min-width: 0;
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0 12px;
    padding: 8px 12px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--game-font);
    text-align: left;
    cursor: pointer;
}

.sr-route-pick:hover {
    filter: brightness(0.96);
}

.sr-route-name {
    overflow: hidden;
    font-size: 20px;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.sr-route-best {
    grid-row: span 2;
    align-self: center;
    font-size: 24px;
    font-weight: bold;
    font-variant-numeric: tabular-nums;
}

.sr-route-meta {
    font-size: 15px;
    opacity: 0.7;
}

.sr-route-forget {
    appearance: none;
    width: 36px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    font-size: 22px;
    cursor: pointer;
}

.article-loading {
    padding: 40px 24px;
    border: 2px solid #a2a9b1;
    border-radius: 3px;
    background: color-mix(in srgb, var(--surface) 96%, transparent);
    color: #54595d;
    font-family: var(--game-font);
    font-size: 32px;
    text-align: center;
    box-shadow: 0 8px 0 rgba(0, 0, 0, 0.22);
}

.article-error {
    display: grid;
    gap: 6px;
    padding: 20px 24px;
    border: 3px solid #d33;
    border-radius: 7px;
    background: #fff8f8;
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 28.8px;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.14);
}

.article-error strong {
    color: #d33;
    font-size: 20px;
}

.wiki-content {
    margin-top: 12px;
    /* Contain floated infoboxes/thumbnails within the article page. */
    display: flow-root;
}

/* Headings (Parsoid wraps them in .mw-heading). */
.wiki-content h2,
.wiki-content .mw-heading2 {
    margin: 24px 0 8px;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--wiki-border);
    font-family: "Linux Libertine", "Georgia", "Times", serif;
    font-size: 22px;
    font-weight: normal;
    line-height: 1.3;
}

.wiki-content h3,
.wiki-content .mw-heading3 { margin: 18px 0 6px; font-size: 17px; font-weight: bold; }
.wiki-content h4,
.wiki-content .mw-heading4 { margin: 14px 0 6px; font-size: 15px; font-weight: bold; }
.wiki-content .mw-heading { border: 0; }

.wiki-content p { margin: 0.5em 0; }

.wiki-content ul,
.wiki-content ol { margin: 0.3em 0 0.6em 1.6em; }

.wiki-content li { margin-bottom: 0.1em; }

.wiki-content dl { margin: 0.2em 0; }
.wiki-content dd { margin-left: 1.6em; }

.wiki-content b,
.wiki-content strong { font-weight: bold; }

/* Links: Wikipedia blue, underline only on hover. */
.wiki-link-inline {
    appearance: none;
    border: 0;
    background: transparent;
    color: var(--wiki-link);
    cursor: pointer;
    font: inherit;
    text-align: left;
    text-decoration: none;
    padding: 0;
    margin: 0;
    line-height: inherit;
    /* Subtle glow in the link's own text color (follows theme overrides). */
    text-shadow: 0 0 6px color-mix(in srgb, currentColor 45%, transparent);
}

.wiki-link-inline:hover { text-decoration: underline; }
.wiki-link-inline:active { color: #faa700; }

/* Non-navigable links (File:, citations, external) read as plain text. */
.wiki-link-dead {
    color: inherit;
    text-decoration: none;
}

/* Images. */
.wiki-content img {
    max-width: 100%;
    height: auto;
}

.wiki-content figure,
.wiki-content .thumb {
    margin: 4px 0 14px;
    font-size: 13.6px;
}

.wiki-content figure[typeof*="mw:File/Thumb"],
.wiki-content figure.mw-default-size,
.wiki-content .tright,
.wiki-content .mw-halign-right {
    float: right;
    clear: right;
    margin: 6px 0 12px 18px;
}

.wiki-content .tleft,
.wiki-content .mw-halign-left {
    float: left;
    clear: left;
    margin: 6px 18px 12px 0;
}

.wiki-content figure img,
.wiki-content .thumbinner img {
    border: 1px solid var(--wiki-border);
    background: var(--surface);
}

.wiki-content figure,
.wiki-content .thumbinner {
    max-width: 320px;
    padding: 3px;
    border: 1px solid var(--wiki-border);
    background: var(--wiki-soft);
}

.wiki-content figcaption,
.wiki-content .thumbcaption {
    padding: 4px 2px 2px;
    color: var(--wiki-muted);
    line-height: 1.4;
    text-align: left;
}

/* Infoboxes. */
.wiki-content .infobox {
    float: right;
    clear: right;
    width: 22em;
    max-width: 45%;
    margin: 4px 0 16px 18px;
    padding: 0;
    border: 1px solid var(--wiki-border);
    border-spacing: 3px;
    background: var(--wiki-soft);
    font-size: 12.6px;
    line-height: 1.5;
}

.wiki-content .infobox caption,
.wiki-content .infobox-title,
.wiki-content .infobox-above {
    padding: 6px 8px;
    font-size: 15px;
    font-weight: bold;
    text-align: center;
}

.wiki-content .infobox th,
.wiki-content .infobox td { padding: 4px 8px; vertical-align: top; text-align: left; }
.wiki-content .infobox-image,
.wiki-content .infobox td[colspan] { text-align: center; }

/* ── Templates that ship their own CSS ────────────────────────────────────────
   Wikipedia articles don't carry their layout in HTML: they carry it in
   TemplateStyles, per-template stylesheets delivered inside the page. The Cat
   article alone ships 21 <style> blocks and 327 more by reference. Every one of
   them is removed by WIKI_STRIP_SELECTOR (script.js) — the right call for
   third-party CSS, but it leaves template markup with no layout at all.

   So the templates that actually matter are reproduced here, using Wikipedia's
   own selectors so the markup needs no rewriting. Frequencies measured across
   20 varied articles: thumbnails 18/20, infoboxes 15/20, multi-image 12/20,
   galleries 7/20, maths 6/20. Everything past that is long tail — it degrades
   to plainly-styled content rather than breaking. */

/* Infoboxes carry an inline width (Cat's says 200px) that beats any ordinary
   rule, and it's narrower than the image grid inside them — so the grid
   overflows and the photos spill down the page. Hence !important. */
.wiki-content .infobox {
    width: 22em !important;
    max-width: 45%;
}

/* {{Multiple image}} — the "various types of cats" grid. Each .trow holds one
   row's images; without the flex every image takes its own line. */
.wiki-content .tmulti .multiimageinner { max-width: 100%; }

.wiki-content .tmulti .trow {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    clear: left;
    width: 100%;
    box-sizing: border-box;
}

.wiki-content .tmulti .tsingle { margin: 1px; }
.wiki-content .tmulti .tsingle img { display: block; max-width: 100%; height: auto; }
.wiki-content .tmulti .thumbcaption { background: transparent; text-align: center; }

/* Galleries: a plain <ul> until told otherwise, which stacks one item per row. */
.wiki-content ul.gallery {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 12px 0;
    padding: 0;
    list-style: none;
}

.wiki-content li.gallerybox { list-style: none; }
.wiki-content li.gallerybox div.thumb { margin: 0; background: transparent; }
.wiki-content .gallerytext { font-size: 12.6px; text-align: center; }

/* Maths and chemistry. The MathML is hidden by an inline style and the visible
   form is an SVG, so formulae survive the strip — but they're black artwork,
   and Wikipedia flips them for dark mode with .skin-invert, which does not.
   Without this every equation is black-on-near-black. */
.wiki-content .mwe-math-element { max-width: 100%; overflow-x: auto; }
.wiki-content .mwe-math-fallback-image-display { display: block; margin: 12px auto; }

.article-stage[data-wiki-color="dark"] .wiki-content img.mw-invert,
.article-stage[data-wiki-color="dark"] .wiki-content img.skin-invert,
.rc-scroll[data-wiki-color="dark"] .wiki-content img.mw-invert,
.rc-scroll[data-wiki-color="dark"] .wiki-content img.skin-invert {
    filter: invert(1) hue-rotate(180deg);
}

@media (prefers-color-scheme: dark) {
    .article-stage[data-wiki-color="auto"] .wiki-content img.mw-invert,
    .article-stage[data-wiki-color="auto"] .wiki-content img.skin-invert,
    .rc-scroll[data-wiki-color="auto"] .wiki-content img.mw-invert,
    .rc-scroll[data-wiki-color="auto"] .wiki-content img.skin-invert {
        filter: invert(1) hue-rotate(180deg);
    }
}

/* Hatnotes / disambig / "for other uses". */
.wiki-content .hatnote,
.wiki-content .dablink,
.wiki-content .rellink {
    margin: 0 0 8px;
    padding-left: 1.6em;
    color: var(--wiki-fg);
    font-style: italic;
}

/* Tables. */
.wiki-content table.wikitable {
    margin: 12px 0;
    border: 1px solid var(--wiki-border);
    border-collapse: collapse;
    background: var(--wiki-soft);
    font-size: 13.6px;
}

.wiki-content table.wikitable > * > tr > th {
    border: 1px solid var(--wiki-border);
    padding: 4px 8px;
    background: var(--wiki-th);
    text-align: center;
    font-weight: bold;
}

.wiki-content table.wikitable > * > tr > td {
    border: 1px solid var(--wiki-border);
    padding: 4px 8px;
}

/* hlist — Wikipedia's horizontal list. Every navbox row is one, and so are
   parts of many infoboxes. It is pure TemplateStyles, which we strip, and
   without it each <li> falls back to list-item: the Volta–Niger navbox laid its
   89 links out one per line and stood 1919px tall. Interpuncts and the
   parentheses around a nested list are how the real thing separates them. */
.wiki-content .hlist ul {
    margin: 0;
    padding: 0;
    list-style: none;
}

.wiki-content .hlist li {
    display: inline;
    margin: 0;
}

.wiki-content .hlist li:not(:last-child)::after {
    content: " · ";
    font-weight: bold;
}

.wiki-content .hlist ul ul { display: inline; }
.wiki-content .hlist ul ul::before { content: "("; }
.wiki-content .hlist ul ul::after { content: ")"; }

/* Navboxes. Wikipedia styles these from TemplateStyles, which ships inside the
   article as a <style> tag we strip on sight — so without this they render as a
   bare stack of links with no structure at all. Rebuilt from the same tokens as
   the tables above, so they follow the light/dark/auto setting like everything
   else. Kept for the links: on a stub the navbox is usually the only way out. */
.wiki-content .navbox {
    clear: both;
    margin: 12px 0;
    border: 1px solid var(--wiki-border);
    background: var(--wiki-soft);
    font-size: 12.6px;
    text-align: center;
}

.wiki-content .navbox-title,
.wiki-content .navbox-abovebelow,
.wiki-content th.navbox-group {
    padding: 3px 10px;
    background: var(--wiki-th);
    font-weight: bold;
}

.wiki-content .navbox-title { font-size: 13.6px; }

/* The group label column: narrow, right-aligned, against the row it labels. */
.wiki-content th.navbox-group {
    white-space: nowrap;
    text-align: right;
}

.wiki-content .navbox-list {
    padding: 3px 10px;
    text-align: left;
}

/* Alternating row wash, the same trick the real navbox uses to separate lists
   that would otherwise run together. */
.wiki-content .navbox-odd { background: transparent; }
.wiki-content .navbox-even { background: var(--wiki-th); }

/* Nested navboxes sit inside a parent's list cell; no double frame. */
.wiki-content .navbox .navbox {
    margin: 0;
    border: 0;
}

/* The little periodic table at the foot of every element article. Each element
   is a 6x8px coloured <span> inside a <td>; the 1px gaps come from a
   TemplateStyles sheet we strip, so without this the cells fall back to the
   browser's 2px border-spacing plus 1px cell padding and the table sprawls to
   roughly twice its width. */
.wiki-content table.periodic-table-32-micro,
.wiki-content table.periodic-table-32-micro-119 {
    margin: 0;
    padding: 0;
    empty-cells: hide;
    border-spacing: 1px;
    text-align: center;
}

.wiki-content table.periodic-table-32-micro td,
.wiki-content table.periodic-table-32-micro-119 td {
    border: none;
    padding: 0;
}

.wiki-content .periodic-table-32-micro-insert-cell-gt118 { width: 6px; min-width: 6px; }
.wiki-content .periodic-table-32-micro-no-insert-cell-gt118 { width: 0; }

/* v-t-e. Every link in it points at Template:, Template talk: or Special:, none
   of which are moves, so the whole thing is dead text here — and the stub
   notice carries one too, which without this hangs under the article as a bare
   "v·t·e" that looks like a rendering fault. */
.wiki-content .navbar {
    display: none;
}

/* References / smaller print. */
.wiki-content .reflist,
.wiki-content .references,
.wiki-content ol.references { font-size: 12.6px; }

.wiki-content sup,
.wiki-content sub { line-height: 1; font-size: 78%; }

.wiki-content blockquote {
    margin: 8px 0 8px 24px;
    padding-left: 12px;
    border-left: 3px solid #eaecf0;
    color: #202122;
}

/* ===========================
   RESPONSIVE DESIGN
   =========================== */

@media (max-width: 1400px) {
    .grid-container {
        grid-template-columns: repeat(3, 1fr);
        gap: 40px 70px;
    }
}

@media (max-width: 1200px) {
    .grid-container {
        gap: 30px 60px;
    }
}

@media (max-width: 992px) {
    .grid-container {
        grid-template-columns: repeat(2, 1fr);
        gap: 30px 50px;
    }

    .route-pill {
        min-width: 210px;
        font-size: 24px;
    }
}

@media (max-width: 768px) {
    .grid-container {
        grid-template-columns: 1fr;
        gap: 25px;
    }

    body > main {
        padding: 15px;
    }

    .menu-stage {
        padding: 80px 0;
    }

    .training-container {
        max-width: calc(100vw - 24px);
    }

    .training-route {
        top: 66px;
        gap: 10px;
        padding: 8px;
    }

    .training-route--snake {
        align-items: flex-start;
    }

    .route-snake-row {
        width: 100%;
        min-width: 0;
        justify-content: flex-start;
    }

    .route-snake-row .route-pill {
        min-width: 0;
        font-size: 16px;
    }

    .route-snake-row .route-target-desc {
        max-width: 42vw;
        font-size: 12px;
    }

    .route-snake-row .route-to {
        padding-top: 5px;
        font-size: 22px;
    }

    .route-pill {
        min-width: 0;
        flex: 0 0 auto;
        font-size: 18px;
    }

    .route-to {
        flex: 0 0 auto;
        font-size: 18px;
    }

    .roulette-card {
        min-height: 210px;
        padding: 20px;
    }

    .roulette-title {
        font-size: 42px;
    }

    .training-progress,
    .wiki-status {
        align-items: flex-start;
        flex-direction: column;
    }

    .progress-trail {
        justify-content: flex-start;
    }

    .wiki-article {
        padding: 22px;
    }

    .wiki-article h1 {
        font-size: 34px;
    }
}

/* ── Phone, part two: full-bleed text and no sideways scroll ───────────────
   Placed after the .wiki-content rules above so it wins on order rather than
   on a pile of !important — the infobox is the one exception, because its own
   width is already !important (see the {{Infobox}} note above).

   The stage is `overflow-y: auto`, and CSS resolves the other axis of a
   scroll container to `auto` too, so anything wider than the viewport gave
   the article its own horizontal scrollbar. Clipping that axis is only half
   an answer — on its own it turns overflow into content you cannot reach at
   all — so everything below is about making the wide things genuinely fit,
   with the clip as a backstop rather than the mechanism. */
@media (max-width: 768px) {
    /* Corner furniture: gone. A phone has no room for it beside the article,
       and Discord is a tap-through to somewhere else entirely. Friends has no
       button of its own to hide any more — it lives in the profile dropdown,
       which the article reading view already hides the trigger for. */
    .discord-btn { display: none; }

    .notif-layer { right: 16px; bottom: 92px; }

    /* Edge to edge — on a screen this narrow the side gutters were costing
       more in reading width than they bought in comfort. */
    .wiki-skin { padding: 12px 0 104px; }
    .wiki-skin .wiki-page { padding: 0; }
    .wiki-title,
    .wiki-content,
    .wiki-attrib { padding-inline: 0; }

    /* Belt to the braces below: clip whatever still manages to be too wide. */
    body.wiki-reading .article-stage { overflow-x: hidden; }

    /* The menu screens have their own leak. Letting the card menu scroll
       means the fit valve resets the body to `overflow: visible`, and that
       also undoes the base `overflow-x: clip` — so a card a few pixels too
       wide could drag the whole menu sideways. `clip` rather than `hidden`:
       hidden on one axis makes the other a scroll container, which would
       take the page's vertical scrolling with it. */
    body.menu-fit.menu-fit-overflow:not(.wiki-reading) { overflow-x: clip; }

    /* 1. The infobox. Floated right at 45%, it squeezed the lead paragraphs
       into barely half a phone's width — the main reason the text did not run
       side to side. Full width and out of the flow of the prose instead, the
       way Minerva stacks it. */
    .wiki-content .infobox {
        float: none;
        clear: both;
        width: 100% !important;
        max-width: 100% !important;
        margin: 12px 0;
    }

    /* 2. Wide tables — EVERY table, not just .wikitable. Scoping this to that
       class was the bug behind the cropping: real articles barely use it.
       Fetched through our own proxy, Cat has 112 tables and 0 wikitables,
       Chess 40 and 0, Telephone 2 and 0. So the rule matched almost nothing,
       and everything it missed was clipped by the overflow-x above instead of
       being scrollable. A table cannot be told to wrap, so each becomes its
       own scroll box: the table slides, the page does not. */
    .wiki-content table {
        display: block;
        width: max-content;
        max-width: 100%;
        overflow-x: auto;
    }

    /* 3. Hard-coded pixel widths, which Wikipedia's markup is full of — 384
       inline ones in Chess, 488 in Periodic table. An inline `width` beats any
       stylesheet `width`, but not `max-width`: that clamps the used value
       afterwards, so the whole class of them is reined in without needing
       !important on each. */
    .wiki-content img,
    .wiki-content table,
    .wiki-content figure,
    .wiki-content div,
    .wiki-content pre,
    .wiki-content blockquote { max-width: 100%; }

    /* 3b. Clamping a floated figure to max-width:100% does not stop it
       floating: it fills the column and leaves the paragraph beside it a gutter
       a few pixels wide to wrap in, one letter per line. Measured on Combustion
       at 500px: figures 449-467px in a 485px column. Give them their own row. */
    .wiki-content figure[typeof*="mw:File/Thumb"],
    .wiki-content figure.mw-default-size,
    .wiki-content .tright,
    .wiki-content .mw-halign-right,
    .wiki-content .tleft,
    .wiki-content .mw-halign-left {
        float: none;
        margin-left: 0;
        margin-right: 0;
    }

    /* 4. Unbreakable strings: long URLs in references, chemical names, the
       German ones. */
    .wiki-content { overflow-wrap: break-word; }

    /* Image rows and galleries lay out with flex and would otherwise run off. */
    .wiki-content .tmulti .trow,
    .wiki-content ul.gallery { flex-wrap: wrap; }
}

/* Shorter viewports (e.g. 4:3 monitors) — nothing above reacts to height,
   so the fixed header/footer and menu padding ate too much vertical space. */
@media (max-height: 820px) {
    :root { --chrome-h: calc(60px * var(--wdm-k, 1)); --logo-clear: 222px; }  /* wordmark 48px -> ends at 208 */

    .menu-stage {
        padding: 50px 0;
    }
}

@media (max-height: 620px) {
    :root { --chrome-h: calc(46px * var(--wdm-k, 1)); --logo-clear: 176px; }  /* wordmark 36px -> ends at 162 */

    .menu-stage {
        padding: 30px 0;
    }

    .back-button {
        width: 48px;
        height: 38px;
    }
}

/* ===========================
   MODE SELECTION
   =========================== */

.training-mode-select {
  width: 100%;
  padding: 16px 0 80px;
  will-change: transform, opacity;
}

.training-container.is-exiting .training-mode-select {
  animation: trainingScreenOutLeft 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.training-container.is-entering .training-mode-select {
  animation: trainingScreenInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

.training-container.is-exiting-back .training-mode-select {
  animation: trainingScreenOutRight 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.training-container.is-entering-back .training-mode-select {
  animation: trainingScreenInLeft 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

/* The four difficulty bars. Shape and layout come from .card-bars in the CARD
   BARS section, which this grid carries too; what is left here is the entrance
   animation and the per-mode colours.

   The animation is declared rather than inherited because this stack lives
   inside .training-container, not .grid-container, so it never gets the
   is-entering/is-exiting card animations the menu screens have. */
.mode-select-grid .glass {
  animation: cardSlideInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
  animation-delay: var(--row-delay);
}

/* Per-mode card color variants */
.mode-card--easy .blur {
  background: rgba(0, 200, 80, 0.534);
}

.mode-card--easy .blue-card::before {
  background:
    radial-gradient(circle at 0% 0%, #0d8f3f 0%, transparent 50%),
    radial-gradient(circle at 100% 0%, #00c840 0%, transparent 50%),
    radial-gradient(circle at 0% 100%, #00c840 0%, transparent 50%),
    radial-gradient(circle at 100% 100%, #0d8f3f 0%, transparent 50%),
    linear-gradient(135deg, #0d8f3f, #00c840);
}

.mode-card--hard .blur {
  background: rgba(255, 140, 0, 0.534);
}

.mode-card--hard .blue-card::before {
  background:
    radial-gradient(circle at 0% 0%, #b85000 0%, transparent 50%),
    radial-gradient(circle at 100% 0%, #e07000 0%, transparent 50%),
    radial-gradient(circle at 0% 100%, #e07000 0%, transparent 50%),
    radial-gradient(circle at 100% 100%, #b85000 0%, transparent 50%),
    linear-gradient(135deg, #b85000, #e07000);
}

.mode-card--fully-random .blur {
  background: rgba(160, 0, 240, 0.534);
}

.mode-card--fully-random .blue-card::before {
  background:
    radial-gradient(circle at 0% 0%, #6600b8 0%, transparent 50%),
    radial-gradient(circle at 100% 0%, #9800e0 0%, transparent 50%),
    radial-gradient(circle at 0% 100%, #9800e0 0%, transparent 50%),
    radial-gradient(circle at 100% 100%, #6600b8 0%, transparent 50%),
    linear-gradient(135deg, #6600b8, #9800e0);
}

/* .goldframe3-text and .mode-card-desc lived here to style the description
   inside each card's gold frame. Both went with the frame; the stack has no
   text but the name. The narrow-screen tightening moved to .card-bars with the
   rest of the shape. */

/* ===========================
   SETTINGS MODE
   =========================== */

.settings-container {
    display: block;
    max-width: min(900px, calc(100vw - 40px));
    margin: 0 auto;
}

.settings-screen {
    width: 100%;
    padding: 16px 0 120px;
    will-change: transform, opacity;
}

.settings-container.is-exiting .settings-screen {
    animation: trainingScreenOutLeft 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.settings-container.is-entering .settings-screen {
    animation: trainingScreenInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

.settings-container.is-exiting-back .settings-screen {
    animation: trainingScreenOutRight 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.settings-container.is-entering-back .settings-screen {
    animation: trainingScreenInLeft 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

.settings-list {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.settings-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    padding: 20px 28px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow:
        0 6px 0 rgba(0, 0, 0, 0.24),
        inset 0 0 0 3px rgba(176, 176, 255, 0.55);
    transition: opacity 120ms;
}

.settings-row.is-disabled {
    opacity: 0.4;
    pointer-events: none;
}

.settings-info {
    display: flex;
    flex-direction: column;
    gap: 5px;
}

.settings-label {
    font-family: var(--game-font);
    font-weight: bold;
    font-size: 35.2px;
    color: var(--ink);
    text-shadow: 1px 1px 0 rgba(176, 176, 255, 0.7);
}

.settings-desc {
    font-family: var(--game-font);
    font-size: 22.4px;
    color: var(--ink-muted);
}

.settings-toggle {
    appearance: none;
    flex-shrink: 0;
    min-width: 80px;
    padding: 9px 18px;
    border: 3px solid #000;
    border-radius: 6px;
    font-family: var(--game-font);
    font-weight: bold;
    font-size: 28.8px;
    cursor: pointer;
    background: #c0c0c8;
    color: var(--ink-muted);
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.28);
    transition: background 120ms, color 120ms;
    text-align: center;
}

.settings-toggle.is-on {
    background: #0032ff;
    color: #fff;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}

.settings-toggle:active {
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.28);
    transform: translateY(3px);
}

.settings-choice {
    display: flex;
    flex-shrink: 0;
    border: 3px solid #000;
    border-radius: 6px;
    overflow: hidden;
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.28);
}

.settings-choice button {
    appearance: none;
    border: 0;
    border-left: 2px solid #000;
    padding: 9px 18px;
    font-family: var(--game-font);
    font-weight: bold;
    font-size: 25.6px;
    cursor: pointer;
    background: var(--surface-3);
    color: var(--ink-muted);
    transition: background 120ms, color 120ms;
    white-space: nowrap;
}

.settings-choice button:first-child {
    border-left: 0;
}

.settings-choice button.is-active {
    background: #0032ff;
    color: #fff;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}

.settings-choice button:disabled {
    cursor: default;
}

@media (max-width: 600px) {
    .settings-row {
        flex-direction: column;
        align-items: flex-start;
        gap: 14px;
    }

    .settings-label {
        font-size: 18px;
    }
}

/* ===========================
   SHARED SCREEN ANIMATIONS
   (about, wikichannel, shop, custom, challenges, multiplayer)
   Not the shop: it arrives and leaves in four blocks of its own
   (.shopx--intro, .shop-container.is-exiting).
   =========================== */

.about-container.is-exiting .about-screen,
.wikichannel-container.is-exiting .wch,
.custom-container.is-exiting .custom-screen,
.challenges-container.is-exiting .challenges-screen,
.multiplayer-container.is-exiting .multiplayer-screen {
    animation: trainingScreenOutLeft 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.about-container.is-entering .about-screen,
.wikichannel-container.is-entering .wch,
.custom-container.is-entering .custom-screen,
.challenges-container.is-entering .challenges-screen,
.multiplayer-container.is-entering .multiplayer-screen {
    animation: trainingScreenInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

.about-container.is-exiting-back .about-screen,
.wikichannel-container.is-exiting-back .wch,
.custom-container.is-exiting-back .custom-screen,
.challenges-container.is-exiting-back .challenges-screen,
.multiplayer-container.is-exiting-back .multiplayer-screen {
    animation: trainingScreenOutRight 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
}

.about-container.is-entering-back .about-screen,
.wikichannel-container.is-entering-back .wch,
.custom-container.is-entering-back .custom-screen,
.challenges-container.is-entering-back .challenges-screen,
.multiplayer-container.is-entering-back .multiplayer-screen {
    animation: trainingScreenInLeft 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
}

.about-container, .wikichannel-container, .shop-container,
.custom-container, .challenges-container,
.multiplayer-container {
    display: block;
    max-width: min(980px, calc(100vw - 40px));
    margin: 0 auto;
}

.about-screen, .wikichannel-screen, .shop-screen,
.custom-screen, .challenges-screen,
.multiplayer-screen {
    width: 100%;
    padding: 16px 0 120px;
    will-change: transform, opacity;
}

/* The 120px bottom padding above is scroll headroom for the views that need
   it (lobby chat, room lists). The row-menu screens opt into MenuFit instead
   (see shell() in MultiplayerController) precisely so they don't scroll, and
   that padding would just get measured as extra height for MenuFit to shrink
   the rows to fit around — so it drops out under the same body class. */
body.menu-fit .multiplayer-screen {
    padding: 0;
}

/* ===========================
   ABOUT
   =========================== */

.about-card {
    padding: 36px 40px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 6px 0 rgba(0,0,0,0.22), inset 0 0 0 3px rgba(176,176,255,0.45);
}

.about-title {
    font-family: var(--game-font);
    font-size: 83.2px;
    font-weight: bold;
    color: var(--ink-link);
    text-shadow: 3px 3px 0 rgba(176,176,255,0.5);
    margin-bottom: 14px;
}

/* ── The team, above the tabs ──
   Each person is their pointer. The cursor is the portrait: it is what other
   players actually see of them in a lobby, and the creator's is live — it
   changes whenever they equip something else. */
.about-team {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 20px;
}

.about-team:empty { display: none; }

.team-member {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 14px 8px 10px;
    border: 2px solid #000;
    border-radius: 10px;
    background: var(--surface);
    text-decoration: none;
}

/* Only a row that goes somewhere lifts. A plain portrait row must not look
   pressable — see teamRowHtml, which picks the element to match. */
a.team-member,
button.team-member {
    width: 100%;
    font: inherit;
    text-align: left;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.2);
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}

a.team-member:hover,
button.team-member:hover {
    transform: translateY(-2px);
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.22);
}

/* A fixed box, so a tall cursor and a short one leave the names on the same
   line — the sprites are authored at different heights. */
.team-cursor {
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    flex: none;
}

.team-cursor img {
    max-width: 100%;
    max-height: 100%;
    image-rendering: pixelated;
}

.team-who {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.team-name {
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: 900;
    color: var(--ink-navy);
}

.team-role {
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: bold;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.about-tabs {
    display: flex;
    gap: 8px;
    margin-bottom: 18px;
}

/* ── One racer's profile ──
   Reuses the About card's shell, since it is the same kind of page: a single
   panel of read-only facts. Everything on it comes from the server's own race
   record — see PlayerProfileController for why achievements are not here. */
.pp-head {
    display: flex;
    align-items: center;
    gap: 16px;
    padding-bottom: 16px;
    margin-bottom: 16px;
    border-bottom: 2px solid #dde;
}

.pp-portrait {
    display: grid;
    place-items: center;
    width: 84px;
    height: 84px;
    flex: none;
    border: 3px solid #000;
    border-radius: 12px;
    background: radial-gradient(120% 100% at 50% 0%, #eef1ff, #fdfdff 70%);
}

.pp-portrait img {
    max-width: 100%;
    max-height: 100%;
    image-rendering: pixelated;
}

.pp-name {
    font-family: var(--game-font);
    font-size: 44.8px;
    font-weight: 900;
    line-height: 1.1;
    color: var(--ink-navy);
}

.pp-tier {
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.pp-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px;
}

.pp-tile {
    padding: 12px 14px;
    border: 2px solid #000;
    border-radius: 10px;
    background: var(--surface);
    font-family: var(--game-font);
}

.pp-tile-value {
    font-size: 24px;
    font-weight: 900;
    line-height: 1.15;
    color: var(--ink-link);
}

.pp-tile-label {
    font-size: 11px;
    font-weight: 900;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

/* Course names and "wins / played" — long enough to need their own line. */
.pp-tile-note {
    margin-top: 4px;
    font-size: 12px;
    color: var(--ink-mid);
    overflow-wrap: anywhere;
}

.pp-empty {
    font-family: var(--game-font);
    font-size: 22.4px;
    line-height: 1.5;
    color: var(--ink-muted);
}

.about-tab {
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: bold;
    color: #556;
    background: var(--surface-2);
    border: 2px solid #b9c2e8;
    border-radius: 8px;
    padding: 7px 20px;
    cursor: pointer;
}

.about-tab:hover { border-color: #6a68fe; }

.about-tab.on {
    background: #0032ff;
    border-color: #0032ff;
    color: #fff;
}

.about-tab-body {
    max-height: 52vh;
    overflow-y: auto;
    padding-right: 6px;
}

.legal-body {
    font-family: var(--game-font);
    font-size: 21.6px;
    line-height: 1.65;
    color: var(--ink-mid);
    text-align: left;
}

.legal-body h3 {
    margin: 16px 0 6px;
    font-size: 15px;
    color: var(--ink-link);
}

.legal-body h3:first-child { margin-top: 0; }

.legal-body p { margin: 0 0 10px; }

.legal-body a { color: var(--ink-link); }

.credits-intro {
    font-family: var(--game-font);
    font-size: 21.6px;
    color: var(--ink-mid);
    margin: 0 0 14px;
}

.credits-status {
    font-family: var(--game-font);
    font-size: 21.6px;
    color: var(--ink-faint);
}

.credit-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.credit-row {
    border: 2px solid #dde1f0;
    border-radius: 8px;
    overflow: hidden;
    background: var(--surface);
}

/* A link now rather than a button: the row opens that package's notice on
   /acknowledgements, where the licence texts live. Kept looking exactly like
   the row it replaced. */
.credit-head {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 8px 12px;
    border: 0;
    background: none;
    cursor: pointer;
    font-family: var(--game-font);
    text-align: left;
    text-decoration: none;
}

a.credit-head:hover { background: var(--surface-2); }

.credit-name {
    font-size: 13px;
    font-weight: bold;
    color: var(--ink-navy);
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.credit-version {
    font-size: 11px;
    color: #8891a3;
    flex: 0 0 auto;
}

.credit-license {
    font-size: 10.5px;
    font-weight: bold;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--ink-link);
    background: rgba(0, 50, 255, 0.08);
    border-radius: 999px;
    padding: 2px 9px;
    flex: 0 0 auto;
}

/* The one-line "what this is" under a row, for the two libraries that are
   bundled rather than installed. */
.credit-note {
    margin: 0;
    padding: 0 12px 9px;
    font-family: var(--game-font);
    font-size: 18.4px;
    line-height: 1.45;
    color: var(--ink-faint);
}

/* ===========================
   WIKICHANNEL
   =========================== */

.channel-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
    padding: 12px 18px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 4px 0 rgba(0,0,0,0.2), inset 0 0 0 3px rgba(176,176,255,0.45);
}

.channel-brand {
    font-family: var(--game-font);
    font-size: 35.2px;
    font-weight: bold;
    color: var(--ink-link);
    letter-spacing: 2px;
    text-shadow: 1px 1px 0 rgba(176,176,255,0.7);
}

.channel-content {
    min-height: 360px;
}

.channel-loading, .channel-error {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 200px;
    font-family: var(--game-font);
    font-size: 32px;
    color: var(--ink-faint);
    border: 2px solid #dde;
    border-radius: 7px;
    background: color-mix(in srgb, var(--surface) 70%, transparent);
}

/* ===========================
   SHOP
   =========================== */

.shop-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: 24px;
    padding-bottom: 12px;
    border-bottom: 3px solid #000;
}

.shop-title {
    font-family: var(--game-font);
    font-size: 57.6px;
    font-weight: bold;
    color: var(--ink);
}

.shop-stat {
    font-family: var(--game-font);
    font-size: 24px;
    color: var(--ink-muted);
}

.shop-section {
    margin-bottom: 32px;
}

.shop-category-label {
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin-bottom: 12px;
}

.shop-items-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 14px;
}

.shop-item {
    padding: 18px 20px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 4px 0 rgba(0,0,0,0.2);
    display: flex;
    flex-direction: column;
    gap: 5px;
}

.shop-item-name {
    font-family: var(--game-font);
    font-size: 28.8px;
    font-weight: bold;
    color: var(--ink);
}

.shop-item-desc {
    font-family: var(--game-font);
    font-size: 20.8px;
    color: var(--ink-muted);
}

.shop-item-badge {
    margin-top: 6px;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    padding: 4px 10px;
    border-radius: 4px;
    align-self: flex-start;
}

.shop-item.is-unlocked .shop-item-badge {
    background: #d4f5d4;
    color: #1a6b1a;
    border: 1px solid #1a6b1a;
}

.shop-item.is-locked {
    opacity: 0.6;
}

.shop-item.is-locked .shop-item-badge {
    background: #f0f0f0;
    color: var(--ink-faint);
    border: 1px solid #ccc;
}

.shop-note {
    font-family: var(--game-font);
    font-size: 20.8px;
    color: #aaa;
    text-align: center;
    margin-top: 8px;
}

@media (max-width: 700px) {
    .shop-items-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ===========================
   CUSTOM MODE
   =========================== */

.custom-form {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    max-width: 520px;
    margin: 0 auto;
    padding: 36px 40px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 6px 0 rgba(0,0,0,0.22), inset 0 0 0 3px rgba(176,176,255,0.45);
}

.custom-field {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.custom-label {
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.custom-input {
    appearance: none;
    width: 100%;
    padding: 12px 16px;
    border: 3px solid #000;
    border-radius: 6px;
    font-family: var(--game-font);
    font-size: 32px;
    color: var(--ink);
    background: var(--surface);
    box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
    transition: border-color 120ms;
}

.custom-input:focus {
    outline: none;
    border-color: #0032ff;
}

.custom-err {
    font-family: var(--game-font);
    font-size: 20.8px;
    color: #d33;
    min-height: 18px;
}

.custom-divider {
    font-family: var(--game-font);
    font-size: 35.2px;
    font-weight: bold;
    color: var(--ink-link);
    text-shadow: 1px 1px 0 rgba(176,176,255,0.7);
}

.custom-play-btn {
    appearance: none;
    margin-top: 8px;
    width: 100%;
    padding: 14px;
    border: 3px solid #000;
    border-radius: 6px;
    background: #0032ff;
    color: #fff;
    font-family: var(--game-font);
    font-size: 32px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 5px 0 rgba(0,0,0,0.28);
    transition: background 120ms;
}

.custom-play-btn:hover { background: #0028cc; }
.custom-play-btn:active { transform: translateY(3px); box-shadow: 0 2px 0 rgba(0,0,0,0.28); }
.custom-play-btn:disabled { background: #aab; cursor: default; }

/* ===========================
   STORY MODE
   =========================== */

.story-intro-btns {
    display: flex;
    gap: 12px;
}

.story-back-btn {
    appearance: none;
    padding: 12px 20px;
    border: 3px solid #000;
    border-radius: 6px;
    background: var(--surface-3);
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0,0,0,0.2);
}

.story-back-btn:hover { background: var(--surface-3); }

.story-begin-btn {
    appearance: none;
    flex: 1;
    padding: 12px 28px;
    border: 3px solid #000;
    border-radius: 6px;
    background: #0032ff;
    color: #fff;
    font-family: var(--game-font);
    font-size: 28.8px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0,0,0,0.28);
}

.story-begin-btn:hover { background: #0028cc; }

@media (max-width: 700px) {
}

/* ===========================
   CHALLENGES
   =========================== */

.challenge-card {
    max-width: 600px;
    margin: 0 auto;
    padding: 36px 40px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 6px 0 rgba(0,0,0,0.22), inset 0 0 0 3px rgba(176,176,255,0.45);
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.challenge-eyebrow {
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--ink-link);
}

.challenge-date {
    font-family: var(--game-font);
    font-size: 44.8px;
    font-weight: bold;
    color: var(--ink);
}

.challenge-route {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
}

.challenge-pill {
    padding: 10px 18px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 35.2px;
    font-weight: bold;
    color: var(--ink-link);
    text-shadow: 1px 1px 0 rgba(0,0,0,0.1);
}

.challenge-arrow {
    font-family: var(--game-font);
    font-size: 28.8px;
    font-weight: bold;
    color: var(--ink-strong);
}

.challenge-play-btn {
    appearance: none;
    margin-top: 8px;
    padding: 14px 28px;
    border: 3px solid #000;
    border-radius: 6px;
    background: #0032ff;
    color: #fff;
    font-family: var(--game-font);
    font-size: 28.8px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 5px 0 rgba(0,0,0,0.28);
    align-self: flex-start;
}

.challenge-play-btn:hover { background: #0028cc; }
.challenge-play-btn:active { transform: translateY(3px); box-shadow: 0 2px 0 rgba(0,0,0,0.28); }

/* ===========================
   MULTIPLAYER
   =========================== */

.mp-home {
    max-width: 620px;
    margin: 0 auto;
    padding: 36px 40px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 6px 0 rgba(0,0,0,0.22), inset 0 0 0 3px rgba(176,176,255,0.45);
}

/* The row-menu screens (Multiplayer, Create Room) drop the panel entirely —
   the rows sit straight on the page background, the way a Wii menu's list
   is never boxed in its own card. Wider too, since there is no panel edge to
   fit inside. */
.mp-home--bare {
    max-width: 1600px;
    padding: 20px 0 0;
    border: none;
    border-radius: 0;
    background: none;
    box-shadow: none;
}

.mp-title {
    /* On a light panel the polarity flips: dark ink, white edge. A black edge
       here would be a black box round every letter, and white ink would
       disappear — there is no photograph underneath to hide against. */
    --edge: #fff;
    --edge-w: 3px;
    font-family: var(--game-font);
    font-size: 57.6px;
    font-weight: bold;
    color: var(--ink-link);
    margin-bottom: 6px;
}

/* Bare screens have a photo underneath instead of a panel, so the title takes
   the same white-on-black treatment card art uses (see .cardtitle) rather
   than the panel's ink-on-white. */
.mp-home--bare .mp-title {
    --edge: #000;
    --edge-w: 3px;
    color: #fff;
}

.mp-subtitle {
    font-family: var(--game-font);
    font-size: 25.6px;
    color: var(--ink-muted);
    margin-bottom: 28px;
}

/* ── Multiplayer menu rows ──────────────────────────────────────────────────
   Stacked full-width banners rather than a card grid: one option per row, a
   dark navy gradient with a faint horizontal stripe texture — same recipe as
   a Wii menu's unselected rows. Size and spacing (row height, gap above each
   row) were hand-tuned with a dev-only slider/dragger tool, since removed —
   these are just the values it landed on. */
.mp-menu-list {
    position: relative;
    display: flex;
    flex-direction: column;
    margin-bottom: 8px;
}

/* .goldframe2/.goldframe3 normally size themselves to 100% of a card slot
   with a fixed height set by their parent; here it's the opposite — the
   button inside carries the real height and both frame layers just
   shrink-wrap around it, so 100% would be circular (auto has nothing to
   size against until the button underneath is measured). */
.mp-row-frame.goldframe2,
.mp-row-frame .goldframe3 {
    width: 100%;
    height: auto;
}

.mp-row-frame {
    position: relative;
    margin-top: 61px;
    /* Same entrance the main menu's cards get on arrival (see
       .grid-container.is-entering / @keyframes cardSlideInRight above) —
       each row slides in from the right, staggered by --row-delay (set per
       row in renderMenuRows: index * 45ms, the same stagger createCard()
       uses), so the first row leads and each one after follows a beat
       behind it rather than the whole list popping in at once. `backwards`
       holds the from-state until the delay elapses, so a late row doesn't
       flash in place before its turn. */
    animation: cardSlideInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
    animation-delay: var(--row-delay, 0ms);
    transition: transform 0.12s ease;
}

.mp-menu-list > .mp-row-frame:first-child { margin-top: 0; }

/* Single Player's menu reuses these rows (createMenuRow in script.js). Its
   container is a plain card-screen grid, so it takes the Multiplayer
   shell's width and top gap here, and the card screens' enter/exit slides
   are pointed at the rows instead of at .glass. */
.row-menu {
    display: block;
    width: 100%;
    max-width: min(980px, calc(100vw - 40px));
    margin: 0 auto;
    padding-top: 20px;
}
/* Six rows against Multiplayer's four: at Multiplayer's 61px gap MenuFit
   shrinks the whole list to ~63%, so the rows stop matching in size. A
   tighter gap keeps them close to full size instead. */
.row-menu .mp-row-frame { margin-top: 24px; }
.row-menu .mp-menu-list > .mp-row-frame:first-child { margin-top: 0; }
.row-menu.is-exiting .mp-row-frame { animation: cardSlideOutLeft 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards; animation-delay: var(--row-delay); }
.row-menu.is-entering .mp-row-frame { animation: cardSlideInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards; animation-delay: var(--row-delay); }
.row-menu.is-exiting-back .mp-row-frame { animation: cardSlideOutRight 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards; animation-delay: var(--row-delay); }
.row-menu.is-entering-back .mp-row-frame { animation: cardSlideInLeft 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards; animation-delay: var(--row-delay); }

.mp-row-frame:hover,
.mp-row-frame:focus-within {
    transform: translateX(4px);
}

.mp-row-frame:active { transform: translateX(4px) translateY(2px); }

.mp-row {
    appearance: none;
    position: relative;
    display: flex;
    align-items: center;
    gap: 18px;
    width: 100%;
    height: 97px;
    padding: 0 28px;
    border: 3px solid #000;
    border-radius: 8px;
    background: repeating-linear-gradient(90deg, var(--theme-wash, #fefefe),
            var(--theme-wash, #fefefe) 3px,
            var(--theme-wash2, #f2f2f2) 3px,
            var(--theme-wash2, #f2f2f2) 6px);
    overflow: hidden;
    cursor: pointer;
    font: inherit;
    text-align: left;
    box-shadow: 0 4px 0 rgba(0,0,0,0.28);
    outline: none;
}

.mp-row-frame:hover .mp-row,
.mp-row-frame:focus-within .mp-row {
    border-color: var(--accent, #d0d20d);
}

/* Faint horizontal stripes, the same texture a Wii menu's unselected rows
   carry, so a row reads as "made of the same stuff" whether or not it is the
   one currently highlighted. */
.mp-row::before {
    content: "";
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(0deg, rgba(255,255,255,0.05) 0 2px, transparent 2px 5px);
    pointer-events: none;
}

/* Wraps the icon + title so the two of them can be centred as one unit in
   whatever width is left after the badge (see .mp-row-badge below) claims
   its own space — centering the button directly wouldn't work for the rows
   that carry a badge, since the badge has to stay pinned to the right edge
   regardless of how the label is aligned. */
.mp-row-label {
    position: relative;
    z-index: 1;
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 18px;
}

.mp-row-icon {
    width: 44px;
    flex-shrink: 0;
    font-size: 30px;
    font-weight: bold;
    line-height: 1;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    /* The row has no fill behind it any more, so the glyph needs the same
       black edge the title gets to stay legible over an arbitrary photo. */
    text-shadow: 0 0 4px #000, 0 0 4px #000, 0 1px 2px #000;
}

.mp-row-title {
    /* Row has no background fill any more — same white-on-black-edge
       treatment as card art text (see the shared selector above). Font stays
       on Arial rather than --game-font: renderMenuRows() is Multiplayer-only
       (Quick Match/Ranked/Create Room/Join Room, and Public/Private Room),
       so there's no other screen to keep in sync by reverting it here. */
    --edge: #000;
    --edge-w: 2px;
    font-family: Arial, sans-serif;
    font-size: 48px;
    font-weight: bold;
    color: #fff;
}

/* Count pill on Ranked / Join Room — see updateMenuBadges() in script.js
   for what each one counts and why Join Room's can hide (0 public rooms is
   really nothing to join). .mp-row-label's flex: 1
   already claims all the space that isn't the badge's, so this just has to
   not shrink. */
.mp-row-badge {
    position: relative;
    z-index: 1;
    flex-shrink: 0;
    min-width: 30px;
    padding: 5px 12px;
    border: 2px solid var(--accent, #d0d20d);
    border-radius: 999px;
    background: #000;
    color: #fff;
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: 900;
    text-align: center;
}

.mp-note, .mp-gate-text {
    font-family: var(--game-font);
    font-size: 22.4px;
    color: #aaa;
    text-align: center;
    margin-top: 4px;
}

.mp-gate-text {
    color: var(--ink-muted);
    margin-bottom: 10px;
    text-align: left;
}

.mp-back-btn {
    appearance: none;
    margin-top: 16px;
    padding: 10px 26px 10px 20px;
    border: 3px solid #000;
    border-radius: 999px;
    background: linear-gradient(180deg, #fff, #dfe3f5);
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0,0,0,0.2);
    transition: transform 0.1s ease, box-shadow 0.1s ease;
}

.mp-back-btn:hover { background: linear-gradient(180deg, #fff, #eef0ff); }
.mp-back-btn:active { transform: translateY(2px); box-shadow: 0 2px 0 rgba(0,0,0,0.2); }

@media (max-width: 480px) {
    .grid-container {
        gap: 20px;
    }

    body > main {
        padding: 10px;
    }

    :root { --chrome-h: calc(50px * var(--wdm-k, 1)); --logo-clear: 144px; }  /* wordmark 30px at left:16 -> ends at 130 */

    .back-button {
        left: 12px;
        width: 48px;
        height: 42px;
    }

    .back-button::before {
        left: 18px;
        width: 18px;
        height: 18px;
        border-left-width: 5px;
        border-bottom-width: 5px;
    }
}

/* ===========================
   CHALLENGE LIST
   =========================== */

.challenge-daily-strip {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    margin-bottom: 20px;
    padding: 16px 22px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 4px 0 rgba(0,0,0,0.2), inset 0 0 0 3px rgba(176,176,255,0.45);
}

.challenge-daily-info {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.challenge-pill--sm {
    font-size: 16px;
    padding: 6px 12px;
}

.challenge-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 20px;
}

.ch-card {
    appearance: none;
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 18px;
    border: 3px solid #000;
    border-radius: 7px;
    padding: 20px 24px 20px 30px;
    background: linear-gradient(135deg, rgba(255,255,255,0.97) 55%, color-mix(in srgb, var(--ch-accent, #0032ff) 10%, rgba(255,255,255,0.97)));
    box-shadow: 0 5px 0 rgba(0,0,0,0.22), inset 0 0 0 3px rgba(176,176,255,0.45);
    text-align: left;
    cursor: pointer;
    font: inherit;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
    animation: cardSlideInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
    animation-delay: var(--row-delay);
}

.ch-card::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 8px;
    background: var(--ch-accent, #0032ff);
}

.ch-card:hover {
    transform: scale(1.03);
    box-shadow:
        0 5px 0 rgba(0,0,0,0.22),
        0 0 22px color-mix(in srgb, var(--ch-accent, #0032ff) 50%, transparent),
        inset 0 0 0 3px rgba(176,176,255,0.45);
}

.ch-card:hover .ch-name { color: var(--ch-accent, #0032ff); }

.ch-card:hover .ch-icon span {
    display: inline-block;
    animation: fxWiggle 0.6s ease;
}

.ch-icon {
    flex-shrink: 0;
    width: 62px;
    height: 62px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 32px;
    line-height: 1;
    background: color-mix(in srgb, var(--ch-accent, #0032ff) 16%, #fff);
    border: 2px solid color-mix(in srgb, var(--ch-accent, #0032ff) 45%, #fff);
    box-shadow: inset 0 -4px 0 color-mix(in srgb, var(--ch-accent, #0032ff) 22%, #fff);
}

@keyframes fxWiggle {
    0%, 100% { transform: rotate(0) scale(1); }
    25%      { transform: rotate(-13deg) scale(1.12); }
    55%      { transform: rotate(11deg) scale(1.15); }
    80%      { transform: rotate(-5deg) scale(1.05); }
}

.ch-name {
    font-family: var(--game-font);
    font-size: 32px;
    font-weight: bold;
    color: var(--ink);
    margin-bottom: 5px;
    transition: color 0.15s ease;
}

.ch-desc {
    font-family: var(--game-font);
    font-size: 20.8px;
    line-height: 1.45;
    color: var(--ink-muted);
}

.ch-intro {
    position: relative;
    overflow: hidden;
    max-width: 620px;
    margin: 0 auto;
    padding: 40px 40px 36px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(160deg, rgba(255,255,255,0.97) 60%, color-mix(in srgb, var(--ch-accent, #0032ff) 10%, rgba(255,255,255,0.97)));
    box-shadow: 0 6px 0 rgba(0,0,0,0.22), inset 0 0 0 3px rgba(176,176,255,0.45);
}

.ch-intro::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 8px;
    background: var(--ch-accent, #0032ff);
}

.ch-intro .challenge-eyebrow { color: var(--ch-accent, #0032ff); }

.ch-intro-icon {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 48px;
    line-height: 1;
    margin-bottom: 18px;
    background: color-mix(in srgb, var(--ch-accent, #0032ff) 16%, #fff);
    border: 2px solid color-mix(in srgb, var(--ch-accent, #0032ff) 45%, #fff);
    box-shadow: inset 0 -5px 0 color-mix(in srgb, var(--ch-accent, #0032ff) 22%, #fff);
}

.ch-intro-icon span {
    display: inline-block;
    animation: fxFloatIdle 3s ease-in-out infinite;
}

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

.ch-intro-name {
    font-family: var(--game-font);
    font-size: 48px;
    font-weight: bold;
    color: var(--ink);
    margin: 6px 0 14px;
}

.ch-intro-desc {
    font-family: Georgia, serif;
    font-size: 18px;
    color: var(--ink-mid);
    line-height: 1.7;
    margin-bottom: 14px;
}

.ch-intro-note {
    font-family: var(--game-font);
    font-size: 22.4px;
    color: var(--ink-faint);
    margin-bottom: 26px;
}

@media (max-width: 700px) {
    .challenge-grid { grid-template-columns: 1fr; }
}

/* ===========================
   CHALLENGE EFFECTS
   =========================== */

/* Alone in the Dark */
.fx-darkness {
    position: fixed;
    inset: 0;
    z-index: 950;
    pointer-events: none;
    background: radial-gradient(
        circle 190px at var(--fx-x, 50%) var(--fx-y, 50%),
        rgba(0, 0, 0, 0) 0%,
        rgba(0, 0, 0, 0.25) 55%,
        rgba(0, 0, 0, 0.93) 80%,
        rgba(0, 0, 0, 0.995) 100%
    );
}

/* The Files */
/* The classified document itself stays fully readable — it's the links that
   are censored. Every link (navigable move or dead reference) becomes a solid
   black bar; the navigable ones are still clickable underneath, so you have to
   guess which redactions actually lead somewhere. */
body.fx-redacted .wiki-link-inline,
body.fx-redacted .wiki-link-dead {
    color: transparent !important;
    background: #1a1a1a !important;
    border-radius: 2px;
    text-decoration: none !important;
    box-shadow: 0 0 0 1px #1a1a1a;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

body.fx-redacted .wiki-link-inline img,
body.fx-redacted .wiki-link-dead img {
    filter: brightness(0);
}

/* Upside Down */
/* Only the article itself: the middle column of .wiki-skin. The contents
   column, the appearance column and everything around the skin (header, menus,
   chat) stay upright, so the joke costs you the reading, not the interface.
   Children of the skin only, so the nested <main class="wiki-page"> inside a
   bare skin is still the one rotated exactly once. */
body.fx-upside-down .wiki-skin > .wiki-page {
    transform: rotate(180deg);
}

/* Earthquake */
/* Shake the page content, not the stage itself — the stage carries the
   equipped theme's background art, which should hold still. */
body.fx-quake .article-stage .wiki-skin {
    animation: fxQuake 0.28s linear infinite;
}

.fx-quake-debris {
    position: fixed;
    z-index: 600;
    margin: 0;
    padding: 0;
    white-space: nowrap;
    pointer-events: none;
    will-change: transform, opacity;
}

@keyframes fxQuake {
    0%   { transform: translate(0, 0) rotate(0); }
    20%  { transform: translate(-7px, 4px) rotate(-0.4deg); }
    40%  { transform: translate(6px, -5px) rotate(0.35deg); }
    60%  { transform: translate(-5px, -6px) rotate(0.3deg); }
    80%  { transform: translate(7px, 5px) rotate(-0.3deg); }
    100% { transform: translate(0, 0) rotate(0); }
}

/* Virtual cursor (wind / storm / ice / tasty) */
body.fx-hide-cursor,
body.fx-hide-cursor * {
    cursor: none !important;
}

.fx-cursor {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 990;
    width: 26px;
    height: 26px;
    pointer-events: none;
    transform-origin: 0 0;
    
    transition: none;
}

.fx-cursor svg {
    display: block;
    width: 100%;
    height: 100%;
}

.fx-cursor--skin {
    
}

.fx-cursor--skin img {
    /* Size set inline by fillFxCursor at a whole-number scale — stretching to
       100% of the 26px box would land on a fractional one. */
    display: block;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
}

/* Emoji cursor skins: nudge so the glyph sits where its hotspot would be. */
.fx-cursor--emoji span {
    display: block;
    font-size: 24px;
    line-height: 26px;
    transform: translate(-6px, -4px);
}

/* Math quiz */
/* Pop Quiz link trap (multiplayer): a bare purple calculator pinned onto the
   cursed link, inside the article scroll — deliberately primitive. */
.fx-mathpop {
    position: absolute;
    z-index: 350;
    width: 168px;
    padding: 8px;
    background: #7a1fd0;
    font-family: var(--game-font);
}

.fx-mathpop-eq {
    margin-bottom: 6px;
    color: #fff;
    font-size: 18px;
    font-weight: bold;
    text-align: center;
}

.fx-mathpop-input {
    display: block;
    width: 100%;
    height: 30px;
    padding: 0 6px;
    margin-bottom: 4px;
    border: 0;
    border-radius: 0;
    box-sizing: border-box;
    background: var(--surface);
    color: var(--ink-strong);
    font-family: var(--game-font);
    font-size: 28.8px;
    font-weight: bold;
    text-align: right;
}

.fx-mathpop-msg {
    min-height: 14px;
    margin-bottom: 4px;
    color: #ff2020;
    font-size: 12px;
    font-weight: bold;
    text-align: center;
}

.fx-mathpop-pad {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 4px;
}

.fx-mathpop-key {
    appearance: none;
    padding: 8px 0;
    border: 0;
    border-radius: 0;
    background: var(--surface);
    color: var(--ink-strong);
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    cursor: pointer;
}

.fx-mathpop-key:active { background: #ccc; }

.fx-mathpop-key--zero { grid-column: span 2; }

.fx-mathpop-go {
    appearance: none;
    border: 0;
    border-radius: 0;
    background: #1faf3c;
    color: #fff;
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: bold;
    cursor: pointer;
}

.fx-mathpop-go:active { background: #178a2f; }

.fx-math-overlay {
    position: fixed;
    inset: 0;
    z-index: 970;
    display: grid;
    place-items: center;
    background: rgba(10, 15, 40, 0.55);
}

.fx-math-card {
    min-width: min(400px, calc(100vw - 48px));
    padding: 30px 38px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 6px 0 rgba(0,0,0,0.3), inset 0 0 0 3px rgba(176,176,255,0.45);
    text-align: center;
}

.fx-math-card.is-shake {
    animation: fxMathShake 0.3s linear;
}

@keyframes fxMathShake {
    0%   { transform: translateX(0); }
    25%  { transform: translateX(-9px); }
    50%  { transform: translateX(8px); }
    75%  { transform: translateX(-5px); }
    100% { transform: translateX(0); }
}

.fx-math-title {
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    letter-spacing: 2px;
    color: var(--ink-link);
    margin-bottom: 12px;
}

.fx-math-eq {
    font-family: var(--game-font);
    font-size: 67.2px;
    font-weight: bold;
    color: var(--ink);
    margin-bottom: 20px;
}

.fx-math-row {
    display: flex;
    gap: 10px;
    justify-content: center;
}

.fx-math-input {
    width: 130px;
    padding: 10px 14px;
    border: 3px solid #000;
    border-radius: 6px;
    font-family: var(--game-font);
    font-size: 35.2px;
    font-weight: bold;
    text-align: center;
}

.fx-math-input:focus {
    outline: none;
    border-color: #0032ff;
}

.fx-math-btn {
    appearance: none;
    padding: 10px 22px;
    border: 3px solid #000;
    border-radius: 6px;
    background: #0032ff;
    color: #fff;
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0,0,0,0.28);
}

.fx-math-btn:hover { background: #0028cc; }
.fx-math-btn:active { transform: translateY(2px); box-shadow: 0 2px 0 rgba(0,0,0,0.28); }

.fx-math-feedback {
    min-height: 22px;
    margin-top: 12px;
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: bold;
    color: #c62828;
}

/* Toast (déjà vu, babel, tasty) */
.fx-toast {
    position: fixed;
    bottom: 116px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 985;
    padding: 12px 24px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 4px 0 rgba(0,0,0,0.25);
    font-family: var(--game-font);
    font-size: 28.8px;
    font-weight: bold;
    color: var(--ink);
    opacity: 0;
    transition: opacity 200ms ease;
    pointer-events: none;
}

.fx-toast.is-visible {
    opacity: 1;
}

/* Mid-race connection drop (public/multiplayer-screen.js showReconnecting).
   The toast's panel, pinned to the top and held until the race is back or
   given up. */
.mp-reconnecting {
    position: fixed;
    top: 16px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 990;
    max-width: calc(100vw - 32px);
    padding: 10px 20px;
    border: 3px solid #000;
    border-radius: 0;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 4px 0 rgba(0,0,0,0.25);
    font-family: var(--game-font);
    font-size: 20px;
    font-weight: bold;
    color: var(--ink);
    pointer-events: none;
}

/* First-visit usage-stats notice (public/script.js mountStatsNotice). Same
   panel language as .fx-toast above (border/radius/gradient/shadow), but
   persistent rather than a timed fade, and with buttons — this is the
   opt-out, not a flourish. */
.stats-notice {
    position: fixed;
    left: 50%;
    bottom: 16px;
    transform: translateX(-50%);
    z-index: 986;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
    width: min(520px, calc(100vw - 24px));
    padding: 14px 18px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 4px 0 rgba(0,0,0,0.25);
    font-family: var(--game-font);
    color: var(--ink);
}

.stats-notice p {
    margin: 0;
    flex: 1 1 240px;
    font-size: 13px;
    line-height: 1.45;
}

.stats-notice a {
    color: var(--ink-link);
}

.stats-notice-actions {
    display: flex;
    gap: 8px;
    flex: 0 0 auto;
    margin-left: auto;
}

.stats-notice button {
    font-family: var(--game-font);
    font-size: 12px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: .02em;
    padding: 8px 12px;
    border: 2px solid #000;
    border-radius: 5px;
    cursor: pointer;
    white-space: nowrap;
}

.stats-notice-ok {
    background: var(--accent);
    color: #000;
}

.stats-notice-off {
    background: transparent;
    color: var(--ink);
}

/* Alone in the Dark: eyes watching from the darkness */
.fx-eyes {
    position: fixed;
    z-index: 960;
    display: flex;
    gap: 10px;
    pointer-events: none;
    animation: fxEyesBlink 3.6s ease forwards;
}

.fx-eyes span {
    width: 11px;
    height: 15px;
    border-radius: 50%;
    background: radial-gradient(circle at 50% 40%, #fffde7, #ffca28);
    box-shadow: 0 0 14px 4px rgba(255, 202, 40, 0.45);
}

@keyframes fxEyesBlink {
    0%, 100% { opacity: 0; }
    12%, 40% { opacity: 1; transform: scaleY(1); }
    46%      { opacity: 0.9; transform: scaleY(0.08); }
    52%, 86% { opacity: 1; transform: scaleY(1); }
}

/* Déjà Vu: glitch flash on article swap */
.fx-glitch {
    position: fixed;
    inset: 0;
    z-index: 968;
    pointer-events: none;
    background:
        repeating-linear-gradient(0deg,
            rgba(142, 36, 170, 0.16) 0 3px,
            transparent 3px 8px),
        rgba(142, 36, 170, 0.08);
    animation: fxGlitch 650ms steps(7) forwards;
}

@keyframes fxGlitch {
    0%   { opacity: 1; transform: translateX(0) skewX(0); }
    18%  { transform: translateX(-16px) skewX(4deg); }
    36%  { transform: translateX(12px) skewX(-3deg); opacity: 0.85; }
    54%  { transform: translateX(-8px) skewX(2deg); }
    72%  { transform: translateX(5px) skewX(-1deg); opacity: 0.6; }
    100% { opacity: 0; transform: translateX(0) skewX(0); }
}

/* ═══ Multiplayer ═══════════════════════════════════════════════════════════ */

.mp-error {
    margin: 0;
    padding: 8px 14px;
    border: 2px solid #b71c1c;
    border-radius: 6px;
    background: #ffebee;
    color: #b71c1c;
    font-family: var(--game-font);
    font-weight: bold;
    font-size: 22.4px;
}

.mp-name-input,
.mp-code-input {
    padding: 10px 14px;
    border: 3px solid #000;
    border-radius: 8px;
    background: var(--surface);
    color: var(--ink-link);
    font-family: var(--game-font);
    font-size: 28.8px;
    font-weight: bold;
    text-align: center;
    outline: none;
}

.mp-name-input:focus,
.mp-code-input:focus {
    border-color: #0032ff;
}

.mp-name-input.is-error {
    border-color: #b71c1c;
    background: #ffebee;
}

.mp-code-input {
    width: 160px;
    font-size: 26px;
    letter-spacing: 6px;
    text-transform: uppercase;
}

/* A username is not a room code: it needs room for real words, and forcing it
   to caps would misrepresent the name being searched for. */
.mp-code-input--name {
    width: 260px;
    font-size: 18px;
    letter-spacing: 0;
    text-transform: none;
}

.mp-rank-badge {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 8px 18px;
    border: 3px solid #000;
    border-radius: 8px;
    background: linear-gradient(180deg, #ffe082, #f6d83f);
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.25);
}

.mp-rank-tier {
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: 900;
    color: #4a3200;
    text-transform: uppercase;
}

.mp-rank-rating {
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    color: #6d5400;
}

wd-loader {
    display: inline-block;
    width: var(--loader-size, 120px);
    height: var(--loader-size, 120px);
    max-width: 100%;
    flex: 0 0 auto;
    vertical-align: middle;
    pointer-events: none;
}
wd-loader canvas { display: block; width: 100%; height: 100%; }
wd-loader.mp-queue-loader { display: block; --loader-size: 160px; margin-inline: auto; }
/* The Quick Match queue is a room while it fills: everyone's pointer (and the
   bots' wandering ones) is drawn over this card, measured against its box. */
.mp-home--queue { position: relative; max-width: 760px; min-height: 440px; overflow: hidden; }
.mp-queue-roster { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; max-width: 460px; margin: 14px auto 0; }
.mp-queue-seat {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 6px 13px; border: 2px solid #c8ccd1; border-radius: 20px;
    font-family: var(--game-font); font-size: 13px; font-weight: 800;
    color: var(--ink-navy); background: #fff;
}
.mp-queue-seat.is-me { border-color: #0032ff; }
.mp-queue-seat.is-empty { border-style: dashed; color: #8891a0; background: none; opacity: .7; }
.article-loading > wd-loader { display: block; --loader-size: 160px; margin: 0 auto 8px; }
.channel-loading { flex-direction: column; gap: 8px; }
.credits-status > wd-loader, .room-music-warn > wd-loader { display: block; margin: 0 auto 8px; }
wd-loader.loader-inline { --loader-size: 32px; margin-inline-end: 6px; }

.mp-join-form {
    display: flex;
    align-items: center;
    gap: 12px;
}

.mp-play-btn {
    padding: 12px 30px;
    border: 3px solid #000;
    border-radius: 8px;
    background: linear-gradient(180deg, #fff, #dfe3f5);
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 30.4px;
    font-weight: 900;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.3);
    transition: transform 0.1s, background 0.1s;
}

.mp-play-btn:hover { background: linear-gradient(180deg, #fff, #eef0ff); transform: scale(1.04); }
.mp-play-btn:active { transform: translateY(2px); box-shadow: 0 2px 0 rgba(0, 0, 0, 0.3); }

.mp-room-code {
    padding: 10px 34px;
    border: 4px solid #000;
    border-radius: 10px;
    background: var(--surface);
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 70.4px;
    font-weight: 900;
    letter-spacing: 12px;
}

.mp-lobby-list,
.mp-result-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: min(100%, 420px);
}

.mp-lobby-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 10px;
    padding: 9px 16px;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    font-family: var(--game-font);
    font-weight: bold;
}

/* --mp-panel-alpha is a devtools-only lever, never a real setting: paste
   document.documentElement.style.setProperty('--mp-panel-alpha', '40%')
   into the console to preview a room's panels at that opacity. It has no
   default anywhere, so it does nothing until someone sets it by hand, and
   nothing here reads or writes it — see the color-mix() fallback (100%,
   fully opaque) each of these rules falls back to.
   Scoped to .mp-lobby-home rather than the bare .mp-lobby-row class: that
   class is also the results/spectate roster row, and those screens have no
   wallpaper showing through them to make a transparency lever worth having. */
.mp-lobby-home .mp-lobby-row {
    background: color-mix(in srgb, var(--surface) var(--mp-panel-alpha, 100%), transparent);
    /* The room roster only: cursor, then name+level, then (optionally) Trade.
       space-between is wrong here with just two children — it would fling
       the cursor and the name+level column to opposite ends of the row. */
    justify-content: flex-start;
}

.mp-lobby-home .mp-lobby-row .mp-rival-cursor {
    flex: none;
}

.mp-lobby-who {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
    gap: 1px;
}

/* Trade, on a rival's row in the lobby. Only appears for players who could
   actually take it — signed-in accounts, which is what canTrade carries. */
.mp-lobby-trade {
    appearance: none;
    padding: 4px 10px;
    border: 2px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, #ffe082, #f6d83f);
    font-family: var(--game-font);
    font-size: 17.6px;
    font-weight: 900;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: #4a3200;
    cursor: pointer;
}

.mp-lobby-trade:hover { filter: brightness(1.07); }

/* .mp-bot-add keeps its name: the lobby's bot bar is gone, but this is also
   the button style for Refresh, Join and Watch in the room and spectate
   browsers. */
.mp-bot-add {
    appearance: none;
    padding: 7px 12px;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.2);
}

.mp-bot-add:hover {
    background: #e3f2fd;
}

.mp-bot-add:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
}

.mp-lobby-row.is-me {
    background: #e3f2fd;
    border-color: #0032ff;
}

.mp-lobby-home .mp-lobby-row.is-me {
    background: color-mix(in srgb, #e3f2fd var(--mp-panel-alpha, 100%), transparent);
}

html.theme-midnight .mp-lobby-row,
html.theme-midnight .mp-lobby-left,
html.theme-midnight .mp-lobby-right,
html.theme-midnight .mp-chat {
    background: color-mix(in srgb, #000 var(--mp-panel-alpha, 100%), transparent);
    border: none;
}

.mp-lobby-name {
    color: #17233b;
    font-size: calc(16px * var(--mp-name-scale, 1.4));
    /* Black outline so the name stays legible over a player's full-colour
       theme banner (see themeBannerBackground). Four corners draw a clean 1px
       stroke on every side without thinning the glyph the way text-stroke does.
       !important opts back out of the flat UI's global `* { text-shadow: none
       !important }` reset, which would otherwise strip it. */
    text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000 !important;
}
/* A rainbow name is a gradient clipped to the glyphs with a transparent fill,
   so the text-shadow above smears black through the letters instead of
   outlining them (see the .is-rainbow note below). Drop the shadow and outline
   the rendered gradient with stacked drop-shadow filters instead — same trick
   .mp-lobby-cursor-name.is-rainbow uses — which keeps the colours and still
   gives the four-way black edge. */
.mp-lobby-name.is-rainbow {
    text-shadow: none !important;
    filter: drop-shadow(1px 0 0 #000) drop-shadow(-1px 0 0 #000)
            drop-shadow(0 1px 0 #000) drop-shadow(0 -1px 0 #000);
}
.mp-lobby-rating { color: #5b6474; font-size: 13px; }
.mp-lobby-name, .mp-lobby-level { font-family: 'Pretendard', sans-serif !important; font-weight: 700; }
/* Usernames keep the case the player typed (SneakyPiston), outside the house all-caps rule. */
.mp-lobby-name { text-transform: none; }
.mp-lobby-level {
  color: #fff !important;
  font-size: calc(13px * var(--mp-level-scale, 1.4));
  /* Same four-corner black outline as the name; !important beats the flat UI's text-shadow reset. */
  text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000 !important;
}
/* Scrim: dark layer over a theme banner, strongest behind the cursor and name, fading
   out to the right so the pattern still shows. z-index -1 inside an isolated row puts it
   above the row's own background but below its content. Strength: DEV MENU "Scrim". */
.mp-lobby-row.has-banner { position: relative; isolation: isolate; }
.mp-lobby-row.has-banner::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(90deg, rgb(0 0 0 / var(--mp-scrim, 0.6)) 0%,
    rgb(0 0 0 / calc(var(--mp-scrim, 0.6) * 0.8)) 45%, rgb(0 0 0 / 0) 100%);
}
.mp-lobby-level .flag { height: 0.88em; margin-right: 0.45em; vertical-align: -10%; }

.mp-lobby-btns {
    display: flex;
    align-items: center;
    gap: 16px;
}

/* ── Room lobby: header, tabs, settings, chat ── */

.mp-lobby-home {
    max-width: 940px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    /* Containing block for the rival-cursor layer below. */
    position: relative;
}

/* ── Rival cursors in the lobby ──
   A full-size overlay whose box is what positions are measured against, on
   both the sending and receiving side, so a fraction means the same place in
   two different windows. It must never take a click: the settings panel and
   the start button live underneath it. */
.mp-lobby-cursors {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 4;
}

.mp-lobby-cursor {
    position: absolute;
    top: 0;
    left: 0;
    /* Positions arrive about every 90ms; the transition carries the cursor
       between them so it glides instead of teleporting. Transform only —
       animating offsets here would relayout the panel under it. */
    transition: transform 90ms linear;
    will-change: transform;
    /* A column, so the name hangs under the pointer rather than beside it —
       beside, a long name ran out over the settings panel and the start
       button, and two rivals standing close overlapped each other's tags. */
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    opacity: 0;
}

.mp-lobby-cursor.is-live { opacity: 1; }

.mp-lobby-cursor img {
    image-rendering: pixelated;
    display: block;
}

/* The bundled hand sprite, stacked exactly over the arrow (both start at
   `.mp-lobby-cursor`'s own top-left — the arrow because it's the flex
   column's first item with no padding ahead of it, the hand because it's
   taken out of that flow to sit at the same origin) and swapped for it once
   moveLobbyCursor marks this rival's stream as hovering a button on their
   screen. Each keeps its own hotspot transform, since a hand sprite is not
   guaranteed to share the arrow's. */
.mp-lobby-cursor-hand {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
}

.mp-lobby-cursor.is-hover .mp-lobby-cursor-arrow { opacity: 0; }
.mp-lobby-cursor.is-hover .mp-lobby-cursor-hand { opacity: 1; }

/* The winner's trophy, floating over their own rival cursor on the results
   screen only — lobbyCursorEl adds this img solely for resultWinnerId, so
   its mere presence in the DOM is the whole condition. Positioned against
   `.mp-lobby-cursor` itself rather than sat in the flex column with the
   pointer art and name tag, so it can't nudge either of those out of place. */
.mp-lobby-cursor-trophy {
    position: absolute;
    /* `left: 0` anchors on the cursor's own hotspot point (x=0 of
       `.mp-lobby-cursor`, where the sprite img's -hotX/-hotY transform puts
       the actual pointer tip) rather than 50% of the whole flex column,
       which is as wide as the name tag underneath and pulled the trophy off
       to the right of narrower cursor art. Static — no bob animation, since
       an independent vertical wobble on top of the cursor's own gliding
       transform read as the trophy trailing loose behind it rather than
       sitting fixed to it. */
    left: 0;
    bottom: 100%;
    width: 44px;
    height: auto;
    margin-bottom: 4px;
    image-rendering: pixelated;
    
    transform: translate(-50%, 0);
}

/* A cursor standing on a podium step: the name tag is centred under the tip
   rather than running off to the right of it, so it stays over its own step. */
.mp-lobby-cursor.is-podium .mp-lobby-cursor-name {
    position: absolute;
    top: 100%;
    left: 0;
    transform: translateX(-50%);
    max-width: 150px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mp-lobby-cursor.is-podium { transform-origin: 0 0; }
.mp-lobby-cursor.is-warping { animation: mpPodiumWarpIn 380ms ease-out; }

/* The stand-in for the player's own pointer while it is away on a step. */
.fx-cursor.is-warping-out { z-index: 9300; animation: mpPodiumWarpOut 380ms ease-in forwards; }
.fx-cursor.is-warping-in { z-index: 9300; animation: mpPodiumWarpIn 380ms ease-out; }

@keyframes mpPodiumWarpIn {
    0%   { opacity: 0; scale: 0.2; filter: brightness(3) blur(3px); }
    60%  { opacity: 1; scale: 1.25; filter: brightness(1.8); }
    100% { opacity: 1; scale: 1; filter: none; }
}

@keyframes mpPodiumWarpOut {
    0%   { opacity: 1; scale: 1; filter: none; }
    100% { opacity: 0; scale: 0.2; filter: brightness(3) blur(3px); }
}

/* Plain text under the pointer, in that player's favourite colour — the one
   they answered the signup question with, set inline by lobbyCursorEl.

   No pill, no border: the tag is meant to read as a label on the person, not
   as another piece of the game's furniture, and the colour is the thing doing
   the identifying. What the pill was really buying was contrast, so that comes
   back as a hard dark outline instead — the lobby panel sits on the player's
   own wallpaper, which can be anything, and a mid-tone photo would otherwise
   swallow half the hues in the wheel. */
.mp-lobby-cursor-name {
    /* Clears the pointer art, which is drawn up and left of its layout box by
       the hotspot offset and so hangs below what the column reserves for it. */
    margin-top: 6px;
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: bold;
    line-height: 1.2;
    white-space: nowrap;
    color: #e8eaf5;
    text-shadow:
        -1px -1px 0 rgba(6, 8, 16, .9), 1px -1px 0 rgba(6, 8, 16, .9),
        -1px 1px 0 rgba(6, 8, 16, .9), 1px 1px 0 rgba(6, 8, 16, .9),
        0 2px 3px rgba(6, 8, 16, .55);
}

/* ── A label that changes colour as one ───────────────────────────────────
   Rainbow 2's name, written the way Rainbow 2 looks: not a spectrum spread
   across the letters but a single colour on all of them, moving through the
   wheel together.

   Animating `color` and not a clipped gradient, because that is the whole
   difference being drawn — and because it needs no background, so the three
   labels' own `color` rules lose to it on specificity alone. */
.is-solidcycle,
.cos-name.is-solidcycle,
.inv-item-name.is-solidcycle,
.mp-trade-item-name.is-solidcycle {
    animation: cosSolidCycle 3s linear infinite;
}

@keyframes cosSolidCycle {
    0%   { color: #f91f1f; }
    17%  { color: #f9c11f; }
    33%  { color: #90f91f; }
    50%  { color: #1ff9c1; }
    67%  { color: #1f60f9; }
    83%  { color: #c11ff9; }
    100% { color: #f91f1f; }
}

@media (prefers-reduced-motion: reduce) {
    /* Left on the colour its arrow ships in, rather than mid-sweep. */
    .is-solidcycle { animation: none; color: #f91f1f; }
}

/* ── The Chameleon's tile ─────────────────────────────────────────────────
   A shop tile has no page to sample, so it cannot show the skin doing its job
   by doing its job. It shows the RELATIONSHIP instead: a patch of colour with
   the arrow sitting on it, both rotated by one filter so they travel together
   and the arrow stays matched to its background the whole way round.

   The patch is the item's restHex (server/cursors.js CHAMELEON); the arrow is
   painted a lightened version of the same colour at ingest so it stands off the
   patch (the live pointer wears its colour exactly). Change one and change the
   other or the tile stops being a demonstration and starts being a lie. */
.cos-chamdemo {
    background: #4caf50;
    border-radius: 3px;
    animation: cosHueCycle 3s linear infinite;
}

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

/* ── A skin whose colours travel ──────────────────────────────────────────
   Worn by anything the catalog marks with hueCycleMs. This covers every
   surface that draws the sprite as an image — shop tile, inventory tile, trade
   window, and a rival's pointer in a lobby — with one compositor-level filter
   rather than repainting pixels in JS.

   The real pointer cannot come here: `cursor: url()` takes a still image, so
   that one walks a ring of pre-rendered PNGs instead (public/script.js
   startCursorHueCycle). Keep the 3s here in step with the item's hueCycleMs,
   or your own pointer and your tile in the shop drift apart. */
.cos-huecycle {
    animation: cosHueCycle 3s linear infinite;
}

@keyframes cosHueCycle {
    from { filter: hue-rotate(0deg); }
    to   { filter: hue-rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    /* Left on its authored colours rather than mid-rotation, so a skin nobody
       asked to see moving still looks like the thing they bought. */
    .cos-huecycle { animation: none; }
}

/* ── The Rainbow name ─────────────────────────────────────────────────────
   Worn with the Rainbow pointer and nothing else (public/script.js
   RAINBOW_CURSOR_ID). A name is normally written in one colour, taken from the
   last colour cursor its owner wore; this is the one skin that cannot be said
   in a hex, so it is painted as a gradient clipped to the glyphs instead.

   `-webkit-text-fill-color` and not just `color`: the chat name is also a
   button and the lobby row has its own ink, both of which set `color` at equal
   or higher specificity. text-fill-color is what actually paints the glyph, so
   it settles the argument wherever the class lands.

   The selectors are doubled up on purpose. `.mp-chat-who--action` sets
   `background: none`, and the three item labels each set their own `color`
   further down the file, all at equal specificity — this rule has to win from
   wherever it sits, so it says the element and the class rather than trusting
   source order. */
.is-rainbow,
.mp-chat-who.is-rainbow,
.cos-name.is-rainbow,
.inv-item-name.is-rainbow,
.mp-trade-item-name.is-rainbow {
    background-image: linear-gradient(90deg,
        #ff2d2d, #ff9f1c, #ffe600, #3ddc57, #23b5ff, #7b5cff, #ff2d2d);
    /* Double width, scrolled a full width per cycle, with the same red at both
       ends: that is what makes the loop seamless instead of snapping back. */
    background-size: 200% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    animation: rainbowName 4s linear infinite;
}

/* The Negative writes its name as the negative of whatever sits behind it:
   white text under a difference blend, which is exactly what the lens does to
   the page under the pointer. `color` is !important because every chat surface
   sets its own name ink at higher specificity (mine, hover, the in-game panel),
   and a tint here would turn the blend into a plain colour. */
.mp-chat-who.is-negative {
    color: #fff !important;
    mix-blend-mode: difference;
}

@keyframes rainbowName {
    from { background-position: 0 50%; }
    to   { background-position: 200% 50%; }
}

/* The cursor tag carries a hard four-way text-shadow for contrast, because it
   is read against whatever wallpaper the room's host chose. Clipped-to-text
   backgrounds and text-shadow do not mix — the shadow paints behind glyphs
   that are transparent, so it comes through as a smear where the letters
   should be. drop-shadow filters the rendered result instead, which keeps the
   name legible on a pale photo without touching the gradient. */
.mp-lobby-cursor-name.is-rainbow {
    text-shadow: none;
    
}

@media (prefers-reduced-motion: reduce) {
    /* The gradient stays, the travel stops. The colours are the item; the
       movement is decoration. */
    .is-rainbow,
    .mp-chat-who.is-rainbow,
    .cos-name.is-rainbow,
    .inv-item-name.is-rainbow,
    .mp-trade-item-name.is-rainbow { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
    .mp-lobby-cursor { transition: none; }
}

.mp-lobby-title {
    margin-bottom: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The host's click-to-rename affordance: same title styling (inherited via
   .mp-title in the shared selector list above), just reset out of button
   chrome and given a hover/focus cue so it reads as clickable without
   shouting about it. */
.mp-lobby-title-btn {
    appearance: none;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    text-decoration-style: dashed;
}

.mp-lobby-title-btn:hover,
.mp-lobby-title-btn:focus-visible {
    text-decoration: underline;
    text-decoration-style: dashed;
    text-underline-offset: 4px;
}

/* Also carries .mp-set-text (so the generic settings-field wiring in
   renderLobby treats it like any other text field) and .mp-title (for the
   shared edge-text treatment) — both declared elsewhere with a lower
   max-width, right-aligned text and a smaller size that would otherwise win
   on source order alone, so this is written two classes deep to outrank
   them on specificity instead of depending on where it sits in the file. */
.mp-lobby-head .mp-lobby-title-input {
    max-width: 100%;
    padding: 2px 10px;
    font-size: 57.6px;
    text-align: left;
    color: var(--ink-link);
}

.mp-lobby-meta {
    display: flex;
    align-items: center;
    gap: 10px;
}

.mp-room-code--small {
    padding: 2px 12px;
    font-size: 15px;
    letter-spacing: 2px;
    border-width: 2px;
}

/* The room code doubles as the copy-the-invite-link button. Stacked rather
   than inline — a "click to copy" label above a small code line — so the
   whole thing is shorter than the room title beside it, which is what lets
   the lobby columns below stretch a little further down the page. */
.mp-room-copy {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
    cursor: pointer;
    line-height: 1.15;
    /* The trailing icon sits outside the tracking, which would otherwise push
       it a full letter-space away from the code. */
    padding-right: 12px;
    transition: transform 90ms ease, background 90ms ease, color 90ms ease;
}

.mp-room-copy-label {
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.5px;
    opacity: 0.75;
}

.mp-room-copy-code {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.mp-room-copy svg {
    width: 0.7em;
    height: 0.7em;
    letter-spacing: normal;
}

.mp-room-copy:hover {
    background: var(--surface-hover, rgba(0, 0, 0, 0.06));
}

.mp-room-copy:active {
    transform: translateY(2px);
}

.mp-room-copy.is-copied {
    border-style: solid;
    border-color: #00a350;
    background: #00a350;
    color: #fff;
    /* "COPIED" is a word, not a code — the code's wide tracking makes it
       unreadable at this size. */
    letter-spacing: 2px;
}

.mp-room-copy.is-copy-failed {
    border-style: solid;
    border-color: #d10000;
    color: #d10000;
}

.mp-lobby-cols {
    display: grid;
    grid-template-columns: minmax(260px, 340px) minmax(320px, 1fr);
    gap: 18px;
    width: 100%;
    align-items: start;
    text-align: left;
}

/* ── The room at full width ───────────────────────────────────────────────
   A room is a workspace, not a card: the roster, the settings and the chat
   are all wanted at once, so it takes the whole window instead of sitting in
   the 980px box every other menu screen uses. Three columns, a header strip
   and an action bar — the shape a lobby wants, and the shape TETR.IO uses for
   the same job.

   Scoped to .mp-wide, which only the room view sets (multiplayer's own menu,
   the queue and the results stay centred cards). */
/* The stage normally pads 120px top and bottom to keep cards clear of the
   header and footer. A room wants that space, so in room mode it clears the
   chrome by exactly the chrome's height and nothing more. :not(.wiki-reading)
   because a race started from a room must get its scrolling article back. */
body.mp-room:not(.wiki-reading) {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
}

body.mp-room:not(.wiki-reading) main {
    min-height: 0;
    padding: 0 14px;
}

body.mp-room:not(.wiki-reading) .menu-stage {
    height: 100%;
    padding: calc(var(--chrome-h) + 10px) 0;
    /* visible, not hidden: .mp-wide .mp-lobby-btns deliberately escapes down
       onto the footer banner (see below), and hidden here would clip it —
       nothing else in a room is tall enough to overflow this box.
       No z-index here (see #menuGrid below): giving the stage itself the
       raised z-index put its own empty top padding — which reaches all the
       way up behind the header — above the header in paint order, which
       made #backButton unclickable in a room despite still being visible. */
    overflow: visible;
}

/* Carries the z-index .menu-stage used to have. #menuGrid sits below the
   stage's top padding (it never reaches into the header band the way the
   stage's own box does), so raising it above the footer's z-index: 100
   here no longer costs the header its clicks. Still beats the footer for
   the same reason as before: .mp-wide .mp-lobby-btns escapes down onto the
   footer banner (see below), and #menuGrid is the nearest z-indexed
   ancestor bounding where that escape gets to rank in the stacking order. */
body.mp-room:not(.wiki-reading) #menuGrid {
    position: relative;
    z-index: 101;
}

.multiplayer-container.mp-wide {
    max-width: none;
    width: 100%;
    height: 100%;
}

.mp-wide .multiplayer-screen { padding: 0; height: 100%; }

.mp-wide .mp-lobby-home {
    max-width: none;
    width: 100%;
    height: 100%;
    align-items: stretch;
    gap: 12px;
    /* .mp-home--bare's 20px top padding is meant for the row-menu screens
       above, which have no bottom counterpart to match against. A room does:
       its footer rides on a fixed .mp-lobby-btns with zero padding beneath
       the columns, so keeping the 20px here would leave the top gap to the
       header banner wider than the bottom gap to the footer banner. */
    padding-top: 0;
}

.mp-wide .mp-lobby-title { text-align: left; }

/* Room code + privacy chip ride the page's own top banner instead of the
   room's flow — merged into the chrome the same way .mp-lobby-btns merges
   into the footer below. Centred rather than tucked into a corner: the site
   wordmark owns the left half of the banner and the fixed profile button
   owns the right (see .profile-btn), so the middle is the only clear strip
   left for it. */
.mp-wide .mp-lobby-meta {
    position: fixed;
    left: 50%;
    top: calc(-1 * (var(--chrome-h) + 10px));
    transform: translateX(-50%);
    height: var(--chrome-h);
    z-index: 101;
}

/* roster | settings | chat */
.mp-wide .mp-lobby-cols {
    grid-template-columns: minmax(220px, 300px) minmax(360px, 1fr) minmax(260px, 360px);
    align-items: stretch;
    flex: 1 1 auto;
    min-height: 0;
}

.mp-wide .mp-lobby-left,
.mp-wide .mp-lobby-right,
.mp-wide .mp-chat { height: 100%; min-height: 0; }

/* The roster is a list that can outgrow its column; it scrolls, the page does not. */
.mp-wide .mp-lobby-left { overflow-y: auto; }
.mp-wide .mp-lobby-left .mp-lobby-list { width: 100%; }

/* Settings keep their own scroll — but against the full column height rather
   than the 380px cap the narrow layout needed, so on an ordinary screen there
   is nothing left to scroll. Without dropping the cap the panel would stop
   short and leave the column half empty. */
.mp-wide .mp-set-panel {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    overflow-y: auto;
}

/* Chat grows into its column instead of being a fixed 150px box. */
.mp-wide .mp-chat { justify-content: stretch; }
.mp-wide .mp-chat-log { flex: 1 1 auto; height: auto; min-height: 0; }
.mp-wide .mp-chat.has-lobby-game .mp-chat-log { flex-basis: 0; min-height: 76px; }

/* Action bar rides on the footer banner itself rather than the room's own
   flow. Taking it out of .mp-lobby-home's flex stack frees that height for
   the three columns to grow into — .mp-wide .mp-lobby-cols is already
   flex: 1 1 auto, so it just stretches on its own once nothing below it is
   claiming space. Start sits centred rather than hard right: it is the one
   thing everyone in the room is looking for, and the corner it would
   otherwise occupy belongs to the fixed Discord button. */
.mp-wide .mp-lobby-btns {
    position: fixed;
    left: 0;
    right: 0;
    /* Would be bottom: 0, but .multiplayer-screen sets will-change: transform
       for its page-transition animation, which makes IT the containing block
       for fixed descendants instead of the viewport — its padding box ends
       10px above the real bottom (see .menu-stage's own padding above). This
       walks that back out to the true viewport edge. */
    bottom: calc(-1 * (var(--chrome-h) + 10px));
    height: var(--chrome-h);
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    padding: 0 24px;
    z-index: 101;
}

.mp-wide .mp-lobby-btns .mp-play-btn,
.mp-wide .mp-lobby-btns .mp-note {
    grid-column: 2;
    justify-self: center;
}

/* Below three columns' worth of room, fall back to the stacked layout rather
   than crushing all three. Chat returns to the bottom, where it started. */
@media (max-width: 1100px) {
    .mp-wide .mp-lobby-cols {
        grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
    }
    .mp-wide .mp-chat { grid-column: 1 / -1; height: 220px; }
    .mp-wide .mp-chat.has-lobby-game {
        height: 520px;
        min-height: 440px;
    }
    .mp-wide .mp-lobby-home { height: auto; }
    .mp-wide .mp-lobby-left { overflow-y: visible; }
    /* Stacked, the room is taller than the window again, so give the page its
       scrolling back rather than clipping the action bar off the bottom. */
    body.mp-room:not(.wiki-reading),
    body.mp-room:not(.wiki-reading) .menu-stage {
        height: auto;
        overflow: visible;
    }
}

.mp-lobby-left,
.mp-lobby-right {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}

/* Its own panel, border and background included, so the roster reads as a
   box the same height as the chat and gamerules panels beside it instead of
   however tall its player rows happen to stack up. .mp-lobby-right shares
   the treatment so the room title and tabs sit inside the same card as the
   settings below them, rather than floating above a shorter inner box. */
.mp-lobby-left,
.mp-lobby-right {
    border: 2px solid #000;
    border-radius: 8px;
    padding: 8px;
    background: color-mix(in srgb, var(--surface) var(--mp-panel-alpha, 100%), transparent);
}

.mp-lobby-left .mp-lobby-list { width: 100%; }

.mp-lobby-row--empty {
    justify-content: center;
    border-style: dashed;
    background: color-mix(in srgb, var(--surface) 55%, transparent);
    color: #8a92a5;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* Chat */

.mp-chat {
    display: flex;
    flex-direction: column;
    gap: 6px;
    border: 2px solid #000;
    border-radius: 8px;
    /* --mp-panel-alpha: devtools-only, see the comment above .mp-lobby-row. */
    background: color-mix(in srgb, var(--surface) var(--mp-panel-alpha, 100%), transparent);
    padding: 8px;
}

.mp-chat-log {
    height: 150px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 3px;
    font-family: var(--chat-font);
    font-size: 16px;
    color: var(--ink-navy);
    /* Chat is the one place carrying other players' own words rather than the
       game's copy — forcing it to caps would misrepresent what they typed,
       same reasoning as .mp-code-input--name below. */
    text-transform: none;
}

.mp-chat-line { word-break: break-word; }

/* The only reachable path back to a muted player's mute/report menu once
   their messages have stopped appearing in the log to click on. */
.mp-muted-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 4px 0;
    font-size: 12px;
    color: var(--ink-faint);
}

.mp-muted-chip {
    padding: 2px 8px;
    border: 1px solid var(--ink-faint);
    border-radius: 999px;
    background: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.mp-muted-chip:hover,
.mp-muted-chip:focus-visible {
    color: var(--ink-link);
    border-color: var(--ink-link);
}

/* Your own line used to be marked by painting the name #0032ff. The name now
   carries the colour of the cursor you are wearing (see chatInk), so the
   marker moves off the name and onto the row: it did not need to be a colour
   to do its job, and as a colour it was overwriting the one thing this line is
   now supposed to show. A fixed blue also ignored the accent and theme colours,
   so it was already the odd one out. */
.mp-chat-line.is-me {
    padding: 0 4px;
    border-radius: 3px;
    background: color-mix(in srgb, var(--ink-navy) 8%, transparent);
}
.mp-chat-who { font-weight: 900; }

.mp-chat-line--system {
    color: var(--ink-faint);
    font-style: italic;
}

/* Another player's name is the handle for muting/reporting them: it has to
   read as a name first and a button second, so it only reveals itself on
   hover. */
.mp-chat-who--action {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-weight: 900;
    color: inherit;
    cursor: pointer;
}

.mp-chat-who--action:hover,
.mp-chat-who--action:focus-visible {
    text-decoration: underline;
    color: var(--ink-link);
}

/* ── Trading ──
   The window sits on <body>, not in the multiplayer panel: a lobby re-renders
   on every join, chat line and settings edit, and a trade can outlive the
   screen it started on. It is deliberately modal — a trade is the one thing in
   this game that permanently moves something between two accounts, and it
   should not be possible to do it by accident while doing something else. */
.mp-trade-wrap {
    position: fixed;
    inset: 0;
    z-index: 950;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(6, 10, 26, 0.55);
    font-family: var(--game-font);
}

.mp-trade {
    width: min(720px, 100%);
    max-height: 100%;
    overflow-y: auto;
    padding: 16px 18px 18px;
    border: 3px solid #000;
    border-radius: 14px;
    background: var(--surface);
    box-shadow: 0 8px 0 rgba(0, 0, 0, 0.3);
}

.mp-trade-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.mp-trade-title {
    font-size: 20px;
    font-weight: 900;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--ink-navy);
}

.mp-trade-close {
    appearance: none;
    width: 30px;
    height: 30px;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    font-size: 16px;
    font-weight: 900;
    line-height: 1;
    color: var(--ink-navy);
    cursor: pointer;
}

/* Two ends of one table, side by side and the same size — a trade window that
   gave one side more room would be reading as an opinion about the deal. */
.mp-trade-sides {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.mp-trade-side {
    display: flex;
    flex-direction: column;
    padding: 10px;
    border: 2px solid #000;
    border-radius: 10px;
    background: color-mix(in srgb, var(--surface) 92%, #000);
}

/* Ready is a state worth seeing from across the window, since it is the thing
   that decides whether the next click moves anything. */
.mp-trade-side.is-confirmed {
    border-color: #1faf3c;
    background: color-mix(in srgb, #43d95e 12%, var(--surface));
}

.mp-trade-side-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 8px;
}

.mp-trade-who {
    font-weight: 900;
    color: var(--ink-navy);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mp-trade-ready {
    font-size: 11px;
    font-weight: 900;
    letter-spacing: 1px;
    color: #1faf3c;
}

.mp-trade-slots {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    min-height: 92px;
    align-content: flex-start;
}

.mp-trade-item {
    position: relative;
    width: 84px;
    padding: 6px 4px 5px;
    border: 2px solid #000;
    border-radius: 8px;
    background: radial-gradient(120% 90% at 50% 0%,
        color-mix(in srgb, var(--rar, #9aa3b2) 42%, #fff) 0%, #fdfdff 78%);
    font-family: var(--game-font);
    text-align: center;
    cursor: default;
}

button.mp-trade-item { cursor: pointer; }

.mp-trade-item-art {
    display: grid;
    place-items: center;
    height: 58px;
    overflow: hidden;
}

.mp-trade-item-name {
    font-size: 10px;
    font-weight: bold;
    line-height: 1.2;
    color: var(--ink-navy);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Only on your own side: theirs is not yours to edit. */
.mp-trade-item-x {
    position: absolute;
    top: -7px;
    right: -7px;
    width: 18px;
    height: 18px;
    border: 2px solid #000;
    border-radius: 50%;
    background: #e53935;
    font-size: 12px;
    font-weight: 900;
    line-height: 15px;
    color: #fff;
}

[data-trade-drop] { cursor: pointer; }

.mp-trade-add {
    appearance: none;
    width: 84px;
    min-height: 88px;
    border: 2px dashed #98a0bb;
    border-radius: 8px;
    background: transparent;
    font-size: 24px;
    font-weight: 900;
    color: #98a0bb;
    cursor: pointer;
}

.mp-trade-add:hover {
    border-color: #0032ff;
    color: var(--ink-link);
}

.mp-trade-empty {
    margin: 4px 2px;
    font-size: 12px;
    color: var(--ink-muted);
}

.mp-trade-picker {
    margin-top: 12px;
    padding: 10px;
    border: 2px solid #000;
    border-radius: 10px;
    background: color-mix(in srgb, var(--surface) 92%, #000);
}

.mp-trade-picker-head {
    margin-bottom: 8px;
    font-size: 12px;
    font-weight: 900;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.mp-trade-picker-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    max-height: 210px;
    overflow-y: auto;
}

.mp-trade-note {
    margin: 12px 2px 0;
    font-size: 12px;
    line-height: 1.45;
    color: var(--ink-muted);
}

.mp-trade-btns {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 12px;
}

.mp-trade-invite {
    padding: 6px 2px 2px;
}

.mp-trade-invite-text {
    margin: 0;
    font-size: 15px;
    color: var(--ink-navy);
}

@media (max-width: 620px) {
    .mp-trade-sides { grid-template-columns: 1fr; }
}

.mp-mod-menu {
    position: fixed;
    z-index: 900;
    min-width: 150px;
    padding: 6px;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
    font-family: var(--game-font);
    font-size: 20.8px;
}

.mp-mod-head {
    padding: 2px 8px 6px;
    border-bottom: 1px solid #d8dced;
    margin-bottom: 4px;
    font-weight: 900;
    color: var(--ink-navy);
    max-width: 200px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mp-mod-item {
    display: block;
    width: 100%;
    padding: 7px 8px;
    border: 0;
    border-radius: 6px;
    background: none;
    text-align: left;
    font: inherit;
    font-weight: bold;
    color: var(--ink-navy);
    cursor: pointer;
}

.mp-mod-item:hover { background: var(--surface-2); }
.mp-mod-item--report { color: #b3261e; }
.mp-mod-item--report:hover { background: #fdecea; }

.mp-chat-input {
    width: 100%;
    padding: 8px 10px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface-2);
    font-family: var(--chat-font);
    font-size: 16px;
    font-weight: bold;
    color: var(--ink-navy);
    text-transform: none;
}

.mp-chat-input::placeholder { text-transform: none; }

.mp-chat-input:focus { outline: none; border-color: #0032ff; background: var(--surface); }

.mp-chat-input--game { margin-top: 8px; }

.mp-c4 {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    padding-top: 6px;
    border-top: 2px solid #000;
}

.mp-c4-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.mp-c4-title {
    font-family: var(--game-font);
    font-size: 17px;
    font-weight: 900;
    color: var(--ink-strong);
}

.mp-c4-status {
    font-family: var(--chat-font);
    font-size: 13px;
    font-weight: 900;
    color: var(--ink-muted);
}

.mp-c4-actions {
    display: flex;
    gap: 4px;
    flex: 0 0 auto;
}

.mp-c4-action {
    padding: 5px 7px;
    border: 2px solid #000;
    background: var(--surface-2);
    font-family: var(--game-font);
    font-size: 12px;
    font-weight: 900;
    color: var(--ink-navy);
    cursor: pointer;
}

.mp-c4-action:hover,
.mp-c4-action:focus-visible {
    background: var(--accent-soft);
}

.mp-c4-seats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;
}

.mp-c4-seat {
    min-width: 0;
    padding: 4px 6px;
    border: 2px solid #000;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-family: var(--chat-font);
    font-size: 12px;
    font-weight: 900;
    color: #111;
}

.mp-c4-seat.is-red { background: #ff635d; }
.mp-c4-seat.is-yellow { background: #ffd84d; }

.mp-c4-drops,
.mp-c4-board {
    --c4-gap: 4px;
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: var(--c4-gap);
    width: min(100%, 330px);
    margin: 0 auto;
}

.mp-c4-drop {
    aspect-ratio: 1.35 / 1;
    border: 2px solid #000;
    background: #e8f0ff;
    cursor: pointer;
}

.mp-c4-drop span {
    display: block;
    width: 42%;
    aspect-ratio: 1;
    margin: 0 auto;
    background: #1756d1;
    clip-path: polygon(50% 88%, 8% 28%, 92% 28%);
}

.mp-c4-drop:hover,
.mp-c4-drop:focus-visible {
    background: var(--accent-soft);
}

.mp-c4-drop:disabled {
    cursor: default;
    opacity: 0.35;
}

.mp-c4-board {
    position: relative;
    gap: 0;
    padding: 0;
    background: #1756d1;
    overflow: hidden;
}

.mp-c4-cell {
    position: relative;
    margin: 3px;
    aspect-ratio: 1;
    border-radius: 50% !important;
    background: #071126;
    box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.66);
    overflow: visible;
}

@supports ((-webkit-mask-image: radial-gradient(circle, #000 50%, transparent 51%)) or (mask-image: radial-gradient(circle, #000 50%, transparent 51%))) {
    .mp-c4-board {
        background: transparent;
    }

    .mp-c4-board::after {
        content: "";
        position: absolute;
        inset: 0;
        z-index: 20;
        pointer-events: none;
        background: #1756d1;
        -webkit-mask-image: radial-gradient(
            circle at 50% 50%,
            transparent 0 41%,
            #000 43%
        );
        -webkit-mask-position: 0 0;
        -webkit-mask-repeat: repeat;
        -webkit-mask-size: 14.285714% 16.666667%;
        mask-image: radial-gradient(
            circle at 50% 50%,
            transparent 0 41%,
            #000 43%
        );
        mask-position: 0 0;
        mask-repeat: repeat;
        mask-size: 14.285714% 16.666667%;
    }
}

/* Rows stack front-to-back, top row highest, so a falling disc passes
   *behind* every hole above the one it lands in instead of sliding over
   them, then only comes into view once it clears the last obstruction. */
.mp-c4-cell:nth-child(-n+7) { z-index: 6; }
.mp-c4-cell:nth-child(n+8):nth-child(-n+14) { z-index: 5; }
.mp-c4-cell:nth-child(n+15):nth-child(-n+21) { z-index: 4; }
.mp-c4-cell:nth-child(n+22):nth-child(-n+28) { z-index: 3; }
.mp-c4-cell:nth-child(n+29):nth-child(-n+35) { z-index: 2; }
.mp-c4-cell:nth-child(n+36):nth-child(-n+42) { z-index: 1; }

.mp-c4-cell::before {
    content: "";
    position: absolute;
    inset: 2px;
    z-index: 1;
    border-radius: 50% !important;
    opacity: 0;
    transform: translateY(0);
}

.mp-c4-cell.is-red::before {
    opacity: 1;
    background: #ff3932;
}

.mp-c4-cell.is-yellow::before {
    opacity: 1;
    background: #ffd02f;
}

.mp-c4-cell.is-dropping::before {
    animation: c4-disc-fall 220ms linear;
}

.mp-c4-cell.is-last {
    outline: none;
}

.mp-c4-cell.is-last::after {
    content: "";
    position: absolute;
    inset: 5px;
    z-index: 2;
    pointer-events: none;
    border: 3px solid #fff;
    border-radius: 50% !important;
}

@keyframes c4-disc-fall {
    from { transform: translateY(var(--c4-fall, -240px)); }
    to { transform: translateY(0); }
}

/* Settings tabs */

/* Groups the room title with the tab row above the settings panel, so the
   two move and space together as one header block instead of being two
   independent items in .mp-lobby-right's own flex stack. */
.mp-set-head {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.mp-set-tabs {
    display: flex;
    gap: 6px;
}

.mp-set-tab {
    appearance: none;
    flex: 1;
    padding: 9px 6px;
    border: 3px solid #000;
    border-radius: 8px 8px 0 0;
    border-bottom: none;
    background: color-mix(in srgb, #dfe3f2 var(--mp-panel-alpha, 100%), transparent);
    color: #4a5268;
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: 900;
    letter-spacing: 1px;
    cursor: pointer;
}

.mp-set-tab.is-active {
    background: color-mix(in srgb, var(--surface) var(--mp-panel-alpha, 100%), transparent);
    color: var(--ink-link);
    box-shadow: inset 0 3px 0 #0032ff;
}

.mp-set-panel {
    display: flex;
    flex-direction: column;
    padding: 6px 12px;
    max-height: 380px;
    overflow-y: auto;
}

.mp-set-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 9px 0;
    border-bottom: 1px solid #e2e6f2;
}

.mp-set-row:last-child { border-bottom: none; }

.mp-set-row--stack {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
}

.mp-set-info { min-width: 0; }

.mp-set-label {
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: 900;
    color: var(--ink-navy);
}

.mp-set-desc {
    font-family: var(--game-font);
    font-size: 19.2px;
    color: var(--ink-faint);
    margin-top: 1px;
}

/* ── The article picker ──
   What the custom start/target boxes offer while you type. Parented to <body>
   and positioned over the page, like .mp-mod-menu, because the settings panel
   scrolls and would clip a dropdown hanging out of it. */
.wiki-picker {
    position: fixed;
    z-index: 1400;
    max-height: 300px;
    overflow-y: auto;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    box-shadow: 0 6px 0 rgba(0, 0, 0, 0.18);
}

.wiki-pick {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 7px 10px;
    border: 0;
    border-bottom: 1px solid var(--line, #dde1f0);
    background: none;
    font-family: var(--game-font);
    text-align: left;
    cursor: pointer;
}

.wiki-pick:last-child { border-bottom: 0; }
.wiki-pick:hover,
.wiki-pick.is-active { background: var(--surface-2); }

/* Wikimedia hands back 60px thumbnails, so this is their natural size. The
   placeholder keeps the same box for an article that has no picture — a list
   where some rows are indented and some are not reads as broken. */
.wiki-pick-thumb {
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    border-radius: 5px;
    object-fit: cover;
    background: var(--surface-2);
}

.wiki-pick-thumb--none {
    display: block;
    border: 1px dashed #c3c9dd;
}

.wiki-pick-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.wiki-pick-title {
    font-size: 13px;
    font-weight: bold;
    color: var(--ink-navy);
    overflow-wrap: anywhere;
}

.wiki-pick-desc {
    font-size: 11px;
    color: var(--ink-faint);
    overflow-wrap: anywhere;
}

/* ── The match row: start, rule, target ──
   Three selects sharing one row, in the order the race happens. They were one
   control until the two questions in it were separated — where a race runs, and
   what the rule is — so showing them side by side is not decoration: it is the
   thing that makes it obvious they are independent. */
.mp-match {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    margin: 4px 0 12px;
}

.mp-match-slot {
    flex: 1 1 0;
    min-width: 0;
}

/* The middle one is the interesting choice and the widest word, so it gets the
   room. */
.mp-match-slot:nth-child(2) {
    flex: 1.2 1 0;
}

.mp-match-cap {
    font-family: var(--game-font);
    font-size: 16px;
    font-weight: 900;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: var(--ink-muted);
    margin-bottom: 5px;
}

/* The shared dropdown is built for a label-on-the-left settings row, so it
   caps at 180px. In this row it is the whole column: it fills its slot, and
   the cap comes off or the three controls sit in a third of the panel with the
   rest of it empty. */
.mp-match .mp-match-select {
    width: 100%;
    max-width: none;
    flex-shrink: 1;
}

.mp-match--rule-only {
    max-width: 260px;
}

.mp-rally-route {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    margin: 4px 0 10px;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 4px;
}

.mp-rally-route .mp-match-slot {
    flex: 0 0 180px;
}

.mp-rally-route .mp-match-slot--checkpoint {
    flex-basis: 150px;
}

.mp-match-arrow {
    flex: 0 0 auto;
    padding-bottom: 13px;
    color: var(--ink-strong);
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: 900;
    line-height: 1;
}

@media (max-width: 560px) {
    .mp-match { flex-wrap: wrap; }
    .mp-match-slot { flex: 1 1 100%; }
    .mp-rally-route { gap: 7px; }
    .mp-rally-route .mp-match-slot { flex-basis: 170px; }
    .mp-rally-route .mp-match-slot--checkpoint { flex-basis: 145px; }
}

/* ── Tournament bracket (MATCH tab) — its own class family, since
   .mp-match/.mp-match-slot above already name the start/rule/target row. ── */

.mp-bracket-controls {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 8px 0;
    flex-wrap: wrap;
}

.mp-bracket {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    margin: 4px 0 12px;
}

.mp-bracket-slot {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 7px 10px;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    font-family: var(--game-font);
    font-weight: bold;
    min-width: 74px;
    /* Dragging owns the pointer during a swap — the page must not also try
       to scroll or refresh underneath it. */
    touch-action: none;
}

.mp-bracket--seed .mp-bracket-slot {
    cursor: grab;
}

.mp-bracket-slot--empty {
    border-style: dashed;
    color: var(--ink-faint);
    cursor: default;
}

.mp-bracket-empty {
    font-size: 11px;
    color: var(--ink-faint);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.mp-bracket-name {
    font-size: 12px;
    letter-spacing: 0.5px;
}

.mp-bracket-slot.is-drag-source {
    opacity: 0.4;
}

.mp-bracket-slot.is-drop-target {
    border-color: #0032ff;
    box-shadow: 0 0 0 2px rgba(0, 50, 255, 0.25);
}

/* The floating copy that follows the pointer while dragging. */
.mp-bracket-ghost {
    position: fixed;
    top: 0;
    left: 0;
    transform: translate(-50%, -50%);
    pointer-events: none;
    z-index: 999;
    opacity: 0.9;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.35);
}

/* The locked/running/finished bracket: one column per round, matches
   centered against the pair that feeds them via space-around, no SVG. */
.mp-bracket--tree {
    align-items: stretch;
}

.mp-bracket-round {
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    gap: 10px;
    min-width: 150px;
}

.mp-bracket-match {
    display: flex;
    flex-direction: column;
    gap: 3px;
    border: 2px solid #000;
    border-radius: 8px;
    padding: 6px;
    background: var(--surface-3);
}

.mp-bracket-match .mp-bracket-slot {
    min-width: 0;
}

.mp-bracket-vs {
    align-self: center;
    font-size: 9px;
    font-weight: 900;
    color: var(--ink-faint);
    letter-spacing: 1px;
}

.mp-bracket-live {
    box-shadow: 0 0 0 2px #0032ff;
}

.mp-bracket-slot--winner {
    background: linear-gradient(180deg, #43d95e, #1faf3c);
    color: #fff;
}

.mp-bracket-champion {
    margin: 10px 0 6px;
    display: flex;
    align-items: center;
    gap: 6px;
    font-family: var(--game-font);
    font-weight: 900;
    font-size: 22.4px;
}

@media (max-width: 560px) {
    .mp-bracket--tree { flex-direction: column; }
    .mp-bracket-round { min-width: 0; }
}

.mp-set-hint {
    margin: 8px 0;
    font-family: var(--game-font);
    font-size: 19.2px;
    color: var(--ink-faint);
    font-style: italic;
}

.mp-set-toggle {
    appearance: none;
    flex-shrink: 0;
    min-width: 58px;
    padding: 6px 0;
    border: 2px solid #000;
    border-radius: 999px;
    background: var(--surface-3);
    color: var(--ink-muted);
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: 900;
    letter-spacing: 1px;
    cursor: pointer;
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.2);
}

.mp-set-toggle.is-on {
    background: linear-gradient(180deg, #43d95e, #1faf3c);
    color: #fff;
}

.mp-set-toggle:disabled,
.mp-set-select:disabled,
.mp-set-text:disabled,
.mp-set-slider input:disabled,
.mp-item-chip:disabled,
.mp-play-btn:disabled {
    opacity: 0.6;
    cursor: default;
    box-shadow: none;
}

.mp-set-select,
.mp-set-text {
    flex-shrink: 0;
    padding: 6px 8px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface-2);
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    color: var(--ink-navy);
    max-width: 180px;
}

.mp-select {
    position: relative;
    flex-shrink: 0;
    max-width: 180px;
    font-family: var(--game-font);
}

.mp-select-btn {
    width: 100%;
    min-height: 42px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 6px 8px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface-2);
    color: var(--ink-navy);
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    text-align: left;
    cursor: pointer;
}

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

.mp-select-caret {
    flex: 0 0 auto;
    color: #0032ff;
    font-size: 16px;
    line-height: 1;
}

.mp-select-btn:focus {
    outline: none;
    border-color: #0032ff;
    background: var(--surface);
}

.mp-select.is-disabled {
    opacity: 0.6;
}

.mp-select-btn:disabled {
    cursor: default;
}

.mp-select-menu {
    display: none;
    position: absolute;
    z-index: 100020;
    pointer-events: auto;
    top: calc(100% + 4px);
    left: 0;
    width: max(100%, 190px);
    max-height: 260px;
    overflow-y: auto;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface);
    box-shadow: 0 8px 0 rgba(0, 0, 0, 0.24);
}

.mp-select.is-open .mp-select-menu {
    display: block;
}

.mp-select-option {
    width: 100%;
    display: block;
    padding: 8px 10px;
    border: 0;
    border-bottom: 1px solid #d7dded;
    background: transparent;
    color: var(--ink-navy);
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: 900;
    text-align: left;
    cursor: pointer;
}

.mp-select-option:last-child {
    border-bottom: 0;
}

.mp-select-option:hover,
.mp-select-option:focus,
.mp-select-option.is-selected {
    outline: none;
    background: #0032ff;
    color: #fff;
}

.mp-set-text { text-align: right; }
.mp-set-text:focus, .mp-set-select:focus { outline: none; border-color: #0032ff; background: var(--surface); }

.mp-set-slider {
    display: flex;
    align-items: center;
    gap: 8px;
    font-family: var(--game-font);
    font-size: 16px;
    font-weight: 900;
    color: var(--ink-faint);
    letter-spacing: 1px;
}

.mp-set-slider input {
    flex: 1;
    accent-color: #0032ff;
}

.mp-item-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
    gap: 6px;
}

.mp-item-chip {
    appearance: none;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 8px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: bold;
    color: var(--ink-navy);
    cursor: pointer;
    text-align: left;
}

.mp-item-chip-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mp-item-chip.is-banned {
    background: #f3f3f3;
    color: #aaa;
    border-color: #999;
    text-decoration: line-through;
}

.mp-ban-input-row {
    display: flex;
    gap: 6px;
    width: 100%;
}

.mp-ban-input-row .mp-set-text {
    flex: 1;
    max-width: none;
    text-align: left;
}

.mp-ban-add {
    appearance: none;
    flex-shrink: 0;
    padding: 6px 12px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface-2);
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: bold;
    color: var(--ink-navy);
    cursor: pointer;
}

.mp-ban-chip-list {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    width: 100%;
}

.mp-ban-chip {
    appearance: none;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 8px;
    border: 2px solid var(--danger);
    border-radius: 6px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 16px;
    font-weight: bold;
    color: var(--danger);
    cursor: pointer;
}

.mp-ban-chip-title {
    text-decoration: line-through;
    max-width: 200px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mp-ban-chip-x { display: flex; }
.mp-ban-chip-x svg { width: 12px; height: 12px; }

.mp-set-readonly-note { font-size: 12px; margin: 0; }

/* In-game backtrack button */

.mp-goback-btn {
    appearance: none;
    width: 100%;
    padding: 10px;
    border: 2px solid #000;
    border-radius: 8px;
    background: linear-gradient(180deg, #ffd66b, #f7b733);
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: 900;
    color: #5b3a00;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.25);
}

.mp-goback-btn:disabled {
    background: var(--surface-3);
    color: #999;
    cursor: default;
    box-shadow: none;
}

/* Arena "Leave room" button. */
.mp-quit-btn {
    appearance: none;
    width: 100%;
    padding: 8px;
    border: 2px solid #000;
    border-radius: 8px;
    background: linear-gradient(180deg, #fff, #f5dede);
    font-family: var(--game-font);
    font-size: 18.4px;
    font-weight: 700;
    color: #7a1f1f;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.2);
}

.mp-quit-btn:hover { background: linear-gradient(180deg, #fff, #ffd4d4); }
.mp-quit-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); }

/* Shown while Escape is held in a live race; the bar fills as the hold
   nears the quit threshold. */
.mp-quit-warning {
    position: fixed;
    top: 18px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10000;
    padding: 10px 22px 14px;
    border: 2px solid #000;
    border-radius: 8px;
    background: linear-gradient(180deg, #ff6b6b, #d32f2f);
    color: #fff;
    font-family: var(--game-font);
    font-size: 18.4px;
    font-weight: 700;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
    overflow: hidden;
    pointer-events: none;
}

.mp-quit-warning-bar {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 5px;
    width: 100%;
    background: #fff;
    transform-origin: left;
    animation: mp-quit-fill linear forwards;
}

@keyframes mp-quit-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* Public room browser */

.mp-roomlist-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: min(100%, 460px);
    margin: 18px auto 8px;
}

.mp-roomlist-title {
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: 900;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-mid);
}

.mp-roomlist {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: min(100%, 460px);
    margin: 0 auto;
    max-height: 260px;
    overflow-y: auto;
}

.mp-room-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    font-family: var(--game-font);
    text-align: left;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

/* Same yellow lift as the menu rows above, so a list entry reads as part of
   the same family as the mode-select rows it followed to get here. */
.mp-room-row:hover,
.mp-room-row:focus-within {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.mp-room-row-info {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.mp-room-row-name {
    font-size: 14px;
    font-weight: 900;
    color: var(--ink-navy);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mp-room-row-side {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 5px;
    flex-shrink: 0;
    min-width: 0;
}

.mp-room-row-topline {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
}

.mp-room-row-rules {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 4px;
    max-width: 260px;
}

.mp-room-rule {
    padding: 2px 5px;
    border: 1px solid var(--ink-faint);
    border-radius: 4px;
    background: var(--surface-2);
    color: var(--ink-mid);
    font-size: 10px;
    font-weight: 900;
    line-height: 1.15;
    white-space: nowrap;
}

.mp-room-row-meta {
    font-size: 11px;
    color: var(--ink-faint);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mp-room-row-count {
    font-size: 13px;
    font-weight: 900;
    color: var(--ink-link);
    flex-shrink: 0;
}

.mp-room-row-live {
    font-size: 11px;
    font-weight: 900;
    color: #b71c1c;
    flex-shrink: 0;
}

/* ── Spectator mode ── */

.mp-ghost-cursor {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 340;
    pointer-events: none;
    will-change: transform;
}

.mp-ghost-skin {
    position: relative; /* .fx-cursor is fixed by default — pin it to the ghost */
    left: auto;
    top: auto;
    z-index: auto;
}

/* The watched racer's name, hung directly under their pointer and centred on
   it — `top` is set inline by buildGhost, which knows how far down the sprite
   actually reaches.

   No pill: the tag labels a person moving over an article, and a blue lozenge
   over the text read as a piece of the page rather than as part of the cursor.
   The pill was really buying contrast, so that comes back as a hard dark
   outline, the same one the lobby tags wear — the article underneath can be
   any theme, from white paper to somebody's photo wallpaper. The ink itself is
   set inline: that player's colour, exactly as the chat writes them. */
.mp-ghost-name {
    position: absolute;
    left: 0;
    transform: translateX(-50%);
    color: #e8eaf5;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    line-height: 1.2;
    white-space: nowrap;
    text-shadow:
        -1px -1px 0 rgba(6, 8, 16, .9), 1px -1px 0 rgba(6, 8, 16, .9),
        -1px 1px 0 rgba(6, 8, 16, .9), 1px 1px 0 rgba(6, 8, 16, .9),
        0 2px 3px rgba(6, 8, 16, .55);
}

/* Same trade as the lobby tag: a clipped gradient cannot carry a text-shadow
   (it paints through the transparent glyphs), so the outline becomes a filter
   on the rendered result. */
.mp-ghost-name.is-rainbow {
    text-shadow: none;
    
}

.mp-spectate-badge {
    padding: 4px 10px;
    border: 2px solid #000;
    border-radius: 6px;
    background: #d40000;
    color: #fff;
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: 900;
    letter-spacing: 1px;
    animation: mpLivePulse 1.6s ease-in-out infinite;
}

@keyframes mpLivePulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}

.mp-rival--watch {
    appearance: none;
    display: block;
    width: 100%;
    text-align: left;
    cursor: pointer;
}

.mp-rival--watch:hover { border-color: #8fa3ff; }

.mp-rival--watch.is-followed {
    border-color: #0032ff;
    background: #e3f2fd;
    box-shadow: 0 0 0 2px rgba(0, 50, 255, 0.25);
}

.mp-spectate .wiki-link-inline { cursor: default; }

.mp-spectate-leave { width: 100%; }

.mp-result-btns {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
}

.mp-result-btns .mp-back-btn { margin-top: 0; }

@media (max-width: 860px) {
    .mp-lobby-cols { grid-template-columns: 1fr; }
    .mp-set-panel { max-height: none; }
}

/* ── In-game layout ── */

.mp-game {
    position: relative;
    width: min(100%, 1240px);
    margin: 0 auto;
    padding-bottom: 40px;
}

.mp-game-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 260px;
    gap: 18px;
    align-items: start;
}

.mp-article-stage {
    width: 100%;
}

.mp-side {
    position: sticky;
    top: 196px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}

/* Solo's copy of this panel lives in the shell from mount() (see
   trainingAppearancePanelHtml in script.js), so WikiSkin.placeSide() has
   something to find and graft into .wiki-skin the moment the first article
   exists — but nothing has grafted it yet during the roulette, where there
   is no .wiki-skin at all. Left visible, it just sat there as a plain,
   unstyled block behind the spinning cards. Hidden at this, its only home
   before that graft happens. */
.training-screen > .mp-side {
    display: none;
}

.mp-side-block {
    border: 3px solid #000;
    border-radius: 10px;
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.22);
    padding: 10px 12px;
}

.mp-side-title {
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--ink-muted);
    margin-bottom: 8px;
}

/* Nested in the wiki skin's own grid, it fills the column Appearance used to:
   drop the floating card's black border/shadow for the same plain divider
   TOC and Appearance always used, and follow the article's own light/dark
   tokens rather than the menu's, so the rail reads correctly even when the
   article is on dark and the rest of the UI is not. */
.wiki-skin > .mp-side { color: var(--wiki-fg); }

.wiki-skin > .mp-side .mp-side-block {
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    padding: 0;
}

.wiki-skin > .mp-side .mp-side-title,
.wiki-toc .mp-side-title {
    font-family: sans-serif;
    font-size: 13px;
    font-weight: bold;
    letter-spacing: normal;
    text-transform: none;
    color: var(--wiki-fg);
    padding-bottom: 6px;
    margin-bottom: 8px;
    border-bottom: 1px solid var(--wiki-border);
}

/* The my-turn highlight still has to survive the flattening above — tandem
   is unplayable if the one cue for "it's you" goes quiet. A tinted edge
   instead of the floating card's full border + drop shadow. */
.wiki-skin > .mp-side .mp-side-block--turn:has(.mp-turn.is-mine) {
    border-bottom: 2px solid #0032ff;
    background: color-mix(in srgb, #0032ff 8%, transparent);
    padding: 0 6px 12px;
    margin: 0 -6px 2px;
}

/* Names, move counts, the item's name, feed lines: plain text, so it has to
   follow the ARTICLE's own light/dark (`--wiki-fg`/`--wiki-muted`) rather
   than the menu ink tokens they used to carry — those track the player's UI
   theme (classic/midnight), which is independent of the article's own
   colour setting and can land white-on-white or black-on-black when the two
   disagree. Status colour (the hare badge, the my-turn highlight above) is
   deliberately left alone — that's signal, not body text. */
.wiki-skin > .mp-side .mp-rival-name,
.wiki-skin > .mp-side .mp-rival-moves,
.wiki-skin > .mp-side .mp-turn-who,
.wiki-skin > .mp-side .mp-mode-line,
.wiki-skin > .mp-side .mp-item-name,
.wiki-skin > .mp-side .mp-feed-entry,
.wiki-skin > .mp-side .mp-app-radio {
    color: var(--wiki-fg);
}

.wiki-skin > .mp-side .mp-rival-article,
.wiki-skin > .mp-side .mp-turn-next,
.wiki-skin > .mp-side .mp-mode-note {
    color: var(--wiki-muted);
}

/* Matches the size/weight/colour of the block titles above it (Appearance,
   Item, Racers, Feed) rather than the smaller, greyer standalone-race style
   below — a sub-heading three lines under "Appearance" shouldn't read as a
   different, quieter tier of text than "Feed" one block down. */
.wiki-skin > .mp-side .mp-app-lab {
    font-size: 13px;
    color: var(--wiki-fg);
}

/* Same reasoning for the options themselves (Small/Standard/Large etc.) — the
   standalone-race size (20.8px) reads fine against the game's own chunky
   sidebar, but sitting under a 13px label and beside the article's own 14.4px
   body text, it towers over both. Drop it to the article's own text size. */
.wiki-skin > .mp-side .mp-app-radio {
    font-size: 14.4px;
}

/* ── Tandem: whose turn it is ──
   The mode puts the whole room on one page and lets one racer click, which
   means the screen has to answer "is this mine?" from across the desk and
   without being read. Two answers, both loud: the panel, and the article
   itself going quiet when the page is somebody else's. */
.mp-turn {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.mp-turn-who {
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: 900;
    letter-spacing: 0.5px;
    color: var(--ink-mid);
}

.mp-turn.is-mine .mp-turn-who {
    color: #0032ff;
    text-transform: uppercase;
}

/* The block itself lights up, so the answer is readable in the corner of the
   eye of someone whose attention is on the article. */
.mp-side-block--turn {
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.mp-side-block--turn:has(.mp-turn.is-mine) {
    border-color: #0032ff;
    box-shadow: 0 5px 0 rgba(0, 50, 255, 0.3);
    background: color-mix(in srgb, #0032ff 8%, var(--surface));
}

/* A bar, not a number. What a racer needs off the clock is how much of their
   turn is left, which a shrinking bar answers without being read — and a
   number ticking down in the corner of the eye is a second pressure the mode
   does not need on top of the one it already has. */
.mp-turn-clock {
    height: 8px;
    border: 2px solid #000;
    border-radius: 5px;
    background: var(--surface-3);
    overflow: hidden;
}

.mp-turn-bar {
    display: block;
    height: 100%;
    width: 100%;
    background: #0032ff;
    /* Matches the 250ms tick in script.js, so the bar slides instead of
       stepping four times a second. */
    transition: width 0.25s linear, background-color 0.2s ease;
}

.mp-turn-bar.is-low { background: #d32f2f; }

.mp-turn-next {
    font-family: var(--game-font);
    font-size: 17.6px;
    font-weight: bold;
    color: var(--ink-faint);
}

/* Somebody else's turn: the links stop pretending a click will do anything.
   Kept as a dimming rather than pointer-events:none, because the racer is
   still reading the page and planning their own turn on it — scrolling,
   selecting and hovering all still work, and a dead link that eats the cursor
   reads as the page having crashed. */
.article-stage.is-not-my-turn .wiki-link-inline {
    opacity: 0.55;
    cursor: not-allowed;
}

.mp-rivals {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.mp-rival {
    padding: 7px 10px;
    border: 2px solid #c8ccd1;
    border-radius: 0;
    background: none;
    font-family: var(--game-font);
}

.mp-rival.is-me {
    border-color: #0032ff;
}

.mp-rival-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
}

.mp-rival-name {
    font-weight: 900;
    font-size: 14px;
    color: var(--ink-navy);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A rival's name doubles as the mute/report handle (see renderRivals) — it
   must still read as a label, so the affordance only shows on hover. */
.mp-rival-name--action {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-weight: 900;
    font-size: 14px;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.mp-rival-name--action:hover,
.mp-rival-name--action:focus-visible {
    text-decoration: underline;
    color: var(--ink-link);
}

.mp-rival-moves {
    flex: 0 0 auto;
    font-weight: bold;
    font-size: 12px;
    color: var(--ink-link);
}

/* ── Golf and Bloodhound: the one line of state that has to be on screen ──
   Both modes break a rule the racer already knows — reaching the target does
   not end golf, and a hound cannot move for the first twenty seconds — so the
   correction lives in the sidebar rather than in a toast that has faded by the
   time it matters. */
.mp-mode-line {
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: 900;
    letter-spacing: 0.5px;
    color: var(--ink-mid);
}

.mp-mode-line.is-good {
    color: #0032ff;
    text-transform: uppercase;
}

.mp-mode-note {
    font-family: var(--game-font);
    font-size: 17.6px;
    font-weight: bold;
    color: var(--ink-faint);
    margin-top: 2px;
}

/* A number, not a bar — unlike the tandem turn clock. These two count seconds
   the racer is waiting OUT rather than seconds they are spending, and the
   question is "how long until I can move", which wants an answer you can read. */
.mp-mode-clock {
    font-family: Consolas, monospace;
    font-size: 12px;
    font-weight: bold;
    color: var(--ink-link);
    margin-top: 6px;
}

.mp-mode-clock.is-low { color: #d32f2f; }

/* ── Appearance: the reading prefs (Settings > Customization) surfaced back
   into the race rail so they're reachable without leaving the article — see
   the mpWikiText/mpWikiWidth/mpWikiColor change handler in onGameStart.
   These sit right against the article's own text (and inside .wiki-skin, a
   reader's own Text/Width/Color choice), so they follow .wiki-article's
   sans-serif rather than --game-font — the game's chunky pixel face read as
   a mismatched sticker next to real Wikipedia text. */
.mp-app-lab {
    font-family: sans-serif;
    font-size: 17.6px;
    font-weight: bold;
    color: var(--ink-faint);
    margin: 10px 0 4px;
}

.mp-app-lab:first-child { margin-top: 0; }

.mp-app-radio {
    display: flex;
    align-items: center;
    gap: 6px;
    font-family: sans-serif;
    font-size: 20.8px;
    color: var(--ink-navy);
    padding: 2px 0;
    cursor: pointer;
}

.mp-app-radio input { margin: 0; accent-color: #0032ff; }

/* One theme's own touch — see POLKA_DOTS_THEME_ID in script.js. Native
   radios can't be reshaped with accent-color, so this rebuilds the same
   ring-and-dot a real radio button draws (outline, gap, filled center) out
   of three nested clip-path hearts at 100%/72%/42% scale (same center, see
   the scaling script in the commit) instead of one flat fill: unchecked is
   the outer heart in the separator color with the white middle layer
   showing through as the gap, checked adds the theme's own #ff33e4 pink as
   both the outline and the small centered dot. */
body[data-equipped-theme-id="theme-custom-mthu2arq-hx84"] .mp-app-radio input {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 15px;
    height: 14px;
    background: var(--wiki-border);
    clip-path: path("M7.5 13 C3 9.5 1 7 1 4.5 C1 2.1 3 0.6 5 1.3 C6.2 1.7 7.5 2.9 7.5 2.9 C7.5 2.9 8.8 1.7 10 1.3 C12 0.6 14 2.1 14 4.5 C14 7 12 9.5 7.5 13 Z");
    cursor: pointer;
}

body[data-equipped-theme-id="theme-custom-mthu2arq-hx84"] .mp-app-radio input::before {
    content: "";
    position: absolute;
    inset: 0;
    background: #fff;
    clip-path: path("M7.5 11.29 C4.26 8.77 2.82 6.97 2.82 5.17 C2.82 3.44 4.26 2.36 5.7 2.87 C6.56 3.16 7.5 4.02 7.5 4.02 C7.5 4.02 8.44 3.16 9.3 2.87 C10.74 2.36 12.18 3.44 12.18 5.17 C12.18 6.97 10.74 8.77 7.5 11.29 Z");
}

body[data-equipped-theme-id="theme-custom-mthu2arq-hx84"] .mp-app-radio input::after {
    content: "";
    position: absolute;
    inset: 0;
    background: #ff33e4;
    clip-path: path("M7.5 9.46 C5.61 7.99 4.77 6.94 4.77 5.89 C4.77 4.88 5.61 4.25 6.45 4.55 C6.95 4.72 7.5 5.22 7.5 5.22 C7.5 5.22 8.05 4.72 8.55 4.55 C9.39 4.25 10.23 4.88 10.23 5.89 C10.23 6.94 9.39 7.99 7.5 9.46 Z");
    opacity: 0;
}

body[data-equipped-theme-id="theme-custom-mthu2arq-hx84"] .mp-app-radio input:checked {
    background: #ff33e4;
}

body[data-equipped-theme-id="theme-custom-mthu2arq-hx84"] .mp-app-radio input:checked::after {
    opacity: 1;
}

/* Role tags on a racer's name, in the panel and on the results card: the hare,
   a stamped waypoint, a card already handed in. */
.mp-rival-badge {
    flex: 0 0 auto;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    font-family: var(--game-font);
    font-size: 15.2px;
    font-weight: 900;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    background: rgba(0, 50, 255, 0.1);
    color: var(--ink-link);
}

.mp-rival-badge.is-hare {
    background: rgba(211, 47, 47, 0.12);
    color: #d32f2f;
}

.mp-rival-badge.is-in {
    background: rgba(46, 125, 50, 0.14);
    color: #2e7d32;
}

.mp-rival-article {
    font-size: 12px;
    color: var(--ink-muted);
    overflow-wrap: anywhere;
}

.mp-item-slot {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 10px 12px;
    border: 3px solid #f6d83f;
    border-radius: 0;
    background: none;
    cursor: pointer;
    font-family: var(--game-font);
    text-align: left;
    animation: mpItemPulse 1.1s ease-in-out infinite;
}

.mp-item-slot.is-empty {
    border-color: #c8ccd1;
    cursor: default;
    animation: none;
    opacity: 0.75;
}

.mp-item-targeting {
    display: grid;
    margin-top: 8px;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 6px;
}
.mp-targeting-btn {
    display: flex;
    align-items: center;
    gap: 5px;
    min-height: 38px;
    padding: 6px;
    border: 2px solid var(--ink-muted);
    background: transparent;
    color: var(--ink-navy);
    font: 800 11px var(--game-font);
    cursor: pointer;
}
.mp-targeting-btn kbd { font: inherit; opacity: 0.65; }
.mp-targeting-btn[aria-pressed="true"] {
    border-color: #b58b00;
    background: #f6d83f;
    color: #202122;
}
.mp-targeting-btn:hover { border-color: var(--ink-navy); }
.mp-targeting-btn:focus-visible { outline: 2px solid var(--ink-navy); outline-offset: 2px; }
.mp-targeting-help {
    margin: 8px 0 0;
    color: var(--ink-muted);
    font-size: 11px;
    line-height: 1.4;
}
.wiki-skin .mp-targeting-btn:not([aria-pressed="true"]) { color: var(--wiki-fg); }
.wiki-skin .mp-targeting-btn { border-color: var(--wiki-border); }
.wiki-skin .mp-targeting-btn[aria-pressed="true"] { border-color: #b58b00; }
.wiki-skin .mp-targeting-btn:focus-visible { outline-color: var(--wiki-fg); }
.wiki-skin .mp-targeting-help { color: var(--wiki-muted); }

@media (max-width: 768px) {
    body.wiki-reading .mp-side-block--item:has(.mp-item-targeting) {
        padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
        background: var(--wiki-bg, var(--surface));
    }
    .mp-side-block--item:has(.mp-item-targeting) > .mp-side-title { display: none; }
    .mp-item-targeting { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 8px; }
    .mp-targeting-btn { justify-content: center; padding: 5px 2px; font-size: 10px; }
    .mp-targeting-help { min-height: 2.8em; }
    body.wiki-reading .wiki-skin:has(.mp-item-targeting) { padding-bottom: 180px; }
}

@keyframes mpItemPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(246, 216, 63, 0.8); }
    50%      { box-shadow: 0 0 0 7px rgba(246, 216, 63, 0); }
}

.mp-item-icon { font-size: 26px; }

.mp-item-name {
    font-weight: 900;
    font-size: 13px;
    color: var(--ink-navy);
}

.god-menu {
    position: fixed;
    right: 14px;
    bottom: 92px; /* clear of the Discord button */
    z-index: 9500;
    width: 210px;
    padding: 10px;
    background: #ff8a1f;
    color: #1a0f00;
    border: 3px solid #1a0f00;
    box-shadow: 0 0 0 2px #ff8a1f, 0 8px 24px rgba(0, 0, 0, 0.5);
    font: 700 12px ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
    letter-spacing: 0.4px;
}

.god-menu.is-hidden { display: none; }

.god-head {
    cursor: move;
    user-select: none;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 8px;
    font-size: 13px;
}

.god-head span { font-size: 10px; opacity: 0.7; }

.god-win {
    width: 100%;
    padding: 12px 0;
    margin-bottom: 8px;
    border: 3px solid #1a0f00;
    background: #1a0f00;
    color: #ff8a1f;
    font: inherit;
    font-size: 16px;
    font-weight: 900;
    cursor: pointer;
}

.god-win:active { background: #4a2a00; }

.god-reroll { padding: 8px 0; font-size: 13px; }

.god-toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 5px 0;
    cursor: pointer;
}

.god-items {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 5px;
    margin-top: 8px;
}

.god-item {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 6px 0;
    border: 2px solid #1a0f00;
    background: none;
    color: #1a0f00;
    cursor: pointer;
}

.god-item:active { background: rgba(26, 15, 0, 0.2); }

.god-note { margin-top: 8px; font-size: 10px; opacity: 0.7; }

.mp-feed {
    display: flex;
    flex-direction: column;
    gap: 5px;
    min-height: 30px;
}

.mp-feed-entry {
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: bold;
    color: var(--ink-mid);
    padding: 4px 8px;
    border: 1px solid #c8ccd1;
    border-radius: 0;
    background: none;
}

/* Live race chat, floating: its home in .mp-game-body while the article is
   loading, and where it stays on phones and while spectating, when Contents
   (its docked spot, above) is not on screen. Stays attached to the viewport
   while the article scrolls. */
.mp-toc-chat {
    position: fixed;
    left: 16px;
    bottom: 16px;
    z-index: 9050;
    display: flex;
    flex-direction: column;
    gap: 6px;
    width: min(310px, calc(100vw - 32px));
    max-height: min(240px, calc(100dvh - 32px));
    padding: 9px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.72);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
    color: #fff;
}

.mp-toc-chat .mp-side-title {
    margin-bottom: 0;
    font-size: 13px;
    letter-spacing: 1.5px;
    color: rgba(255, 255, 255, 0.78);
}

.mp-toc-chat .mp-chat-log {
    height: 118px;
    min-height: 34px;
    color: #fff;
    overflow-y: auto;
    scrollbar-color: rgba(255, 255, 255, 0.45) transparent;
}

.mp-toc-chat .mp-chat-line.is-me .mp-chat-who,
.mp-toc-chat .mp-chat-who--action:hover,
.mp-toc-chat .mp-chat-who--action:focus-visible {
    color: #80a8ff;
}

.mp-toc-chat .mp-chat-line--system { color: rgba(255, 255, 255, 0.68); }

.mp-toc-chat .mp-chat-input {
    flex: 0 0 auto;
    border: 1px solid rgba(255, 255, 255, 0.42);
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
}

.mp-toc-chat .mp-chat-input::placeholder { color: rgba(255, 255, 255, 0.65); }

.mp-toc-chat .mp-chat-input:focus {
    border-color: #80a8ff;
    background: rgba(255, 255, 255, 0.18);
}

@media (max-width: 600px) {
    .mp-toc-chat {
        left: 8px;
        bottom: 8px;
        width: min(290px, calc(100vw - 16px));
    }
}

/* After the finish the chat outlives the results screen: onGameOver moves it
   out of the article's Contents column and in behind the overlay's DOM
   position, so this z-index puts it above the dim instead of under it. On a
   wide screen it becomes a full-height rail down the left edge with the same
   16px gap the floating chat keeps, and the results card is centred in what
   is left over. Narrow screens just keep the floating box, undimmed. */
.mp-toc-chat.is-after-race {
    z-index: 9200;
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
}

@media (min-width: 901px) {
    .mp-toc-chat.is-after-race {
        top: 16px;
        width: 340px;
        max-height: none;
        padding: 12px;
    }

    .mp-toc-chat.is-after-race .mp-chat-log {
        flex: 1 1 0;
        height: auto;
        min-height: 60px;
    }

    .mp-result-overlay.has-chat { padding-left: calc(340px + 48px); }
}

/* The Racers panel's mirror of the chat rail above (see liftRacersOverResults):
   same move off its spot in the race sidebar, same floating card, the right
   edge instead of the left, so the two frame the results screen evenly. It
   carries its own dark card look here rather than the plain white one it wore
   inside the article rail, since that one leaned on the article's own
   background to read at all. */
.mp-side-block--racers.is-after-race {
    position: fixed;
    right: 16px;
    bottom: 16px;
    z-index: 9200;
    display: flex;
    flex-direction: column;
    gap: 6px;
    width: min(310px, calc(100vw - 32px));
    max-height: min(240px, calc(100dvh - 32px));
    padding: 9px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.72);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
    color: #fff;
}

.mp-side-block--racers.is-after-race .mp-side-title {
    font-family: var(--game-font);
    font-size: 13px;
    font-weight: 900;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.78);
    margin-bottom: 0;
}

/* liftRacersOverResults always replaces this panel's content with the results
   screen's own ranked list (.mp-result-list .mp-lobby-row, styled once
   already, right where that list is defined) — the live .mp-rival rows never
   actually appear here, only the empty-state scrollbar needs setting. */
.mp-side-block--racers.is-after-race .mp-rivals {
    overflow-y: auto;
    scrollbar-color: rgba(255, 255, 255, 0.45) transparent;
}

@media (max-width: 600px) {
    .mp-side-block--racers.is-after-race {
        right: 8px;
        bottom: 8px;
        width: min(290px, calc(100vw - 16px));
    }
}

@media (min-width: 901px) {
    .mp-side-block--racers.is-after-race {
        top: 16px;
        width: 340px;
        max-height: none;
        padding: 12px;
    }

    .mp-side-block--racers.is-after-race .mp-rivals {
        flex: 1 1 0;
        min-height: 60px;
    }

    .mp-result-overlay.has-racers { padding-right: calc(340px + 48px); }
}

/* ── Countdown ── */

.mp-countdown {
    position: fixed;
    inset: 0;
    z-index: 9000;
    display: grid;
    place-items: center;
    background: rgba(10, 14, 34, 0.55);
    pointer-events: none;
}

.mp-countdown span {
    font-family: var(--game-font);
    font-size: 240px;
    font-weight: 900;
    color: #fff;
    -webkit-text-stroke: 5px #0032ff;
    text-shadow: 0 8px 0 rgba(0, 0, 0, 0.4);
    animation: mpCountPop 1s ease-in-out infinite;
}

.mp-countdown.is-go { background: transparent; }
.mp-countdown.is-go span { color: #43d95e; -webkit-text-stroke: 5px #0b5c1e; }

/* Reroll vote: the one interactive island in an otherwise click-through
   overlay (.mp-countdown itself is pointer-events:none so the article can
   still be scrolled while it's up). Hidden the moment GO! fires along with
   the rest of #mpCountdown. */
.mp-reroll {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    pointer-events: auto;
}

.mp-reroll-btn {
    appearance: none;
    padding: 10px 22px;
    border: 3px solid #000;
    border-radius: 999px;
    background: linear-gradient(180deg, #fff, #dfe3f5);
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.3);
}

.mp-reroll-btn:hover { background: linear-gradient(180deg, #fff, #eef0ff); }
.mp-reroll-btn:active { transform: translateY(2px); box-shadow: 0 2px 0 rgba(0, 0, 0, 0.3); }

.mp-reroll-btn.is-voted {
    background: linear-gradient(180deg, #ffe08a, #f7b733);
    color: #5b3a00;
}

.mp-reroll-btn:disabled {
    background: var(--surface-3);
    color: #999;
    cursor: default;
    box-shadow: none;
}

.mp-reroll-tally {
    min-height: 22px;
    color: #fff;
    font-family: var(--game-font);
    font-size: 16.8px;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.6);
}

@keyframes mpCountPop {
    0%   { transform: scale(0.6); opacity: 0; }
    30%  { transform: scale(1.15); opacity: 1; }
    60%  { transform: scale(1); }
    100% { transform: scale(1); opacity: 1; }
}

/* ── Rainbow power-up links ── */

/* A power-up link. The spectrum runs through the LETTERS rather than behind
   them: this sits inside a Wikipedia article, and a filled pill with a glowing
   box around it stops reading as a link in a sentence and starts reading as a
   button someone dropped on the page.

   The ramp is a stop darker than the obvious one all the way round. The old
   version could afford #ffe93d because white text sat on top of it; with the
   gradient clipped to the glyphs the colours ARE the text, and a light yellow
   word on an article's white background is a word nobody can read.

   Nothing here paints a box, so the padding, the radius and the pulsing
   box-shadow are gone with it — a glow with no background to sit on draws a
   rectangle in mid-air. Definition comes from a drop-shadow instead, because
   text-shadow paints behind glyphs that are transparent and comes through as a
   smear where the letters should be. */
.wiki-link-inline.wiki-link-rainbow {
    background-image: linear-gradient(90deg,
        #e03131, #e8590c, #b8a000, #2f9e44, #1c7ed6, #7048e8, #d6336c, #e03131);
    background-size: 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    font-weight: bold;
    text-decoration: none;
    /* Overrides the inline link's own coloured glow, which would smear now. */
    text-shadow: none;
    animation: mpRainbowSlide 2s linear infinite, mpRainbowHalo 2s linear infinite;
}

@keyframes mpRainbowSlide {
    to { background-position: 300% 0; }
}

/* The glow. drop-shadow and not box-shadow, because drop-shadow takes the
   shape of what is actually painted — with the gradient clipped to the glyphs
   that is the letters themselves, so the halo hugs the word instead of drawing
   a rectangle around it, which is what the old pill's box-shadow did.

   Two shadows on every frame: a tight dark one that keeps the letters defined
   against a white article, and a wide coloured one that is the glow. The
   colour walks the same spectrum as the gradient over the same two seconds, so
   the halo is always about the colour the word is currently wearing rather
   than drifting against it. Animated as one `filter` because that is one
   property — the halves cannot be given separate timings. */
@keyframes mpRainbowHalo {
    0%   { filter:  drop-shadow(0 0 6px rgba(224, 49, 49, 0.9)); }
    17%  { filter:  drop-shadow(0 0 6px rgba(232, 89, 12, 0.9)); }
    33%  { filter:  drop-shadow(0 0 6px rgba(184, 160, 0, 0.9)); }
    50%  { filter:  drop-shadow(0 0 6px rgba(47, 158, 68, 0.9)); }
    67%  { filter:  drop-shadow(0 0 6px rgba(28, 126, 214, 0.9)); }
    83%  { filter:  drop-shadow(0 0 6px rgba(112, 72, 232, 0.9)); }
    100% { filter:  drop-shadow(0 0 6px rgba(224, 49, 49, 0.9)); }
}

@media (prefers-reduced-motion: reduce) {
    /* Both animations stop, so the glow has to be stated once as a still. */
    .wiki-link-inline.wiki-link-rainbow {
        animation: none;
        filter:  drop-shadow(0 0 6px rgba(224, 49, 49, 0.9));
    }
}

/* ── Arson ── */
/* The clicked link comes apart into fire-coloured ash (public/snap-ash.js) and
   is gone, the way a Snap takes a link. `burning` keeps the word on screen
   until the ash has picked it up; `burnt` is the state every later render
   re-applies, so a burnt article stays a gap on every page it turns up on for
   the rest of the race. The gap keeps the word's width so the sentence does
   not reflow, and the button is disabled underneath, which is what actually
   makes it unclickable. */
.wiki-link-inline.wiki-link-burnt:not(.wiki-link-burning) {
    visibility: hidden;
    pointer-events: none;
}

/* ── The Snap ──
   A snapped link is gone but not collapsed: the gap keeps the sentence the
   width it was, so the page does not reflow under the reader's eyes. The ash
   itself is drawn by public/snap-ash.js on its own canvas; this class only
   removes the real text once (or if) that has happened. */
.wiki-link-inline.wiki-link-snapped {
    visibility: hidden;
    pointer-events: none;
}

/* ── Untraceable: an article a rival got to first ──
   Dead like ash, but not ash. A burnt link is a thing that happened TO you and
   is written in one charred colour; a taken link is a thing a particular person
   did, so it is written in that person's colour — the same ink their name is in
   in the chat, set inline as --taken-ink by MPFX.takeLink. Reading the page in
   this mode then tells you who has been where, not merely that somebody has.

   The fallback is the burnt colour, for a claimer with no colour of their own
   (a guest who never answered the signup question, or a bot). */
.wiki-link-inline.wiki-link-taken {
    text-decoration: line-through;
    text-decoration-color: currentColor;
    text-decoration-thickness: 2px;
    text-shadow: none;
    cursor: not-allowed;
    opacity: 0.9;
}

/* Split from the rule above so a rainbow claim keeps its clipped gradient:
   `color` and text-fill here would beat .is-rainbow on specificity and paint
   the whole thing one flat colour. */
.wiki-link-inline.wiki-link-taken:not(.is-rainbow) {
    color: var(--taken-ink, var(--wiki-burnt));
    -webkit-text-fill-color: currentColor;
    background-image: none;
}

/* A rainbow claimer takes the link in the gradient the rest of the game writes
   their name in. The strike-through has to be given a colour of its own here:
   the gradient makes currentColor transparent, so a decoration inheriting it
   would leave the link looking merely coloured rather than closed. */
.wiki-link-inline.wiki-link-taken.is-rainbow {
    text-decoration-color: #8a8f9c;
}

/* The moment it goes: a quick pulse in the claimer's own colour, so an article
   dying while you are staring at the list is something you see happen rather
   than something you find already true. Colour is left alone — the animation
   is the event, the ink is the identity. */
.wiki-link-inline.wiki-link-taken.is-claiming {
    animation: mpClaimStruck 700ms ease-out;
}

@keyframes mpClaimStruck {
    0%   { opacity: 1; transform: translateY(0) scale(1.06); filter: brightness(1.5); }
    40%  { opacity: 1; filter: brightness(1.2); }
    100% { opacity: 0.9; transform: translateY(0) scale(1); filter: none; }
}

@media (prefers-reduced-motion: reduce) {
    /* The link is simply shut, with no flourish about it. */
    .wiki-link-inline.wiki-link-taken.is-claiming { animation: none; }
}

/* ── Banned: a title the room host shut out entirely ──
   Dead everywhere it appears, same as burnt or taken, but its own colour —
   red, not ash or a claimer's ink — because nobody DID anything to close it;
   the room decided it never opens. */
.wiki-link-inline.wiki-link-banned {
    color: var(--wiki-banned);
    background-image: none;
    -webkit-text-fill-color: currentColor;
    text-decoration: line-through;
    text-decoration-color: currentColor;
    text-decoration-thickness: 2px;
    text-shadow: none;
    cursor: not-allowed;
    opacity: 0.85;
}

/* ── Airstrike: a destroyed article link ── */
.wiki-link-inline.wiki-link-bombed {
    position: relative;
    color: #c92a2a;
    background-image: none;
    -webkit-text-fill-color: currentColor;
    text-decoration: line-through;
    text-decoration-color: #f08c00;
    text-decoration-thickness: 2px;
    text-shadow: none;
    cursor: not-allowed;
    opacity: 0.9;
}

.wiki-link-explosion {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 82px;
    height: 82px;
    pointer-events: none;
    object-fit: contain;
    transform: translate(-50%, -50%) scale(0.45);
    animation: mpBombBurst 1700ms linear forwards;
    z-index: 2;
}

@keyframes mpBombBurst {
    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.25); filter: brightness(1.5); }
    18%  { opacity: 1; transform: translate(-50%, -50%) scale(0.95); }
    72%  { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.2); filter: none; }
}

@media (prefers-reduced-motion: reduce) {
    .wiki-link-explosion { display: none; }
}

/* ── Result overlay ── */

.mp-result-overlay {
    position: fixed;
    inset: 0;
    z-index: 9100;
    display: grid;
    place-items: center;
    padding: 20px;
    padding-bottom: var(--mp-podium-space, 20px);
    box-sizing: border-box;
    background: #000;
    animation: mpFadeIn 0.3s ease-out;
}

/* Reserves room at the bottom for the standalone podium (see .mp-podium)
   so the centered standings above it don't run under it. The podium itself
   cancels this same space back out to stay flush with the true screen
   edge regardless of how big it is. */
.mp-result-overlay.has-podium { --mp-podium-space: 260px; }

@keyframes mpFadeIn {
    from { opacity: 0; }
}

/* A beam from the roof, aimed down at wherever the winner's cursor currently
   is (see startWinnerSpotlight) — behind the cursor layer and the card, so
   it reads as ambient light on the black background rather than a shape of
   its own. Static center-top until a winner's position is known. */
.mp-spotlight {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: radial-gradient(ellipse 320px 120% at var(--spot-x, 50%) -30%,
        rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.12) 45%, transparent 72%);
}

/* No box any more — the podium standing on its own at the bottom of the
   screen (see .mp-podium) is the winner announcement, and this is just the
   standings and buttons floating as plain text in the space above it.
   Still capped to the viewport and flexed so the standings list is the one
   part that scrolls, keeping the buttons below it in view. */
.mp-result-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    width: min(92vw, 480px);
    max-height: calc(100vh - 40px);
    padding: 30px 34px;
    color: #fff;
    font-family: var(--game-font);
    text-shadow: 0 2px 0 rgba(0, 0, 0, 0.6);
    overflow: hidden;
}

.mp-result-card.is-win .mp-lobby-row.is-me .mp-lobby-name { color: #f6d83f; }

/* The results sit underneath the race page, which drops away to show them, so
   there is no fade of their own to wait for. */
.mp-result-overlay.is-under { animation: none; }

/* Nothing in the card takes a click while the player's own pointer is away
   being warped onto its step: the real one is hidden, so a click would land
   somewhere the player cannot see. */
.mp-result-overlay.is-warping-me .mp-result-card { pointer-events: none; }

.mp-result-card { position: relative; }

.mp-result-replay {
    position: absolute;
    top: 10px;
    right: 10px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border: 3px solid #000;
    border-radius: 0;
    background: var(--surface);
    color: var(--ink-navy);
    font-family: var(--game-font);
    font-size: 17.6px;
    font-weight: 900;
    cursor: pointer;
}

.mp-result-replay:hover { background: #f6d83f; }

/* ── Podium ──
   Standalone now, not part of .mp-result-card: centered on the whole
   screen and flush against its bottom edge. `bottom: 0` on an absolutely
   positioned element sits at the ancestor's padding EDGE (outside its
   padding, same as its border box), so the overlay's own padding doesn't
   gap this from the true screen edge — no offset needed. The headroom
   above the steps is where the cursors stand: a pointer, its name tag and,
   over the winner, the trophy. */
.mp-podium {
    position: absolute;
    left: 50%;
    bottom: 0;
    transform: translateX(-50%);
    display: flex;
    align-items: flex-end;
    justify-content: center;
    gap: 6px;
    width: min(92vw, 480px);
    padding-top: 134px;
}

.mp-podium-step {
    flex: 1 1 0;
    max-width: 132px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding-top: 6px;
    border: 4px solid #000;
    border-bottom: 0;
    font-family: var(--game-font);
    color: #000;
}

.mp-podium-num { font-size: 38px; font-weight: 900; line-height: 1; }
.mp-podium-stat { font-size: 17.6px; font-weight: bold; line-height: 1.1; }

.mp-podium-step.is-place-1 { height: 112px; background: #f6d83f; }
.mp-podium-step.is-place-2 { height: 92px; background: #cfd6e4; }
.mp-podium-step.is-place-3 { height: 72px; background: #d99a5b; }

.mp-result-elo {
    padding: 6px 18px;
    border-radius: 0;
    font-family: var(--game-font);
    font-size: 30.4px;
    font-weight: 900;
}

.mp-result-elo.is-up { background: #e8f5e9; color: #1b7d34; }
.mp-result-elo.is-down { background: #ffebee; color: #b71c1c; }

/* The standings rows: no box at all here, just name/rank and stat as plain
   text in the space above the podium. The one part of the card allowed to
   shrink/scroll — `min-height: 0` is what lets a flex child shrink below its
   content size at all. */
.mp-result-list {
    min-height: 0;
    overflow-y: auto;
    width: 100%;
}

.mp-result-list .mp-lobby-row {
    border: none;
    border-radius: 0;
    background: none;
    padding: 4px 2px;
}

.mp-result-list .mp-lobby-row.is-me { background: rgba(255, 255, 255, 0.12); }

.mp-result-list .mp-lobby-name,
.mp-result-list .mp-lobby-rating { color: #fff; }

.mp-result-card .mp-back-btn,
.mp-result-card .mp-play-btn {
    flex: 0 0 auto;
    border-radius: 0;
}

@media (max-width: 900px) {
    .mp-game-body {
        grid-template-columns: 1fr;
    }

    .mp-side {
        position: static;
        flex-direction: row;
        flex-wrap: wrap;
    }

    .mp-side-block { flex: 1 1 180px; }

    .mp-countdown span { font-size: 90px; }
}

/* ═══ Profile menu & Badges ═════════════════════════════════════════════════ */

.profile-menu {
    position: fixed;
    z-index: 9200;
    min-width: 190px;
    border: 3px solid #000;
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 8px 0 rgba(0, 0, 0, 0.3);
    overflow: hidden;
    animation: pmPop 0.16s ease-out;
    font-family: var(--game-font);
}

@keyframes pmPop {
    from { opacity: 0; transform: translateY(-6px) scale(0.95); }
}

.profile-menu-head {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 9px 13px;
    background: linear-gradient(180deg, #746FFC, #6A68FE);
    color: #fff;
    font-size: 13px;
    font-weight: 900;
}

.profile-menu-item {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 11px 14px;
    border: 0;
    background: var(--surface);
    color: var(--ink-navy);
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    text-align: left;
    cursor: pointer;
}

.profile-menu-item:hover {
    background: #e3f2fd;
    color: var(--ink-link);
}

/* The Friends item's own trailing readout: how many are online, pushed to the
   far edge of the row by margin-left: auto rather than justify-content on the
   item itself, because Badges/Inventory/Log out have no trailing content and
   should stay left-packed. Muted — this is a status line, not the reason to
   click. */
.profile-menu-item-tag {
    margin-left: auto;
    font-size: 11px;
    font-weight: 700;
    color: var(--ink-muted);
    white-space: nowrap;
}

/* Pending friend requests: the one thing on this row asking for an answer, so
   unlike the tag beside it, this gets the accent. Sits after the tag rather
   than fighting it for the same auto margin — both gate on the same "we know
   the friend list" condition, so the tag is never absent while this shows. */
.profile-menu-item-badge {
    margin-left: 6px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    box-sizing: border-box;
    border-radius: 9px;
    background: var(--accent, #d0d20d);
    color: var(--ink, #16161d);
    font-size: 11px;
    font-weight: 800;
    line-height: 18px;
    text-align: center;
}

/* ── Badges overlay ── */

.badges-overlay {
    position: fixed;
    inset: 0;
    z-index: 9300;
    display: grid;
    place-items: center;
    background: rgba(10, 14, 34, 0.6);
    animation: mpFadeIn 0.25s ease-out;
}

.badges-card {
    display: flex;
    flex-direction: column;
    width: min(94vw, 760px);
    max-height: 84vh;
    border: 5px solid #000;
    border-radius: 14px;
    background: var(--surface-2);
    box-shadow: 0 12px 0 rgba(0, 0, 0, 0.35);
    overflow: hidden;
}

.badges-head {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 14px 52px;
    background: linear-gradient(180deg, #746FFC, #6A68FE);
    color: #fff;
    border-bottom: 3px solid #000;
}

.badges-title {
    font-family: var(--game-font);
    font-size: 41.6px;
    font-weight: 900;
}

.badges-summary {
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    opacity: 0.9;
}

.badges-close {
    position: absolute;
    top: 12px;
    right: 12px;
    width: 36px;
    height: 36px;
    border: 3px solid #000;
    border-radius: 9px;
    background: var(--surface);
    color: var(--ink-navy);
    font-size: 17px;
    font-weight: 900;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
}

.badges-close:hover { background: #ffebee; color: #b71c1c; }

.badges-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    padding: 16px;
    overflow-y: auto;
}

.badges-section-label {
    grid-column: 1 / -1;
    margin-top: 6px;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

/* ── Profile modal ── */
/* Same overlay/card chrome as Badges/Inventory (.badges-overlay/.badges-card
   above): the corner mini-card (.profile-btn/.profile-card) drawn again at
   roughly 2x, centered over the dark scrim, with Friends/Badges/Inventory as
   entry points and a link out to the public profile — everything the old
   anchored dropdown held except Log out and Register/Recovery email, which
   stay Settings-only (the Account tab already carried both). */
.profile-modal-card { width: min(94vw, 460px); max-height: 90vh; }

.profile-modal-head { flex-direction: row; align-items: center; justify-content: flex-start; padding: 22px 26px; }

.profile-modal-id { display: flex; align-items: center; gap: 18px; width: 100%; }

/* 128px vs. the corner card's ~67px square — a shade over 2x. */
.profile-modal-avatar { width: 128px; height: 128px; border-radius: 18px; flex-shrink: 0; }

.profile-modal-name {
    font-family: var(--game-font);
    font-size: 48px;
    font-weight: 900;
    line-height: 1.05;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.profile-modal-level {
    display: flex;
    align-items: center;
    gap: 6px;
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    color: rgba(255, 255, 255, 0.85);
}

.profile-modal-body { display: flex; flex-direction: column; gap: 10px; padding: 16px; overflow-y: auto; }

.profile-modal-stats {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 0 4px 6px;
    font-family: var(--game-font);
    font-size: 16px;
    font-weight: bold;
    color: var(--ink-muted);
}

.profile-modal-viewfull {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 14px;
    border: 3px solid #000;
    border-radius: 12px;
    background: var(--accent, #d0d20d);
    color: var(--ink, #16161d);
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: 900;
    text-decoration: none;
    text-align: center;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.3);
}

.profile-modal-viewfull:hover { filter: brightness(1.08); }

/* Guest card: identity only, nothing to click — a guest owns no friend list,
   no badges row (Trophies is per-account) and no /u/ page (findByAccount has
   no account name to resolve), so the body is one line instead of a menu of
   dead ends. */
.profile-modal-anon {
    margin: 0;
    padding: 4px 2px 2px;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    color: var(--ink-muted);
    text-align: center;
}

.badge-card {
    display: flex;
    gap: 11px;
    align-items: flex-start;
    padding: 10px 12px;
    border: 2px solid #000;
    border-radius: 10px;
    background: var(--surface);
    font-family: var(--game-font);
}

.badge-card.is-earned {
    border-color: #b8860b;
    background: linear-gradient(180deg, #fffdf2, #ffedad);
    box-shadow: 0 3px 0 rgba(184, 134, 11, 0.45);
}

.badge-card.is-pinned {
    border-color: #746FFC;
    box-shadow: 0 3px 0 rgba(116, 111, 252, 0.45);
}

/* Pin toggle: a third flex child beside the name/description rather than
   absolutely placed, so a long description wrapping to a third line can
   never run underneath it. */
.badge-pin {
    flex: 0 0 auto;
    align-self: center;
    padding: 4px 9px;
    border: 2px solid #000;
    border-radius: 7px;
    background: var(--surface);
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 17.6px;
    font-weight: 900;
    cursor: pointer;
}

.badge-pin.is-pinned {
    background: linear-gradient(180deg, #8b87ff, #746FFC);
    color: #fff;
    border-color: #000;
}

.badges-pin-hint {
    font-family: var(--game-font);
    font-size: 17.6px;
    font-weight: bold;
    color: rgba(255, 255, 255, 0.75);
}

.badge-card.is-locked {
    border-color: #b9bec7;
    background: #eceef1;
}

.badge-card.is-locked .badge-icon {
    filter: grayscale(1);
    opacity: 0.55;
}

.badge-card.is-locked .badge-name,
.badge-card.is-locked .badge-desc {
    color: #868e99;
}

.badge-icon {
    flex: 0 0 auto;
    font-size: 27px;
    line-height: 1.2;
}

.badge-info {
    flex: 1 1 auto;
    min-width: 0;
}

.badge-name {
    font-size: 14px;
    font-weight: 900;
    color: var(--ink-navy);
}

.badge-desc {
    font-size: 12px;
    color: #4d5766;
    margin-bottom: 6px;
}

.badge-bar {
    height: 9px;
    border: 1px solid rgba(0, 0, 0, 0.35);
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.1);
    overflow: hidden;
}

.badge-bar-fill {
    height: 100%;
    border-radius: 5px;
    background: linear-gradient(90deg, #9aa5b5, #c3ccd9);
    transition: width 0.3s;
}

.badge-card.is-earned .badge-bar-fill {
    background: linear-gradient(90deg, #f6d83f, #ffb300);
}

.badge-progress {
    margin-top: 3px;
    font-size: 11px;
    font-weight: bold;
    color: var(--ink-muted);
}

.badge-card.is-earned .badge-progress {
    color: #8a6d00;
}

/* ── Trophy unlock toast ── */

.trophy-toast {
    position: fixed;
    left: 50%;
    bottom: 74px;
    z-index: 9500;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 22px;
    border: 3px solid #000;
    border-radius: 12px;
    background: linear-gradient(180deg, #fff8dc, #ffe082);
    box-shadow: 0 6px 0 rgba(0, 0, 0, 0.35);
    font-family: var(--game-font);
    transform: translate(-50%, 24px);
    opacity: 0;
    transition: transform 0.35s cubic-bezier(0.2, 1.4, 0.4, 1), opacity 0.3s;
    pointer-events: none;
}

.trophy-toast.is-visible {
    transform: translate(-50%, 0);
    opacity: 1;
}

.trophy-toast-icon { font-size: 30px; }

.trophy-toast-text {
    display: flex;
    flex-direction: column;
}

.trophy-toast-label {
    font-size: 11px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: #8a6d00;
}

.trophy-toast-name {
    font-size: 17px;
    font-weight: 900;
    color: var(--ink-navy);
}

@media (max-width: 700px) {
    .badges-grid {
        grid-template-columns: 1fr;
    }
}

/* ═══ Inventory (profile menu) ══════════════════════════════════════════════ */

.inv-card { width: min(94vw, 920px); }

.inv-body {
    padding: 14px 16px 18px;
    overflow-y: auto;
}

.inv-body .badges-section-label { display: block; margin: 12px 0 8px; }

.inv-bundles {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.inv-bundle {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--surface);
    border: 2px solid #cdd3ec;
    border-radius: 10px;
    padding: 8px 10px;
}

.inv-bundle--new { border-style: dashed; }

.inv-bundle-name {
    flex: 0 0 190px;
    min-width: 0;
    background: var(--surface-2);
    border: 1px solid #cdd3ec;
    border-radius: 7px;
    padding: 7px 9px;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink-navy);
    font-family: inherit;
}

.inv-bundle .cos-mini-row { flex: 1; margin-left: 0; flex-wrap: wrap; }

.inv-none { font-size: 12px; color: #9aa0bd; }

.inv-btn {
    background: var(--surface-2);
    border: 2px solid #b9c2e8;
    color: var(--ink-navy);
    border-radius: 8px;
    padding: 7px 12px;
    font-size: 12.5px;
    font-weight: 700;
    cursor: pointer;
    white-space: nowrap;
}

.inv-btn:hover { border-color: #6a68fe; }

.inv-btn--on {
    background: #3ddc84;
    border-color: #1f9d5a;
    color: #08331c;
}

.inv-btn--del {
    color: #d43a52;
    padding: 7px 9px;
}

.inv-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(136px, 1fr));
    gap: 10px;
}

.inv-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    background: var(--surface);
    border: 2px solid var(--rar, #cdd3ec);
    border-radius: 12px;
    padding: 10px 8px;
    text-align: center;
    /* The admin account owns the whole player-authored cursor catalog, so this
       grid can run into the hundreds of tiles. Skipping layout/paint/animation
       for the ones currently scrolled out of view (content-visibility) is what
       keeps that many hue-cycling <img> tiles from all ticking at once during
       a scroll — contain-intrinsic-size just reserves a sane placeholder size
       so the scrollbar doesn't jump around as tiles skip in and out. */
    content-visibility: auto;
    contain-intrinsic-size: 136px 158px;
}

.inv-item.is-equipped { box-shadow: 0 0 0 3px rgba(61, 220, 132, 0.35); }

.inv-item-preview {
    display: grid;
    place-items: center;
    min-height: 50px;
    font-size: 30px; /* emoji items */
    line-height: 1;
}

.inv-item-name {
    font-size: 12.5px;
    font-weight: 700;
    color: var(--ink-navy);
    line-height: 1.25;
}

.inv-item .inv-btn { width: 100%; }

.inv-empty {
    color: #7a80a5;
    font-size: 14px;
    text-align: center;
    padding: 22px 0;
}

/* ── Locker layout ──
   The owned-items half of the Inventory (renderInventory in script.js), styled
   after Fortnite's Locker: square rarity-bordered tiles grouped into category
   rows on the left, one big sticky preview pane on the right for whichever
   tile was last clicked. Bundles above the rows and the empty state are
   unaffected — this only replaces the old one-tile-per-item .inv-grid list. */
.inv-layout {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}

.inv-scroll {
    flex: 1;
    min-width: 0;
}

.inv-cat-label {
    margin: 14px 0 6px;
    font-family: var(--game-font);
    font-size: 15px;
    font-weight: 900;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.inv-cat-label:first-child { margin-top: 0; }

.inv-tilerow {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: 8px;
}

.inv-tile {
    position: relative;
    display: grid;
    place-items: center;
    aspect-ratio: 1;
    background: color-mix(in srgb, var(--rar, #cdd3ec) 16%, var(--surface));
    border: 3px solid var(--rar, #cdd3ec);
    border-radius: 10px;
    padding: 6px;
    font-family: inherit;
    cursor: pointer;
    /* Same reasoning as the old .inv-item: the admin account's player-authored
       cursor catalog runs into the hundreds of hue-cycling tiles, and this
       keeps the ones scrolled out of view from all animating at once. */
    content-visibility: auto;
    contain-intrinsic-size: 72px 72px;
}

.inv-tile:hover { filter: brightness(1.08); }

.inv-tile.is-selected {
    border-color: #ffd23d;
    box-shadow: 0 0 0 2px #ffd23d, 0 0 14px rgba(255, 210, 61, 0.55);
}

.inv-tile-art {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    font-size: 24px; /* emoji items */
    line-height: 1;
}

.inv-tile-check {
    position: absolute;
    top: -7px;
    right: -7px;
    width: 20px;
    height: 20px;
    display: grid;
    place-items: center;
    background: #3ddc84;
    border: 2px solid #08331c;
    border-radius: 50%;
    font-size: 11px;
    color: #08331c;
}

.inv-preview-pane {
    flex: 0 0 210px;
    position: sticky;
    top: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--rar, #cdd3ec) 24%, var(--surface-2)), var(--surface-2));
    border: 3px solid var(--rar, #cdd3ec);
    border-radius: 14px;
    padding: 18px 0 0;
    overflow: hidden;
}

.inv-preview-art {
    display: grid;
    place-items: center;
    min-height: 170px;
    font-size: 64px; /* emoji items */
}

.inv-preview-footer {
    width: 100%;
    padding: 10px 14px 14px;
    background: rgba(0, 0, 0, 0.3);
    text-align: left;
}

.inv-preview-label {
    font-family: var(--game-font);
    font-size: 11.5px;
    font-weight: 800;
    letter-spacing: 1.5px;
    color: rgba(255, 255, 255, 0.75);
    text-transform: uppercase;
}

.inv-preview-name {
    font-family: var(--game-font);
    font-size: 19px;
    font-weight: 900;
    color: #fff;
    margin: 2px 0 10px;
    line-height: 1.15;
}

.inv-btn--equip { width: 100%; }

@media (max-width: 700px) {
    .inv-layout { flex-direction: column-reverse; }
    .inv-preview-pane { flex: none; width: 100%; position: static; }
}

.loadout-name {
    display: block;
    font-size: 10px;
    font-weight: 700;
    margin-top: 3px;
    max-width: 84px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ═══ Shop, W-Bucks & Cosmetics ═════════════════════════════════════════════ */

/* The shop is a full-bleed storefront, not a menu card: break out of the
   3-column menu grid so it spans the whole page instead of one narrow column.

   It is also the one screen with a hard height. The header and footer are
   fixed 90px bands, so the storefront takes exactly what is left and never a
   pixel more — the whole shop has to be readable at a glance, which a
   scrollbar defeats. Everything inside sizes down to fit rather than
   overflowing. */
/* The shop is a full-bleed storefront over the sunset, not a menu card: it
   breaks out of the menu grid and takes the whole window, and nothing in it
   scrolls. Everything inside sizes down to fit rather than overflowing. */
.grid-container.shop-container {
    display: block;
    width: 100%;
    max-width: none;
    /* The window less <main>'s 20px padding above and below. */
    height: calc(100vh - 40px);
    overflow: hidden;
}

.menu-stage:has(> .shop-container) {
    padding: 0;
}

/* Dressed like the home menu's sub-screens: dark plates, one hue per pane,
   Mono capitals. It fills the space between the banner and the infobar
   (home-menu.css) and everything is in cqw of the window, so the whole shop
   scales with it the way the menu does. Nothing scrolls. */
.shopx {
    position: fixed;
    inset: 0;
    z-index: 1;
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    padding: calc(6.2cqw * var(--wdm-k, 1)) 3cqw calc(5.9cqw * var(--wdm-k, 1));
    font-size: 1cqw;
    line-height: 1.2;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: #d6d6d6;
}
.shopx * { border-radius: 0; }

/* The banner carries the screen's name and the string runs right under it,
   so the wallet (and the admin tools) sit on their own in the bottom right,
   above the infobar. */
.shopx-topbar {
    position: absolute;
    right: 3cqw;
    bottom: calc(6.6cqw * var(--wdm-k, 1));
    z-index: 2;
    display: flex;
    justify-content: flex-end;
}

.shopx-tools {
    display: flex;
    align-items: center;
    gap: .5cqw;
}

/* Parallelogram plates, the same cut as the infobar's link buttons. */
.wbucks-chip,
.shopx-refresh {
    display: inline-flex;
    align-items: center;
    gap: .45cqw;
    padding: .45cqw 1.4cqw;
    border: 0;
    clip-path: polygon(.6cqw 0, 100% 0, calc(100% - .6cqw) 100%, 0 100%);
    background: #303030;
    box-shadow: inset -.1cqw -.1cqw 0 rgb(0 0 0 / .45);
    white-space: nowrap;
    text-decoration: none;
    text-transform: uppercase;
    cursor: pointer;
    transition: background .12s, color .12s;
}

.wbucks-chip {
    font-size: 1.25cqw;
    line-height: 1;
    color: #f8d08f;
}

.wbucks-chip:hover, .wbucks-chip:focus-visible { background: #3a2a1a; outline: none; }
.wbucks-chip svg { width: 1.2cqw; height: 1.2cqw; }

.wbucks-chip small {
    font-size: .75cqw;
    color: #d68c2a;
}

/* ── The clothesline ───────────────────────────────────────────────────────
   One string hanging across the window under the banner, today's cursors
   hanging off it as price tags in their rarity's colour. The line is a size
   container so the tags take their height from it, and 1cqw in here is 1%
   of its width, the unit the string is placed in. */
.shopx-line {
    /* How far the rail sits below the box's top, in percent of its width
       (hangTags reads it; the box is dropped to place it anyway). */
    --ends-y: 1.5;
    position: relative;
    flex: 1;
    min-height: 0;
    margin: -2.6cqw -3cqw 0;
    container-type: size;
    /* Clipped across only (tags going round past the ends), so the line can
       be dropped below its box to the bottom of the window (hangTags). */
    overflow-x: clip;
    overflow-y: visible;
}

/* A tile, centred on the rail at (--x, --y), its top .14 of its width
   (--tw) above the rail. hangTags moves it as the rack scrolls. */
.shopx-tag {
    --tw: 194px;
    position: absolute;
    left: calc(var(--x, 50) * 1cqw);
    top: calc(var(--y, 0) * 1cqw - var(--tw) * .14);
    width: var(--tw);
    margin-left: calc(var(--tw) * -.5);
}

/* Waiting off the edges for their turn. */
.shopx-tag.is-away { visibility: hidden; }

/* The middle slot is the selection (--focus 1, set by hangTags): drawn at
   6x and full brightness; the rest are shrunk to 5x (5/6, about their middle,
   so the ink stays centred between the corner wedges) and dimmed. Hover lifts a tile 4px on top of that. */
.shopx-tag-swing {
    position: relative;
    transform: scale(calc(5 / 6 + var(--focus, 0) / 6));
    transform-origin: 50% 50%;
    filter: brightness(calc(.6 + .4 * var(--focus, 0)));
    transition: translate .1s;
}
.shopx-tag:hover .shopx-tag-swing { translate: 0 -4px; filter: none; }

/* The tile: no box of its own, just the cursor standing over the sunset;
   the slot in the middle (.shopx-slot) is where the colour is. */
.shopx-tag-card {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 40px;
    justify-content: center;
    padding: 8px 3px;
    color: #141414;
}

/* The middle slot of the rack, the selection: two dark columns the width of
   the grid's bars either side of it, from the roof down under the infobar,
   and between them the selected cursor's rarity colour, which crossfades as
   the rack snaps. hangTags places it and gives it the selected tile's
   rarity class. First in the section, so the tiles stand in front of it. */
.shopx-slot {
    position: absolute;
    z-index: 0;
    box-sizing: border-box;
    border-left: var(--g-bar) solid transparent;
    border-right: var(--g-bar) solid transparent;
    background:
        linear-gradient(rgb(255 255 255 / .14), transparent 35%, rgb(0 0 0 / .14)),
        var(--rar, transparent);
    background-clip: padding-box;
    transition: background-color .14s;
    pointer-events: none;
}

/* The slot's two dark columns, drawn over the tiles so the cursors going
   round slide in behind them (the grid's z-index 1 is the same layer). */
.shopx-slot-cols {
    position: absolute;
    z-index: 1;
    box-sizing: border-box;
    border-left: var(--g-bar) solid #1a1a1a;
    border-right: var(--g-bar) solid #1a1a1a;
    pointer-events: none;
}

/* The selected cursor's plates along the bottom of the slot, between the
   columns: its name in one, the price or buy button in the one under it. */
.shopx-slot-title,
.shopx-slot-info {
    position: absolute;
    z-index: 2;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .25vw;
    padding: 0 1.4vw;
    background: #1a1a1a;
    color: #fff;
}
.shopx-slot-title { padding: 0 1.4vw; }
.shopx-slot-name {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 1.2vw;
    line-height: 1;
    text-transform: uppercase;
    letter-spacing: .06em;
}
.shopx-slot-info .cos-btn {
    min-width: 0;
    max-width: 100%;
    flex: none;
    padding: .3vw 1vw;
    font-size: .85vw;
    white-space: nowrap;
}

/* Plain rectangles in the shop, not the infobar's parallelograms. */
.shopx .cos-btn { clip-path: none; }

.shopx-tag-name {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 22px;
    line-height: 1.1;
}

.shopx-tag .cos-btn {
    min-width: 0;
    max-width: 100%;
    font-size: 14px;
}

/* The box the cursor stands in. Shorter than the hand that shows on hover,
   so the hand spills over the name and the tag cuts off whatever is left.
   Flex, not the stage's usual grid: the art is wider than the tag's padded
   inside, and a grid lets it hang off to the right where flex keeps it
   centred. */
.shopx-tag .cos-stage {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 224px;
    width: 100%;
}

/* A cursor tag is a set size (--tw across, --th down) with the cursor
   centred in whatever the hole and the padding leave. It is centred on its
   spot on the string, so a narrower tag loses the same from both sides. */
.shopx-line .shopx-tag { --tw: 144px; --th: 210px; }
.shopx-line .shopx-tag-card { height: var(--th); }
.shopx-line .shopx-tag .cos-stage {
    flex: 1;
    height: auto;
    min-height: 0;
}

/* Each sprite slid by its own offsets (shop-screen.js inkOffset) so the
   drawn pixels are centred on the tag rather than the canvas around them,
   and the hand lands exactly where the arrow was. */
.shopx-line .cos-sprite { translate: var(--ox, 0) var(--oy, 0); }
.shopx-line .cos-sprite--hand { translate: var(--oxh, 0) var(--oyh, 0); }

/* The frame: the dark shapes around the shop's windows of sky. A column
   down each edge, cut into three equal cells by bars the colour of the
   banner, and the middle window between the two uprights. Cells run from
   the banner's bottom edge to the top of the infobar's corner tabs; each
   column stops where the tab's slanted top ends, so the upright beside it
   lands on the tab's corner. Themes fill the left cells, the hanging things
   the right ones.

   Two rules keep the dark in one piece without patching:
   - Every edge where two dark shapes meet is rounded to a whole device
     pixel (--px, set by hangTags), so the edges land on pixel boundaries and
     meet exactly, with no anti-aliased hairline between them.
   - Where the shop meets the banner or the infobar, its shapes carry on
     under them (the chrome is drawn on top) to y=0 and to --g-under, so no
     edge ever has to line up with the chrome's own.

   vw, not cqw: the banner and infobar are sized off the full window, and
   .shopx's own cqw is its padded content box. Registered as lengths so
   hangTags reads them back as px and places the rack on the same pixels. */
@property --g-top     { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-ceiling { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-bottom  { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-floor   { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-under   { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-col     { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-bar     { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-wedge-w { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --g-wedge-h { syntax: '<length>'; inherits: true; initial-value: 0px; }
.shopx {
    --px: 1px;
    /* The banner's bottom edge over the side columns (y=121 of its 122-tall
       viewBox, shifted up .625) and its flat bottom over the middle (y=77,
       less the 12 it is shifted up). */
    --g-top: round(calc(5.677vw * var(--wdm-k, 1)), var(--px));
    --g-ceiling: round(calc(3.3854vw * var(--wdm-k, 1)), var(--px));
    /* Up from the window's bottom: the tabs' top (4.4 bar + 1.2 tab), the
       panel's top edge (4.4 less its 1.0417 drop), and the foot of the
       panel's full-width band (41.333% of its 3 down), where the frame
       ends out of sight. */
    --g-bottom: round(calc(5.6vw * var(--wdm-k, 1)), var(--px));
    --g-floor: round(calc(3.3583vw * var(--wdm-k, 1)), var(--px));
    --g-under: round(calc(2.1183vw * var(--wdm-k, 1)), var(--px));
    --g-col: round(calc(22.1vw * var(--wdm-k, 1)), var(--px));
    --g-bar: round(calc(1vw * var(--wdm-k, 1)), var(--px));
    --g-cell: calc((100% - 2 * var(--g-bar)) / 3);
    /* The right column is split the other way: two bars running down it,
       parallel to the keyring strings, three equal lanes, one keyring and
       the accessory under it in each. */
    --g-lane: calc((var(--g-col) - 2 * var(--g-bar)) / 3);
    /* The corner wedges of the middle window: the size of the sky wedge
       the banner's step leaves, 3.944vw across the narrow end. */
    --g-wedge-w: round(calc(9.618vw * var(--wdm-k, 1)), var(--px));
    --g-wedge-h: round(calc(2.2917vw * var(--wdm-k, 1)), var(--px));
}

/* One dark shape per side, column and upright together, from the top of
   the window to under the infobar. The left one is solid: the themes cover
   its cells and the bars between them are what shows. */
.shopx-frame {
    position: absolute;
    top: 0;
    bottom: var(--g-under);
    width: calc(var(--g-col) + var(--g-bar));
    z-index: 1;
    background: #1a1a1a;
    pointer-events: none;
}
.shopx-frame--l { left: 0; }
/* The right one has the three keyring lanes cut out of it, open to the sky
   from the banner down to the bar over the accessories (where the hanging
   grid's first row ends: two cells and a bar). 100% here is the frame's
   height, so the window's is 100% + --g-under. The outer lane is a notch in
   the right edge; the other two are holes, reached and left along the same
   line so the path to them adds nothing. */
.shopx-frame--r {
    --lanes-b: calc(var(--g-top) + var(--g-bar)
        + 2 * (100% + var(--g-under) - var(--g-top) - var(--g-bottom) - 2 * var(--g-bar)) / 3);
    --x1: var(--g-bar);
    --x2: calc(2 * var(--g-bar) + var(--g-lane));
    --x3: calc(3 * var(--g-bar) + 2 * var(--g-lane));
    right: 0;
    clip-path: polygon(evenodd,
        0 0, 100% 0, 100% var(--g-top), var(--x3) var(--g-top), var(--x3) var(--lanes-b),
        100% var(--lanes-b), 100% 100%, 0 100%,
        var(--x1) var(--g-top), calc(var(--x1) + var(--g-lane)) var(--g-top),
        calc(var(--x1) + var(--g-lane)) var(--lanes-b), var(--x1) var(--lanes-b),
        var(--x1) var(--g-top), 0 100%,
        var(--x2) var(--g-top), calc(var(--x2) + var(--g-lane)) var(--g-top),
        calc(var(--x2) + var(--g-lane)) var(--lanes-b), var(--x2) var(--lanes-b),
        var(--x2) var(--g-top), 0 100%);
}

/* The four corners of the clothesline's space, cut by a wedge each: the
   top pair hangs off the bundles at the roof (--roof, set by hangTags),
   the bottom pair stands on the infobar's panel and carries on under it,
   so their slants line up with the tabs'. z-index 0 and first in the
   section, so the price tags hang in front of them. */
.shopx-corners i {
    position: absolute;
    z-index: 0;
    width: var(--g-wedge-w);
    height: var(--g-wedge-h);
    background: #1a1a1a;
}
.shopx-corners i:nth-child(-n + 2) {
    top: var(--roof, var(--g-ceiling));
    clip-path: polygon(0 0, 100% 0, 41.01% 100%, 0 100%);
}
.shopx-corners i:nth-child(n + 3) {
    bottom: var(--g-under);
    height: calc(var(--g-wedge-h) + var(--g-floor) - var(--g-under));
    clip-path: polygon(0 0, 41.01% 0, 100% var(--g-wedge-h), 100% 100%, 0 100%);
}
.shopx-corners i:nth-child(odd) { left: calc(var(--g-col) + var(--g-bar)); }
.shopx-corners i:nth-child(even) {
    right: calc(var(--g-col) + var(--g-bar));
    scale: -1 1;
}

/* The middle cell: exactly the space between the two uprights, from the
   banner's bottom edge to the infobar's. Anything put in it lands dead
   centre; the box itself lets clicks through to the clothesline. */
.shopx-center {
    position: absolute;
    left: calc(var(--g-col) + var(--g-bar));
    right: calc(var(--g-col) + var(--g-bar));
    top: var(--g-top);
    bottom: var(--g-bottom);
    z-index: 2;
    display: grid;
    place-items: center;
    pointer-events: none;
}
.shopx-center > * { pointer-events: auto; }

/* Bundles of the day: the top of the middle window, over the clothesline,
   edge to edge
   between the uprights. One dark slab like the grid around it, split in two
   by a bar of the same width, each half a stage of its theme's art with
   the other pieces stood on it. */
.shopx-bundles {
    position: absolute;
    left: 0;
    right: 0;
    /* Up past the cell to the top of the window, under the banner, its
       contents starting at the banner's flat bottom; down to the roof
       (--bundles-bottom, set by hangTags). */
    top: calc(-1 * var(--g-top));
    bottom: calc(100% - var(--bundles-bottom, 60%));
    padding-top: var(--g-ceiling);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    background: #1a1a1a;
    font-size: .95vw;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: #d6d6d6;
}

.shopx-bundles-head {
    display: flex;
    justify-content: space-between;
    padding: .45vw .8vw;
    color: #f8d08f;
}

.shopx-bundles-row {
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--g-bar);
}

.shopx-bundle:only-child { grid-column: 1 / -1; }

.shopx-bundle {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.shopx-bundle-head,
.shopx-bundle-foot {
    display: flex;
    align-items: center;
    gap: .8vw;
    padding: .35vw .8vw;
}

.shopx-bundle-name { color: #fff; font-size: 1.2vw; }

.shopx-bundle-items {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-align: right;
    color: #8c8c8c;
}

.shopx-bundle-time { color: #8c8c8c; }

.shopx-bundle-stage {
    position: relative;
    flex: 1;
    min-height: 0;
}

.shopx-bundle-art {
    position: absolute;
    inset: 0;
    width: 100%;
    padding: 0;
    border: 0;
    cursor: pointer;
}

/* No boxes: the cursor stands on the left, the keyring hangs from the top
   edge on the right. The layer lets clicks through to the art, except on the
   pieces themselves. */
.shopx-bundle-pieces {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.shopx-bundle-piece {
    pointer-events: auto;
    position: absolute;
    display: grid;
    place-items: center;
}

.shopx-bundle-piece--cursor {
    left: 8%;
    top: 50%;
    transform: translateY(-50%);
}

.shopx-bundle-piece--keyring {
    right: 8%;
    top: 0;
    width: 6vw;
    height: 78%;
}

/* The live rope, same as the clothesline keyrings: it fills the piece. */
.shopx-bundle-piece--keyring { display: block; }
.shopx-bundle-piece--keyring .shopx-keyring-art { display: block; height: 100%; }

.shopx-bundle-piece.is-owned { opacity: .55; }

.shopx-bundle-foot { justify-content: flex-end; }

.shopx-bundle-foot s { color: #8c8c8c; }

.shopx-bundle-off {
    padding: .15vw .5vw;
    background: #00ff40;
    color: #141414;
}

.shopx-bundle-foot .cos-btn { min-width: 0; font-size: .95vw; }

/* Up to three themes, one per left cell, each stretched to fill it. */
.shopx-themes {
    position: absolute;
    left: 0;
    top: var(--g-top);
    bottom: var(--g-bottom);
    width: var(--g-col);
    z-index: 2;
    display: grid;
    grid-template-rows: repeat(3, var(--g-cell));
    row-gap: var(--g-bar);
}

.shopx-theme {
    min-height: 0;
    display: flex;
    flex-direction: column;
    background: var(--rar);
    padding: 4px;
}

.shopx-theme-art {
    display: block;
    width: 100%;
    flex: 1 1 0;
    min-height: 0;
    padding: 0;
    border: 0;
    cursor: pointer;
}

.shopx-theme-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 6px 4px 2px;
    color: #141414;
}

.shopx-theme-foot .shopx-tag-name { font-size: 16px; }
.shopx-theme-foot .cos-btn { min-width: 0; font-size: 13px; }

/* The right side, mirroring the themes: three keyrings hanging from the
   banner's bottom edge, the roof, and three accessories under them, down to
   the same 0.7cqw clear of the infobar. */
.shopx-hanging {
    position: absolute;
    right: 0;
    top: var(--g-top);
    bottom: var(--g-bottom);
    width: var(--g-col);
    z-index: 2;
    display: grid;
    grid-template-rows: calc(2 * var(--g-cell) + var(--g-bar)) var(--g-cell);
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--g-bar);
}

.shopx-keyrings,
.shopx-accs {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    min-height: 0;
}

.shopx-keyrings { grid-row: 1; gap: var(--g-bar); }
.shopx-accs { grid-row: 2; gap: var(--g-bar); }


.shopx-keyring {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    min-height: 0;
}

/* The string runs from the roof down to the ornament, however tall that is. */
.shopx-keyring-art {
    flex: 1 1 0;
    min-height: 0;
    width: 100%;
    position: relative;
}

/* The rope's canvas spills 140px past the box left, right and down
   (CANVAS_REACH in shop-screen.js) so a swing is never clipped, and lets
   the pointer through to the buttons. */
.shopx-keyring-canvas {
    position: absolute;
    left: -140px;
    top: 0;
    pointer-events: none;
}

.shopx-acc {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 6px 4px;
    background:
        linear-gradient(rgb(255 255 255 / .14), transparent 35%, rgb(0 0 0 / .14)),
        var(--rar);
}

.shopx-acc-art {
    display: flex;
    align-items: center;
    justify-content: center;
    height: calc(96px * var(--wdm-k, 1));
}

.shopx-keyring .cos-btn,
.shopx-acc .cos-btn { min-width: 0; max-width: 100%; font-size: calc(13px * var(--wdm-k, 1)); }

/* Arriving, four blocks slide in from the four sides: the themes from the
   left, the hanging things from the right, the bundles (with the top corner
   wedges under them) from the top and the cursor rack (with its slot and the
   bottom wedges) from the bottom. The rack's tiles move, not .shopx-line:
   hangTags places the line by measuring it, and a line still 75vh down
   got placed 75vh too high. Scoped to .shopx--intro (the bundles to
   .shopx--bundles-in, they load a moment later) so a buy or an equip does
   not run them again; the delay is less --ago, how long the intro has
   already been running, so a re-render mid-intro carries on instead of
   starting over. */
/* The side blocks take their half of the frame with them. */
.shopx { --shopx-side: calc(var(--g-col) + var(--g-bar) + 2vw); }
@keyframes shopxFromLeft  { from { translate: calc(-1 * var(--shopx-side)) 0; } }
@keyframes shopxFromRight { from { translate: var(--shopx-side) 0; } }
/* By the bundles' height (--bundles-h, set by hangTags) rather than 100%,
   so the wedges hanging off them move by exactly as much. */
@keyframes shopxFromTop   { from { translate: 0 calc(-1 * var(--bundles-h, 30vh) - 6vw); } }
@keyframes shopxFromBelow { from { translate: 0 75vh; } }

.shopx--intro .shopx-line .shopx-tag,
.shopx--intro .shopx-slot,
.shopx--intro .shopx-slot-cols,
.shopx--intro .shopx-slot-title,
.shopx--intro .shopx-slot-info,
.shopx--intro .shopx-corners i:nth-child(n + 3),
.shopx--bundles-in .shopx-bundles,
.shopx--bundles-in .shopx-corners i:nth-child(-n + 2) {
    animation: 640ms cubic-bezier(.16, 1, .3, 1) backwards;
    animation-delay: calc(0ms - var(--ago));
}
.shopx--intro :is(.shopx-themes, .shopx-frame--l) {
    animation: shopxFromLeft 640ms cubic-bezier(.16, 1, .3, 1) backwards;
    animation-delay: calc(0ms - var(--ago));
}
.shopx--intro :is(.shopx-hanging, .shopx-topbar, .shopx-frame--r) {
    animation: shopxFromRight 640ms cubic-bezier(.16, 1, .3, 1) backwards;
    animation-delay: calc(0ms - var(--ago));
}
.shopx--bundles-in .shopx-bundles,
.shopx--bundles-in .shopx-corners i:nth-child(-n + 2) {
    animation-name: shopxFromTop;
    animation-delay: calc(0ms - var(--bundles-ago));
}
/* After a fresh load the bundles arrive a moment after the shop: until
   they do, the top wedges stay hidden, so they come with them rather than
   sitting in place first. If none come, they fade in on their own. */
@keyframes shopxWedgeFade { from { opacity: 0; } }
.shopx--intro:not(.shopx--bundles-in) .shopx-corners i:nth-child(-n + 2) {
    animation: shopxWedgeFade 300ms ease-out backwards;
    animation-delay: calc(2500ms - var(--ago));
}
.shopx--intro .shopx-line .shopx-tag,
.shopx--intro .shopx-slot,
.shopx--intro .shopx-slot-cols,
.shopx--intro .shopx-slot-title,
.shopx--intro .shopx-slot-info,
.shopx--intro .shopx-corners i:nth-child(n + 3) { animation-name: shopxFromBelow; }

/* Leaving (back or forward), the same four blocks go back out the way they
   came, accelerating, inside the screen swap's exit time. More specific than
   the intro, so leaving mid-intro still works. */
@keyframes shopxToLeft  { to { translate: calc(-1 * var(--shopx-side)) 0; } }
@keyframes shopxToRight { to { translate: var(--shopx-side) 0; } }
@keyframes shopxToTop   { to { translate: 0 calc(-1 * var(--bundles-h, 30vh) - 6vw); } }
@keyframes shopxToBelow { to { translate: 0 75vh; } }

.shop-container:is(.is-exiting, .is-exiting-back) {
    --shopx-out: 260ms cubic-bezier(.7, 0, 1, .35) forwards;
}
.shop-container:is(.is-exiting, .is-exiting-back) :is(.shopx-themes, .shopx-frame--l) {
    animation: shopxToLeft var(--shopx-out);
}
.shop-container:is(.is-exiting, .is-exiting-back) :is(.shopx-hanging, .shopx-topbar, .shopx-frame--r) {
    animation: shopxToRight var(--shopx-out);
}
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-bundles,
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-corners i:nth-child(-n + 2) {
    animation: shopxToTop var(--shopx-out);
}
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-line .shopx-tag,
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-slot,
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-slot-cols,
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-slot-title,
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-slot-info,
.shop-container:is(.is-exiting, .is-exiting-back) .shopx-corners i:nth-child(n + 3) {
    animation: shopxToBelow var(--shopx-out);
}

.rarity--common    { --rar: #7f8c9e; }
.rarity--uncommon  { --rar: #4ac736; }
.rarity--rare      { --rar: #35a2ff; }
.rarity--epic      { --rar: #b14dff; }
.rarity--legendary { --rar: #f0821e; }
.rarity--mythic    { --rar: #f2c93b; }
.rarity--exotic    { --rar: #22d3c5; }

.cos-stage {
    position: relative; /* above the floor and its shadow */
    display: grid;
    place-items: center;
    transition: transform 0.18s ease;
}

.cos-sprite {
    /* Size comes from the inline width/height cosmeticPreview writes: it fits the
       sprite to the tile at a whole-number scale, which a CSS height would undo. */
    image-rendering: pixelated;
    image-rendering: crisp-edges;
    
}

.cos-emoji {
    display: block;
    line-height: 1;
    
}

/* A cursor and its hand version, stacked rather than side by side: the arrow
   is what is on show, and hovering the tile turns it into the hand — the same
   thing the cursor itself does when it passes over something clickable, so the
   tile demonstrates the item instead of describing it.

   One grid cell holding both, so the stage takes the size of the larger and
   the swap cannot resize the tile or nudge the caption under it. */
.cos-swap {
    display: grid;
    place-items: center;
}

.cos-swap > .cos-sprite {
    grid-area: 1 / 1;
    transition: opacity 0.12s ease;
}

.cos-swap > .cos-sprite--hand { opacity: 0; }

.shopx-tag:hover .cos-swap > .cos-sprite { opacity: 0; }
.shopx-tag:hover .cos-swap > .cos-sprite--hand { opacity: 1; }

/* The inventory row shows the same pair and swaps the same way. */
.inv-item:hover .cos-swap > .cos-sprite { opacity: 0; }
.inv-item:hover .cos-swap > .cos-sprite--hand { opacity: 1; }

/* A tile nobody can hover would only ever show the arrow, so the hand gets its
   turn on tap instead — the tile is the only place either sprite is visible. */
@media (hover: none) {
    .shopx-tag:active .cos-swap > .cos-sprite,
    .inv-item:active .cos-swap > .cos-sprite { opacity: 0; }

    .shopx-tag:active .cos-swap > .cos-sprite--hand,
    .inv-item:active .cos-swap > .cos-sprite--hand { opacity: 1; }
}

/* `place-items: stretch` on the grid above SHOULD be enough on its own to
   size this — that's what it's for — but a <span> turned flex container
   sitting as the lone item of an implicit auto-sized grid track doesn't
   reliably take the stretch in every engine that has to render this; its
   `align-self` reads back as the inherited `auto` rather than a resolved
   `stretch`, and it settles for shrink-wrapping to its content (the
   ornament) instead. Explicit width/height sidesteps needing that
   resolution to go a particular way at all. Without it the string (flex: 1
   below, with nothing to grow into) collapses to zero height — invisible —
   and the ornament, no longer pinned to the bottom of a tile that fills
   the tile, wasn't sitting at the bottom of anything, just the middle of
   whatever shrink-wrapped box it made on its own. */
.cos-keyring-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    height: 100%;
}

/* The ornament is the part worth seeing, so cosmeticPreview scales it to
   fill the tile's width on its own — bigger than sharing one scale with
   the string ever let it be — and it sits at the bottom simply by being
   the fixed-size sibling after the string's flexible one, the way the
   heavy end of a real keyring hangs off the end of its chain.
   `flex-shrink: 0` keeps it at that requested size under pressure — the
   string (min-height: 0, just below) is what's meant to give, not this. */
.cos-keyring-ornament {
    flex-shrink: 0;
}

/* The string fills whatever height the ornament doesn't need, rather than
   rendering once at native size and leaving most of the tile empty above a
   small charm: its sprite is tiled down that space as a repeating CSS
   background — cosmeticPreview sets the actual size/url — the flat,
   static-tile version of what keyring-physics.js does for real by drawing
   the sprite row by row along a simulated rope. */
.cos-keyring-string {
    display: block;
    flex: 1;
    min-height: 0;
    background-repeat: repeat-y;
    background-position: top center;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
}

/* A kite floats up, so it's the other way round: string pinned to the floor
   of the tile, kite flying above it instead of dangling from the roof. */
.cos-keyring-tile--kite {
    flex-direction: column-reverse;
}
.cos-keyring-tile--kite .cos-keyring-string {
    background-position: bottom center;
}

/* Static by default — a keyring's string never moves in the shop, so hovering
   the tile does nothing unless the ornament itself is built to spin on its
   own (see below). */
.cos-keyring-ornament--spin {
    transform-origin: 50% 50%;
}

.shopx-tag:hover .cos-keyring-ornament--spin,
.shopx-keyring:hover .cos-keyring-ornament--spin {
    animation: cosKeyringSpin 1s linear infinite;
}

@keyframes cosKeyringSpin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .shopx-tag:hover .cos-keyring-ornament--spin,
    .shopx-keyring:hover .cos-keyring-ornament--spin { animation: none; }
}

/* Mini cosmetic icons: a racer's loadout in rival panels and result rows. */
.cos-mini-row {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin-left: 7px;
    vertical-align: middle;
}

/* The one icon pulled out of that strip and placed before the name instead —
   see cosmeticCursorIcon() in script.js. */
.mp-rival-cursor {
    display: inline-flex;
    align-items: center;
    margin-right: 6px;
    vertical-align: middle;
}

.cos-mini {
    font-size: 12px;
    line-height: 1;
}

.cos-mini--sprite {
    /* Whole-number size set inline by cosmeticMiniIcon. */
    /* Inside the inline-flex .mp-rival-cursor an <img> is a flex item and was
       shrunk to a fraction of its inline width, squashing the pixels. */
    flex: none;
    max-width: none;
    vertical-align: middle;
    image-rendering: pixelated;
}

.cos-mini--swatch {
    width: 14px;
    height: 14px;
    border-radius: 4px;
    display: inline-block;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
}

/* Reserves the exact box lobbyRosterCursorIcon sized for the Negative's
   still-image stand-in — left empty on purpose. syncRosterLenses (script.js)
   lines the real mix-blend-mode lens up over this rect, so anything drawn
   here would just double up behind it. */
.cos-mini--lens-slot {
    display: inline-block;
}

/* Custom theme tile: a swatch of the article background, sized inline. */
.cos-theme-thumb {
    display: block;
    border-radius: 8px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25),
                3px 4px 0 rgba(20, 16, 40, 0.22);
}

/* The full-screen look a small rectangle can't give on its own — the theme's
   real background, at the size it'd actually be shown at. No card, no
   border: the shop dropped that chrome already (see .shopx-topbar above),
   and here even more so — the picture is the entire point of the screen. */
.theme-preview-overlay {
    position: fixed;
    inset: 0;
    /* Above .profile-btn (9999) and everything else in the normal UI stack
       (10001 is the highest of those) — this is meant to cover the whole
       screen, corner chrome included. */
    z-index: 10010;
    display: flex;
    background: rgba(6, 10, 24, 0.92);
    cursor: zoom-out;
    animation: mpFadeIn 0.15s ease-out;
}

.theme-preview-art {
    flex: 1;
    background-repeat: no-repeat;
    cursor: default;
}

.theme-preview-close {
    position: fixed;
    top: 16px;
    right: 16px;
    z-index: 1;
    width: 40px;
    height: 40px;
    border: 0;
    border-radius: 50%;
    background: rgba(6, 10, 24, 0.7);
    color: #fff;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}

.theme-preview-close:hover {
    background: rgba(6, 10, 24, 0.92);
}

.theme-preview-hide {
    position: fixed;
    top: 16px;
    right: 64px;
    z-index: 1;
    width: 40px;
    height: 40px;
    border: 0;
    border-radius: 50%;
    background: rgba(6, 10, 24, 0.7);
    color: #fff;
    font-size: 18px;
    cursor: pointer;
    display: grid;
    place-items: center;
}

.theme-preview-hide:hover {
    background: rgba(6, 10, 24, 0.92);
}

/* Phase two of the preview (see THEME_PREVIEW_FLIP_MS): the same background,
   but with the article panel that actually sits over it in a race —
   background and panel opacity/fg/link/border colors follow the same rule
   shape as customThemeCss's body.wiki-reading CSS. The panel itself
   (.theme-preview-skin) is NOT full-bleed: the picture is full-bleed
   (.article-stage), but the white/translucent panel is .wiki-skin, which
   .wiki-skin[data-width="standard"] caps to a centered max-width rectangle —
   real gameplay always shows picture on both sides of it, never edge to
   edge. This wrapper is what centers that rectangle. */
.theme-preview-mock {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    padding: 26px;
    box-sizing: border-box;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.4s ease;
}

.theme-preview-mock.is-shown {
    opacity: 1;
    pointer-events: auto;
}

/* Same columns, paddings and type sizes as the real .wiki-skin/.wiki-toc/
   .wiki-title/.wiki-content/.mp-side (see public/style.css's article-reading
   and multiplayer-rail rules) — copied rather than shared because this is
   static markup standing in for a real, live race. Third column carries the
   Appearance block and the race rail stacked under it — see
   THEME_PREVIEW_ARTICLE_HTML. 1520px matches
   .wiki-skin[data-width="standard"].has-side's own cap. */
.theme-preview-skin {
    --tfg: #202122;
    --tlink: #3366cc;
    --tborder: #a2a9b1;
    width: min(1520px, 100%);
    max-height: 100%;
    overflow-y: auto;
    box-sizing: border-box;
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr) 232px;
    gap: 0 26px;
    align-items: start;
    padding: 40px 26px 50px;
    background: #fff;
    color: var(--tfg);
    font-family: sans-serif;
}

.theme-preview-toc {
    position: sticky;
    top: 0;
    font-size: 13px;
}

.theme-preview-toc-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 6px;
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--tborder);
    font-weight: bold;
}

.theme-preview-toc-head span { color: var(--tlink); font-weight: 400; font-size: 12px; }
.theme-preview-toc ul { list-style: none; margin: 0; padding: 0; }
.theme-preview-toc li { padding: 4px 8px; border-left: 2px solid transparent; color: var(--tlink); }
.theme-preview-toc li.on { border-left-color: var(--tlink); color: var(--tfg); font-weight: 600; }

/* Collapsed by default, same as the real .wiki-toc-arrow/.is-collapsed —
   a heading with subsections gets the ▶ and nothing under it, not an
   always-open sublist. */
.theme-preview-toc-arrow {
    display: inline-flex;
    width: 12px;
    color: var(--tfg);
    opacity: 0.6;
    font-size: 9px;
}

/* Feed under Contents — see .mp-toc-feed for the real-game equivalent this
   mirrors. */
.theme-preview-toc-feed {
    margin-top: 20px;
    padding-top: 14px;
    border-top: 1px solid var(--tborder);
}

.theme-preview-page { min-width: 0; }

/* The infobox thumbnail every real article opens with — see
   THEME_PREVIEW_THUMB_SVG. Same float/border/caption shape as the real
   .wiki-content figure/.thumb rules. */
.theme-preview-thumb {
    float: right;
    clear: right;
    width: 240px;
    margin: 2px 0 12px 18px;
    padding: 3px;
    box-sizing: border-box;
    border: 1px solid var(--tborder);
    background: rgba(255, 255, 255, 0.5);
}

.theme-preview-thumb img { display: block; width: 100%; height: auto; border: 1px solid var(--tborder); }
.theme-preview-thumb figcaption { padding: 4px 2px 2px; font-size: 12px; line-height: 1.4; color: #54595d; }

.theme-preview-page h1 {
    margin: 0 0 4px;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--tborder);
    font-family: "Linux Libertine", Georgia, "Times New Roman", serif;
    font-weight: normal;
    font-size: 28px;
    line-height: 1.3;
}

.theme-preview-page h2 {
    margin: 24px 0 8px;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--tborder);
    font-family: "Linux Libertine", Georgia, "Times New Roman", serif;
    font-weight: normal;
    font-size: 22px;
    line-height: 1.3;
}

.theme-preview-page p { font-size: 14.4px; line-height: 1.6; margin: 0.5em 0; }
.theme-preview-page a { color: var(--tlink); text-decoration: none; }
.theme-preview-page dl { font-size: 14.4px; margin: 0.2em 0; }
.theme-preview-page dd { margin-left: 1.6em; }

.theme-preview-side { display: flex; flex-direction: column; gap: 20px; }

/* The rival rail — Racers / Item / Feed, same shape as the real .mp-side
   nested in .wiki-skin: no card chrome of its own (border/background come
   from the rail's own fixed styling, not the theme), text in --tfg, the
   quieter secondary lines in the same fixed muted gray the real rail uses
   regardless of theme (article/mp-rival-article's --wiki-muted is never
   part of what a theme overrides — only fg/link/border are). Game font here
   is right — this rail is genuine game UI overlaid on the article — but it
   must not leak into .theme-preview-app-block above: that one is mocking
   Wikipedia's own Appearance panel, which is plain sans-serif like the rest
   of .theme-preview-skin, never the game's font. */
.theme-preview-side-block { font-family: var(--game-font); }

.theme-preview-side-title {
    display: flex;
    justify-content: space-between;
    font-size: 13px;
    font-weight: bold;
    color: var(--tfg);
    padding-bottom: 6px;
    margin-bottom: 8px;
    border-bottom: 1px solid var(--tborder);
}

.theme-preview-side-title span { color: var(--tlink); font-weight: 400; }

/* Wikipedia's own reading-preference panel — see the comment on
   THEME_PREVIEW_ARTICLE_HTML for why this game's own equivalent isn't
   actually here during a real race, and why it's shown anyway. Same shape
   as cursor-maker.html's .tp-app. */
.theme-preview-app-lab { font-weight: 700; font-size: 12px; margin: 10px 0 3px; color: var(--tfg); }

.theme-preview-app-block label {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 2px 0;
    font-size: 12.5px;
    color: var(--tfg);
}

.theme-preview-radio {
    width: 11px;
    height: 11px;
    flex: none;
    border: 1px solid var(--tborder);
    border-radius: 50%;
    display: inline-block;
    background: #fff;
}

.theme-preview-radio.on {
    border-color: var(--tlink);
    background: radial-gradient(circle, var(--tlink) 0 3px, #fff 3.5px);
}

/* See POLKA_DOTS_THEME_ID in script.js — this one theme's own preview swaps
   the dot for the same ring-and-dot heart the real .mp-app-radio input gets
   once the theme is actually equipped (see that rule for the three-layer
   explanation): unchecked is a white heart outlined in the separator color,
   checked adds the theme's own pink as both the outline and the small
   centered dot. */
.theme-preview-app-block--hearts .theme-preview-radio {
    position: relative;
    width: 13px;
    height: 12px;
    border: none;
    border-radius: 0;
    background: var(--tborder);
    clip-path: path("M6.5 11 C2.5 8 1 6 1 3.8 C1 1.8 2.6 0.5 4.3 1.1 C5.4 1.5 6.5 2.5 6.5 2.5 C6.5 2.5 7.6 1.5 8.7 1.1 C10.4 0.5 12 1.8 12 3.8 C12 6 10.5 8 6.5 11 Z");
}

.theme-preview-app-block--hearts .theme-preview-radio::before {
    content: "";
    position: absolute;
    inset: 0;
    background: #fff;
    clip-path: path("M6.5 9.53 C3.62 7.37 2.54 5.93 2.54 4.35 C2.54 2.91 3.69 1.97 4.92 2.4 C5.71 2.69 6.5 3.41 6.5 3.41 C6.5 3.41 7.29 2.69 8.08 2.4 C9.31 1.97 10.46 2.91 10.46 4.35 C10.46 5.93 9.38 7.37 6.5 9.53 Z");
}

.theme-preview-app-block--hearts .theme-preview-radio::after {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--tlink);
    clip-path: path("M6.5 7.96 C4.82 6.7 4.19 5.86 4.19 4.93 C4.19 4.09 4.86 3.54 5.58 3.8 C6.04 3.96 6.5 4.38 6.5 4.38 C6.5 4.38 6.96 3.96 7.42 3.8 C8.14 3.54 8.81 4.09 8.81 4.93 C8.81 5.86 8.18 6.7 6.5 7.96 Z");
    opacity: 0;
}

.theme-preview-app-block--hearts .theme-preview-radio.on {
    background: var(--tlink);
}

.theme-preview-app-block--hearts .theme-preview-radio.on::after {
    opacity: 1;
}

/* See CASINO_THEME_IDS in script.js — the Casino theme's own Appearance
   radios are card suits, cut from img/casino/*.svg as masks. Each group of
   options (Text, Width, Color) runs spade, red diamond, club, red heart from
   its label down. The same ring-and-dot a real radio draws, like the polka
   hearts above: the suit, a white suit inside it leaving an outline, and a
   small suit in the middle once it is picked. Covers the real panel in a race
   (body[data-theme-radios]) and the shop preview (--suits). */
body[data-theme-radios="suits"] .mp-app-radio input,
.theme-preview-app-block--suits .theme-preview-radio,
.theme-preview-app-block--suits .theme-preview-radio.on {
    --suit: url(/img/casino/spade.svg);
    --suit-c: #292c38;
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 15px;
    height: 15px;
    margin: 0;
    border: none;
    border-radius: 0;
    background: var(--suit-c);
    -webkit-mask: var(--suit) center / contain no-repeat;
    mask: var(--suit) center / contain no-repeat;
    cursor: pointer;
}

body[data-theme-radios="suits"] .mp-app-lab + .mp-app-radio + .mp-app-radio input,
.theme-preview-app-block--suits .theme-preview-app-lab + label + label .theme-preview-radio {
    --suit: url(/img/casino/diamond.svg);
    --suit-c: #af1724;
}

body[data-theme-radios="suits"] .mp-app-lab + .mp-app-radio + .mp-app-radio + .mp-app-radio input,
.theme-preview-app-block--suits .theme-preview-app-lab + label + label + label .theme-preview-radio {
    --suit: url(/img/casino/club.svg);
    --suit-c: #292c38;
}

body[data-theme-radios="suits"] .mp-app-lab + .mp-app-radio + .mp-app-radio + .mp-app-radio + .mp-app-radio input,
.theme-preview-app-block--suits .theme-preview-app-lab + label + label + label + label .theme-preview-radio {
    --suit: url(/img/casino/heart.svg);
    --suit-c: #af1724;
}

body[data-theme-radios="suits"] .mp-app-radio input::before,
body[data-theme-radios="suits"] .mp-app-radio input::after,
.theme-preview-app-block--suits .theme-preview-radio::before,
.theme-preview-app-block--suits .theme-preview-radio::after {
    content: "";
    position: absolute;
    inset: 0;
    background: #fff;
    -webkit-mask: var(--suit) center / 62% no-repeat;
    mask: var(--suit) center / 62% no-repeat;
}

body[data-theme-radios="suits"] .mp-app-radio input::after,
.theme-preview-app-block--suits .theme-preview-radio::after {
    background: var(--suit-c);
    -webkit-mask-size: 34%;
    mask-size: 34%;
    opacity: 0;
}

body[data-theme-radios="suits"] .mp-app-radio input:checked::after,
.theme-preview-app-block--suits .theme-preview-radio.on::after {
    opacity: 1;
}

/* See THEME_RADIO_SHAPES in script.js: themes whose Appearance radios are a
   shape (img/theme-radios/*.svg as a mask) instead of a dot, built like the
   suits above. Unchecked is the shape in the theme's colour with the panel
   showing through its middle, leaving an outline; checked turns it the
   link colour and adds a small solid shape in the centre. Each shape sets
   its mask and unchecked colour; the rules below are shared. */
body[data-theme-radios="star"], .theme-preview-app-block[data-radios="star"] {
    --radio-mask: url(/img/theme-radios/star.svg); --radio-c: #6b7a4f;
}
body[data-theme-radios="drop"], .theme-preview-app-block[data-radios="drop"] {
    --radio-mask: url(/img/theme-radios/drop.svg); --radio-c: #22407c;
}
body[data-theme-radios="paw"], .theme-preview-app-block[data-radios="paw"] {
    --radio-mask: url(/img/theme-radios/paw.svg); --radio-c: #7a4a1c;
}
/* On the dark preview the dark shapes (the drop) would vanish into the
   panel, so unchecked shapes take the separator colour there. */
.theme-preview-skin[data-dark="true"] .theme-preview-app-block--shaped {
    --radio-c: var(--tborder);
}

body[data-theme-radios]:not([data-theme-radios="suits"]) .mp-app-radio input,
.theme-preview-app-block--shaped .theme-preview-radio,
.theme-preview-app-block--shaped .theme-preview-radio.on {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 15px;
    height: 15px;
    margin: 0;
    border: none;
    border-radius: 0;
    background: var(--radio-c);
    -webkit-mask: var(--radio-mask) center / contain no-repeat;
    mask: var(--radio-mask) center / contain no-repeat;
    cursor: pointer;
}

body[data-theme-radios]:not([data-theme-radios="suits"]) .mp-app-radio input::before,
body[data-theme-radios]:not([data-theme-radios="suits"]) .mp-app-radio input::after,
.theme-preview-app-block--shaped .theme-preview-radio::before,
.theme-preview-app-block--shaped .theme-preview-radio::after {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--radio-gap, #fff);
    -webkit-mask: var(--radio-mask) center / 56% no-repeat;
    mask: var(--radio-mask) center / 56% no-repeat;
}

body[data-theme-radios]:not([data-theme-radios="suits"]) .mp-app-radio input::after,
.theme-preview-app-block--shaped .theme-preview-radio::after {
    background: var(--tlink, var(--wiki-link));
    -webkit-mask-size: 34%;
    mask-size: 34%;
    opacity: 0;
}

body[data-theme-radios]:not([data-theme-radios="suits"]) .mp-app-radio input:checked,
.theme-preview-app-block--shaped .theme-preview-radio.on {
    background: var(--tlink, var(--wiki-link));
}

body[data-theme-radios]:not([data-theme-radios="suits"]) .mp-app-radio input:checked::after,
.theme-preview-app-block--shaped .theme-preview-radio.on::after {
    opacity: 1;
}

.theme-preview-rival {
    padding: 7px 10px;
    margin-bottom: 8px;
    border: 2px solid #c8ccd1;
}

.theme-preview-rival.is-me { border-color: #0032ff; }
.theme-preview-rival:last-child { margin-bottom: 0; }

.theme-preview-rival-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    font-weight: 900;
    font-size: 14px;
    color: var(--tfg);
}

.theme-preview-rival-mv { font-weight: bold; font-size: 12px; color: var(--tlink); }
.theme-preview-rival-article { font-size: 12px; color: #54595d; }

.theme-preview-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 3px solid #c8ccd1;
    opacity: 0.75;
}

.theme-preview-item-name { font-weight: 900; font-size: 13px; color: var(--tfg); }

.theme-preview-feed { display: flex; flex-direction: column; gap: 5px; }

.theme-preview-feed-entry {
    font-size: 12px;
    font-weight: bold;
    color: var(--tfg);
    padding: 4px 8px;
    border: 1px solid #c8ccd1;
}

.cos-name {
    max-width: 100%;
    overflow: hidden;
    font-size: 1cqw;
    line-height: 1.1;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: #e2e2e2;
    text-shadow: .1cqw .1cqw 0 #000;
    text-align: center;
}

/* The infobar links' parallelogram, the same as the wallet. */
.cos-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .35cqw;
    min-width: 70%;
    padding: .45cqw 1.2cqw;
    border: 0;
    border-radius: 0;
    clip-path: polygon(.5cqw 0, 100% 0, calc(100% - .5cqw) 100%, 0 100%);
    font-size: .85cqw;
    line-height: 1;
    letter-spacing: .06em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease;
}

.cos-btn svg { width: .9cqw; height: .9cqw; }

.cos-btn s { opacity: 0.6; font-weight: bold; }

.cos-btn--buy {
    background: #3ddc5a;
    color: #0d2a12;
}

.cos-btn--buy:hover { background: #7ff08f; }

.cos-btn--equip {
    background: #303030;
    color: #d6d6d6;
}

.cos-btn--equip:hover { background: #3d3d3d; color: #fff; }

.cos-btn--equipped {
    background: var(--bright, #e2e2e2);
    color: var(--deep, #1c1c1c);
}

.cos-btn.is-shake {
    animation: cosShake 0.4s;
}

@keyframes cosShake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-5px); }
    50% { transform: translateX(5px); }
    75% { transform: translateX(-3px); }
}

/* Less movement asked for: the shop is just there. */
@media (prefers-reduced-motion: reduce) {
    .shopx--intro *,
    .shopx--bundles-in *,
    .shop-container:is(.is-exiting, .is-exiting-back) .shopx * {
        animation: none !important;
    }
}

/* ── Loadouts ── */

.shopx-loadouts {
    padding: 12px 16px;
    margin-bottom: 20px;
    border: 3px solid #000;
    border-radius: 12px;
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.22);
}

.loadout-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
}

.loadout-slot {
    position: relative;
}

.loadout-apply {
    min-width: 74px;
    padding: 9px 12px;
    border: 2px solid #000;
    border-radius: 9px;
    background: var(--surface);
    font-size: 17px;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.22);
}

.loadout-apply:hover { background: #e3f2fd; }

.loadout-del {
    position: absolute;
    top: -8px;
    right: -8px;
    width: 20px;
    height: 20px;
    border: 2px solid #000;
    border-radius: 50%;
    background: #e53935;
    color: #fff;
    font-size: 10px;
    font-weight: 900;
    line-height: 1;
    cursor: pointer;
}

.loadout-save {
    padding: 10px 16px;
    border: 2px dashed #5b6474;
    border-radius: 9px;
    background: transparent;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: 900;
    color: var(--ink-muted);
    cursor: pointer;
}

.loadout-save:hover { border-color: #0032ff; color: var(--ink-link); }

/* ── W-Bucks toast ── */

.wbucks-toast {
    position: fixed;
    right: 22px;
    bottom: 74px;
    z-index: 9500;
    padding: 10px 18px;
    border: 3px solid #000;
    border-radius: 10px;
    background: linear-gradient(180deg, #ffe082, #f6d83f);
    color: #4a3200;
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: 900;
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.3);
    transform: translateY(16px);
    opacity: 0;
    transition: transform 0.3s cubic-bezier(0.2, 1.4, 0.4, 1), opacity 0.25s;
    pointer-events: none;
}

.wbucks-toast.is-visible {
    transform: translateY(0);
    opacity: 1;
}

.profile-menu-bucks { white-space: nowrap; }

/* ── Live cosmetic effects ── */

.cos-trail {
    position: fixed;
    z-index: 9350;
    font-size: 15px;
    line-height: 1;
    pointer-events: none;
    animation: cosTrailFade 0.75s ease-out forwards;
}

.cos-trail--dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
}

@keyframes cosTrailFade {
    to {
        transform: translate(var(--dx, 0), var(--dy, 22px)) scale(0.3) rotate(35deg);
        opacity: 0;
    }
}

/* Pixel flag trails are rendered by trail-ribbon.js on a canvas overlay. */

.cos-click-p {
    position: fixed;
    z-index: 9350;
    font-size: 15px;
    line-height: 1;
    pointer-events: none;
    animation: cosBurst 0.6s ease-out forwards;
}

@keyframes cosBurst {
    to {
        transform: translate(var(--dx), var(--dy)) scale(0.35);
        opacity: 0;
    }
}

.cos-ring {
    position: fixed;
    z-index: 9350;
    width: 14px;
    height: 14px;
    margin: -7px 0 0 -7px;
    border: 3px solid #fff;
    border-radius: 50%;
    box-shadow: 0 0 8px 2px rgba(120, 180, 255, 0.9), inset 0 0 6px rgba(120, 180, 255, 0.9);
    pointer-events: none;
    animation: cosRing 0.55s ease-out forwards;
}

@keyframes cosRing {
    to {
        transform: scale(6);
        opacity: 0;
    }
}

.cos-aura {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9340;
    width: 110px;
    height: 110px;
    border-radius: 50%;
    pointer-events: none;
    mix-blend-mode: screen;
}

/* Custom pixel auras: square art, no blending or circular clipping. */
.cos-aura--pixel {
    border-radius: 0;
    mix-blend-mode: normal;
    image-rendering: pixelated;
}

/* ── The Negative's lens ──────────────────────────────────────────────────
   Stands in for the real pointer (hidden via `cursor: none` while this is
   equipped) rather than drawing over it. Two elements, masked to the same
   sprite, moved together (public/script.js startInvertLens / engineTick):
   the outline is every other cursor's flat black border, always visible so
   the pointer still reads as a pointer over anything; the fill is
   `mix-blend-mode: difference` against a plain white fill, which is the
   whole trick — the browser subtracts that shape from whatever is under it
   on every composited frame, no JS repaint required. Above every other
   cosmetic layer AND the normal UI stack (profile card, notifications,
   friends panel — highest of those is 10001) since together these two
   elements ARE the pointer: with the real cursor hidden, anything drawn
   above the lens leaves no visible pointer at all under it, not just a
   cosmetic layering glitch. */
.cos-lens-outline,
.cos-lens {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10002;
    pointer-events: none;
    image-rendering: pixelated;
}

.cos-lens-outline {
    background: #000;
}

.cos-lens {
    background: #fff;
    mix-blend-mode: difference;
}

/* An equipped accessory rides on top of the lens as a plain image — it isn't
   part of the invert trick, just art sitting over the pointer, so it gets no
   mask and no blend mode, only the same fixed positioning and top z-index. */
.cos-lens-accessory {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10003;
    pointer-events: none;
    image-rendering: pixelated;
}

body.fx-hide-cursor .cos-lens-outline,
body.fx-hide-cursor .cos-lens,
body.fx-hide-cursor .cos-lens-accessory {
    display: none;
}

/* The Chameleon's glass (startGlassChameleon, script.js): the same
   fixed-element-standing-in-for-the-pointer idea as .cos-lens above, but
   clipped to the sprite's shape (clip-path, not mask-image — see
   startGlassChameleon for why) and genuinely blurring whatever is on screen
   behind it instead of blending against it. */
.cos-glass-outline,
.cos-glass-fill {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10002;
    pointer-events: none;
    image-rendering: pixelated;
}

.cos-glass-outline {
    background: #000;
}

.cos-glass-fill {
    backdrop-filter: blur(80px) saturate(130%);
    -webkit-backdrop-filter: blur(80px) saturate(130%);
}

body.fx-hide-cursor .cos-glass-outline,
body.fx-hide-cursor .cos-glass-fill {
    display: none;
}

/* Custom click animations: one spritesheet loop at the click point. */
.cos-sheet-fx {
    position: fixed;
    z-index: 9350;
    pointer-events: none;
    image-rendering: pixelated;
}

.cos-keyring {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9345;
    font-size: 21px;
    line-height: 1;
    pointer-events: none;
    transform-origin: top center;
    text-shadow: 0 2px 3px rgba(0, 0, 0, 0.35);
}

/* ── Card themes ── */

body[data-ctheme="sunset"]    .blue-card { filter: hue-rotate(150deg) saturate(1.15); }
body[data-ctheme="forest"]    .blue-card { filter: hue-rotate(-100deg); }
body[data-ctheme="bubblegum"] .blue-card { filter: hue-rotate(100deg) saturate(1.1); }
body[data-ctheme="crimson"]   .blue-card { filter: hue-rotate(140deg) saturate(1.3); }
body[data-ctheme="nebula"]    .blue-card { filter: hue-rotate(60deg); }
body[data-ctheme="midnight"]  .blue-card { filter: saturate(0.5) brightness(0.65); }
body[data-ctheme="goldrush"]  .blue-card { filter: hue-rotate(185deg) saturate(1.5) brightness(1.08); }

/* ═══ WikiChannel ═══════════════════════════════════════════════════════════ */

.wch {
    width: min(100%, 1080px);
    margin: 0 auto;
    padding-bottom: 50px;
    font-family: var(--game-font);
}

.wch-topbar {
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
    padding: 12px 18px;
    margin-bottom: 16px;
    border: 3px solid #000;
    border-radius: 14px;
    background: linear-gradient(160deg, #10142e, #232b5c);
    box-shadow: 0 6px 0 rgba(0, 0, 0, 0.3);
}

.wch-tabs {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.wch-tab {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    padding: 7px 14px;
    border: 2px solid rgba(255, 255, 255, 0.35);
    border-radius: 9px;
    background: rgba(255, 255, 255, 0.08);
    color: #cfd4ff;
    font-family: var(--game-font);
    cursor: pointer;
    transition: background 0.15s, transform 0.1s;
}

.wch-tab:hover { background: rgba(255, 255, 255, 0.18); transform: translateY(-1px); }

.wch-tab.is-active {
    background: var(--surface);
    border-color: #000;
    color: var(--ink-link);
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.35);
}

.wch-tab-num {
    font-size: 10px;
    font-weight: 900;
    letter-spacing: 2px;
}

.wch-tab-label {
    font-size: 13px;
    font-weight: 900;
}

.wch-screen {
    min-height: 300px;
}

.wch-panel {
    padding: 16px 18px 20px;
    border: 3px solid #000;
    border-radius: 14px;
    background: color-mix(in srgb, var(--surface) 95%, transparent);
    box-shadow: 0 6px 0 rgba(0, 0, 0, 0.25);
}

.wch-panel-title {
    margin-bottom: 14px;
    font-size: 20px;
    font-weight: 900;
    color: var(--ink-navy);
}

/* ── Leaderboard ── */

.wch-board {
    display: flex;
    flex-direction: column;
    gap: 7px;
}

.wch-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 9px 14px;
    border: 2px solid #c8ccd1;
    border-radius: 9px;
    background: var(--surface);
}

.wch-row:nth-child(1) { border-color: #ffb300; background: linear-gradient(180deg, #fffdf2, #fff3bd); }
.wch-row:nth-child(2) { border-color: #9fb0c3; background: linear-gradient(180deg, #fafbfc, #eef2f6); }
.wch-row:nth-child(3) { border-color: #c98a53; background: linear-gradient(180deg, #fff8f2, #ffe8d4); }

.wch-row.is-me {
    outline: 3px solid #0032ff;
    outline-offset: 1px;
}

.wch-rank {
    flex: 0 0 44px;
    font-size: 18px;
    font-weight: 900;
    color: var(--ink-mid);
    text-align: center;
}

/* ── Player pictures on the channels ──
   One slot, four places: the board rows, the replay list, the racer strip
   inside a replay, and your own head on CH 02. It always occupies the same box
   whether or not there is a picture in it, so a board does not reflow when the
   faces arrive a moment after the names — and a channel where nobody has one
   still looks laid out rather than broken.

   The empty state is a plain tinted circle — the `user` glyph is asked for and
   the icon set has no art for it yet, so nothing draws and that is the right
   outcome either way. Not initials: a name here can be any script and any
   length, and a letter in a circle would be the one piece of the page that
   renders differently per player for no reason. */
.wch-pfp {
    flex: 0 0 28px;
    width: 28px;
    height: 28px;
    display: grid;
    place-items: center;
    overflow: hidden;
    border: 2px solid #c8ccd1;
    border-radius: 50%;
    background: var(--surface-2, #e9edf2) center / cover no-repeat;
    color: var(--ink-mid);
    font-size: 14px;
    line-height: 1;
}

/* Filled: the border tightens to the picture and the fallback glyph, which is
   still in the DOM, must not show through a photo with transparency. */
.wch-pfp.has-pic {
    border-color: #0032ff;
    color: transparent;
}

.wch-pfp svg,
.wch-pfp img { width: 16px; height: 16px; }

.wch-pfp.has-pic svg,
.wch-pfp.has-pic img { display: none; }

/* Bigger where there is room for it: your own card, which shows one face
   rather than a column of them. */
.wch-me-head .wch-pfp {
    flex: 0 0 56px;
    width: 56px;
    height: 56px;
    border-color: #ffb300;
    font-size: 26px;
}

.wch-me-head .wch-pfp svg,
.wch-me-head .wch-pfp img { width: 30px; height: 30px; }

.wch-player {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.wch-name {
    font-size: 15px;
    font-weight: 900;
    color: var(--ink-navy);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wch-tier {
    font-size: 11px;
    font-weight: bold;
    color: #8a6d00;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.wch-rating {
    font-size: 17px;
    font-weight: 900;
    color: var(--ink-link);
}

.wch-wl {
    flex: 0 0 74px;
    font-size: 12px;
    font-weight: bold;
    color: var(--ink-muted);
    text-align: right;
}

/* ── Records ── */

.wch-records {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: 12px;
}

.wch-record {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 18px 14px;
    border: 3px solid #000;
    border-radius: 12px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.2);
    text-align: center;
}

.wch-record-emoji { font-size: 32px; }

.wch-record-label {
    font-size: 12px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.wch-record-value {
    font-size: 27px;
    font-weight: 900;
    color: var(--ink-link);
}

.wch-record-holder {
    font-size: 12px;
    font-weight: bold;
    color: var(--ink-mid);
}

/* ── Channel sub-tabs, panel subtitles ── */

.wch-panel-sub {
    margin: -10px 0 14px;
    font-size: 12px;
    font-weight: bold;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.wch-section-title {
    margin: 20px 0 10px;
    font-size: 15px;
    font-weight: 900;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-mid);
}

.wch-subtabs {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.wch-subtab {
    padding: 6px 14px;
    border: 2px solid #c8ccd1;
    border-radius: 999px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: 900;
    color: var(--ink-mid);
    cursor: pointer;
    transition: background 0.15s, transform 0.1s;
}

.wch-subtab:hover { background: #e3f2fd; transform: translateY(-1px); }

.wch-subtab.is-active {
    border-color: #000;
    background: #0032ff;
    color: #fff;
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.3);
}

.wch-records--strip { margin-bottom: 16px; }

.wch-record-note {
    margin: -7px 0 16px;
    font-size: 12px;
    font-weight: 900;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--ink-faint);
}

/* ── Me: profile header, XP bar, recent games ── */

.wch-me-head {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    padding: 14px 16px;
    margin-bottom: 4px;
    border: 3px solid #000;
    border-radius: 12px;
    background: linear-gradient(160deg, #10142e, #232b5c);
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.25);
}

.wch-me-level {
    padding: 8px 14px;
    border: 2px solid #ffb300;
    border-radius: 10px;
    background: rgba(255, 179, 0, 0.15);
    font-size: 20px;
    font-weight: 900;
    color: #ffd257;
    white-space: nowrap;
}

.wch-me-xp {
    flex: 1 1 220px;
    min-width: 180px;
}

.wch-me-xp-bar {
    height: 14px;
    border: 2px solid rgba(255, 255, 255, 0.4);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.35);
    overflow: hidden;
}

.wch-me-xp-fill {
    height: 100%;
    background: linear-gradient(90deg, #f6d83f, #ffb300);
    transition: width 0.3s;
}

.wch-me-xp-label {
    margin-top: 4px;
    font-size: 11px;
    font-weight: bold;
    letter-spacing: 1px;
    color: #cfd4ff;
}

.wch-me-league {
    flex: 0 1 auto;
    font-size: 13px;
    font-weight: 900;
    color: #fff;
}

.wch-recent {
    display: flex;
    flex-direction: column;
    gap: 7px;
}

.wch-recent-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 9px 14px;
    border: 2px solid #c8ccd1;
    border-radius: 9px;
    background: var(--surface);
}

.wch-recent-mode { font-size: 22px; }

.wch-recent-info {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.wch-recent-course {
    font-size: 14px;
    font-weight: 900;
    color: var(--ink-navy);
    overflow-wrap: anywhere;
}

.wch-recent-meta {
    font-size: 11px;
    font-weight: bold;
    color: var(--ink-muted);
}

.wch-recent-result {
    flex: 0 0 auto;
    padding: 3px 10px;
    border-radius: 999px;
    border: 2px solid #000;
    font-size: 11px;
    font-weight: 900;
    letter-spacing: 1px;
}

.wch-recent-result.is-win  { background: #d4f7d9; color: #0c7a24; border-color: #0c7a24; }
.wch-recent-result.is-loss { background: #ffe1e1; color: #b3261e; border-color: #b3261e; }
.wch-recent-result.is-draw { background: #eef0f4; color: #5b6474; border-color: #5b6474; }

.wch-recent-ago {
    flex: 0 0 64px;
    font-size: 11px;
    font-weight: bold;
    color: var(--ink-faint);
    text-align: right;
}

/* ── Achievements grid (reuses the badge cards) ── */

.wch-awards-grid {
    padding: 0;
    overflow-y: visible;
}

@media (max-width: 768px) {
    .wch-topbar { justify-content: center; }
    .wch-wl { display: none; }
    .wch-recent-ago { display: none; }
}

/* ===========================
   FAVOURITE COLOR PICKER
   (join gate + Settings → Customization)
   =========================== */

.fav-hex-row {
    display: flex;
    align-items: center;
    gap: 12px;
}

.fav-hex {
    appearance: none;
    width: 52px;
    height: 44px;
    padding: 0;
    border: 3px solid rgba(0, 0, 0, 0.55);
    border-radius: 10px;
    background: none;
    cursor: pointer;
}

.fav-hex-name {
    font-size: 16px;
    font-weight: 900;
    color: var(--ink-navy);
    min-width: 90px;
    text-align: left;
}

.fav-hex-row--join {
    justify-content: center;
    margin: 14px 0 12px;
}

/* Signup pure-colour swatches */
.join-swatches {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
    margin: 16px auto 12px;
    /* Fills the card's inner width (440 card - 72 padding - 10 border): big,
       easy targets rather than fiddly little dots. */
    max-width: 342px;
    width: 100%;
}

.join-swatch {
    appearance: none;
    aspect-ratio: 1;
    border: 3px solid rgba(0, 0, 0, 0.35);
    border-radius: 4px;
    background: var(--sw, #ccc);
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.join-swatch:hover {
    transform: translateY(-2px) scale(1.05);
}

.join-swatch.is-picked {
    border-color: #17233b;
    box-shadow: 0 0 0 3px #fff, 0 0 0 6px #17233b;
    transform: scale(1.06);
}

/* ===========================
   JOIN SCREEN (login gate)
   =========================== */

.join-overlay {
    position: fixed;
    inset: 0;
    z-index: 100000;
    display: grid;
    place-items: center;
    animation: mpFadeIn 0.25s ease-out;
}

.join-overlay.is-leaving {
    opacity: 0;
    transition: opacity 0.3s ease-out;
    pointer-events: none;
}

.join-card {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: min(92vw, 440px);
    padding: 36px 36px 24px;
    border: 5px solid #000;
    border-radius: 16px;
    background: var(--surface-2);
    box-shadow: 0 12px 0 rgba(0, 0, 0, 0.35);
    overflow: hidden;
    text-align: center;
    font-family: var(--game-font);
}

.join-checkerboard {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 14px;
    background: repeating-conic-gradient(#111 0% 25%, #fff 0% 50%) 0 0 / 14px 14px;
}

.join-logo {
    margin: 6px 0 4px;
    font-size: 42px;
    font-weight: 900;
    font-style: italic;
    color: var(--ink-link);
    letter-spacing: 1px;
    text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15);
}

.join-tagline {
    margin-bottom: 22px;
    font-size: 14px;
    font-weight: bold;
    color: var(--ink-mid);
}

.join-label {
    align-self: flex-start;
    margin: 0 0 6px 2px;
    font-size: 12px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.join-input {
    width: 100%;
    padding: 12px 14px;
    border: 3px solid #000;
    border-radius: 8px;
    background: var(--surface);
    color: var(--ink-link);
    font-family: var(--game-font);
    font-size: 32px;
    font-weight: bold;
    text-align: center;
    outline: none;
}

.join-input:focus {
    border-color: #0032ff;
}

.join-btn {
    appearance: none;
    width: 100%;
    margin-top: 16px;
    padding: 13px;
    border: 3px solid #000;
    border-radius: 8px;
    background: linear-gradient(180deg, #2bd158, #12a53c);
    color: #fff;
    font-family: var(--game-font);
    font-size: 32px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.25);
}

.join-btn:hover {
    filter: brightness(1.07);
}

.join-btn:active {
    transform: translateY(3px);
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.25);
}

.join-legal {
    margin-top: 16px;
    font-size: 11px;
    color: #778;
}

.join-link {
    text-decoration: underline;
    cursor: pointer;
    color: #556;
}

.join-link:hover {
    color: var(--ink-link);
}

/* ── Join screen: multi-step login ── */

.join-logo--sm {
    font-size: 30px;
    margin: 4px 0 2px;
}

.join-welcome {
    margin: 2px 0 6px;
    font-size: 26px;
    font-weight: 900;
    font-style: italic;
    letter-spacing: 1px;
    color: #12a53c;
    text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.12);
}

.join-welcome--ask {
    color: var(--ink-link);
    /* The question is long enough to wrap on a 440px card and much longer on a
       phone. Balancing splits it evenly instead of leaving "color?" alone on a
       second line; browsers without support just wrap as before. */
    text-wrap: balance;
}

.join-tagline--sm {
    margin-bottom: 16px;
    font-weight: normal;
}

.join-tagline--sm strong {
    color: var(--ink-link);
}

.join-label--mt {
    margin-top: 14px;
}

.join-error {
    align-self: stretch;
    margin: 12px 0 0;
    padding: 8px 10px;
    border: 2px solid #b71c1c;
    border-radius: 7px;
    background: #ffe5e5;
    color: #b71c1c;
    font-size: 13px;
    font-weight: bold;
}

/* The age/terms line under the register button: has to be legible and
   unmissable, without competing with the button it sits below. */
.join-legal {
    align-self: stretch;
    margin: 14px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: #4a5468;
    text-align: center;
}

.join-legal a {
    color: var(--ink-link);
    font-weight: bold;
}

.join-claim-text {
    margin: 0 0 10px;
    font-size: 14px;
    line-height: 1.45;
    color: #334;
}

.join-claim-text strong {
    color: var(--ink-link);
}

.join-claim-note {
    margin: 0 0 8px;
    padding: 9px 11px;
    border: 2px dashed #b0863b;
    border-radius: 7px;
    background: #fff6e0;
    font-size: 12px;
    line-height: 1.4;
    color: #7a5a1e;
}

.join-btn-stack {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    margin-top: 12px;
}

.join-btn-stack .join-btn {
    margin-top: 0;
}

.join-btn--neutral {
    background: linear-gradient(180deg, #4a90e2, #2e6fc7);
}

.join-btn--ghost {
    background: #e9ecf5;
    color: #334;
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.18);
}

.join-btn--ghost:hover {
    filter: none;
    background: #dfe3ef;
}

.join-btn:disabled,
.join-btn.is-busy {
    opacity: 0.6;
    cursor: default;
    filter: none;
    transform: none;
}

.settings-guest-tag,
.profile-menu-guest {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 5px;
    background: #b0863b;
    color: #fff;
    font-size: 10px;
    font-weight: 900;
    letter-spacing: 1px;
    vertical-align: middle;
}

.settings-register {
    background: #12a53c !important;
    color: #fff;
}

.profile-menu-user {
    padding: 9px 13px;
    background: #0032ff;
    color: #fff;
    font-size: 14px;
    font-weight: 900;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ===========================
   SETTINGS 2.0 (tabbed)
   =========================== */

.settings-screen {
    display: flex;
    align-items: flex-start;
    gap: 22px;
}

.settings-tabs {
    display: flex;
    flex-direction: column;
    gap: 9px;
    flex-shrink: 0;
    width: 250px;
    position: sticky;
    top: 20px;
}

.settings-tab {
    appearance: none;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    border: 3px solid #000;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    color: var(--ink-navy);
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    text-align: left;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.22);
}

.settings-tab:hover {
    background: #e3f2fd;
}

.settings-tab.is-active {
    background: #0032ff;
    color: #fff;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}

.settings-tab-icon {
    font-size: 18px;
}

.settings-list {
    flex: 1;
    min-width: 0;
}

.settings-section-title {
    margin-bottom: 4px;
    padding: 0 4px;
    font-family: var(--game-font);
    font-size: 41.6px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: #fff;
    text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55);
}

/* ── Sliders ── */

.settings-slider {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
}

.settings-slider-end {
    font-family: var(--game-font);
    font-size: 17.6px;
    font-weight: 900;
    letter-spacing: 1px;
    color: var(--ink-muted);
}

.settings-slider input[type="range"] {
    width: 170px;
    accent-color: #0032ff;
    cursor: pointer;
}

.settings-slider-val {
    min-width: 52px;
    padding: 5px 8px;
    border: 2px solid #000;
    border-radius: 5px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: bold;
    color: var(--ink-link);
    text-align: center;
}

/* ── Text input ── */

.settings-textwrap {
    flex: 1;
    max-width: 560px;
}

.settings-text {
    width: 100%;
    padding: 10px 14px;
    border: 3px solid #000;
    border-radius: 7px;
    background: var(--surface);
    color: var(--ink-link);
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: bold;
    outline: none;
}

.settings-text:focus {
    border-color: #0032ff;
}

/* ── Select ──
   Borrows the text input's shell so the two read as the same control family.
   appearance:none drops the platform chrome, which otherwise ignores the
   border radius and the bold face; the caret is drawn back on as a background
   so it still looks like something you can open. */

.settings-selectwrap {
    flex: 1;
    max-width: 560px;
}

.settings-select {
    width: 100%;
    padding: 10px 38px 10px 14px;
    border: 3px solid #000;
    border-radius: 7px;
    background: var(--surface);
    color: var(--ink-link);
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: bold;
    outline: none;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, #0032ff 50%),
                      linear-gradient(135deg, #0032ff 50%, transparent 50%);
    background-position: calc(100% - 20px) calc(50% + 2px),
                         calc(100% - 14px) calc(50% + 2px);
    background-size: 6px 6px, 6px 6px;
    background-repeat: no-repeat;
}

.settings-select:focus {
    border-color: #0032ff;
}

.settings-select:disabled {
    cursor: not-allowed;
    opacity: .55;
}

/* ── Controls (kbd) & account ── */

.settings-kbd {
    flex-shrink: 0;
    padding: 8px 16px;
    border: 3px solid #000;
    border-radius: 6px;
    background: linear-gradient(180deg, #ffe082, #f6d83f);
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: bold;
    color: var(--ink-navy);
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.25);
    white-space: nowrap;
}

.settings-account {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-shrink: 0;
    /* Only the rename editor ever wraps: it adds an error line under the row. */
    flex-wrap: wrap;
}

.settings-account-name {
    padding: 9px 16px;
    border: 3px solid #000;
    border-radius: 7px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: bold;
    color: var(--ink-link);
}

/* ── Messages from the admin ──
   Bottom-right, stacked upward, above the footer band AND above the Discord
   button that already lives in this corner: 108px + its 64px + air. Both the
   footer height and the Discord offset are tracked by the breakpoints at the
   bottom of this file, so this follows them there too.

   Column-reverse so a new card appears nearest the corner and pushes older
   ones up, which is the direction attention is already pointing. */

.notif-layer {
    position: fixed;
    right: 44px;
    bottom: 188px;
    z-index: 9998;
    display: flex;
    flex-direction: column-reverse;
    gap: 10px;
    /* The layer spans the corner; only the cards inside it take clicks, or an
       invisible column would swallow them for the whole height of the screen. */
    pointer-events: none;
}

.notif-card {
    position: relative;
    pointer-events: auto;
    width: 300px;
    max-width: calc(100vw - 88px);
    padding: 14px 38px 14px 16px;
    border: 3px solid #000;
    border-radius: 9px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.25);
    /* Slides in from off-frame to the right. Paired with .is-in rather than an
       animation so a card that is removed mid-flight cannot leave the layer
       holding a finished animation's end state. */
    transform: translateX(24px);
    opacity: 0;
    transition: transform 200ms ease, opacity 200ms ease;
}

.notif-card.is-in {
    transform: translateX(0);
    opacity: 1;
}

.notif-card.is-leaving {
    transform: translateX(24px);
    opacity: 0;
}

.notif-title {
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: 900;
    color: var(--ink-navy);
    margin-bottom: 4px;
    text-transform: none;
}

.notif-body {
    font-family: var(--chat-font);
    font-size: 22.4px;
    font-weight: bold;
    line-height: 1.35;
    color: var(--ink-mid);
    text-transform: none;
    /* An admin can type 400 characters and a long word should not widen the
       card past the corner it lives in. */
    overflow-wrap: anywhere;
}

.notif-close {
    position: absolute;
    top: 8px;
    right: 10px;
    appearance: none;
    border: 0;
    background: transparent;
    color: var(--ink-muted);
    font-family: var(--game-font);
    font-size: 33.6px;
    line-height: 1;
    font-weight: bold;
    cursor: pointer;
    padding: 0 2px;
}

.notif-close:hover { color: var(--ink-strong); }

/* ── Recovery email row ──
   An address is far longer than a username, so the badge is capped and clipped
   rather than allowed to push the button off the row. The full value is on the
   element's title. */

.settings-recovery {
    max-width: 260px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 15px;
}

/* "None" is a state to fix, not a value: greyed and italic so it does not read
   as an address someone already set. */
.settings-recovery--none {
    color: var(--ink-faint);
    font-style: italic;
}

/* ── Renaming ──
   The input replaces the name badge inside .settings-account, so it has to sit
   in the same flex row without stretching it: a fixed width rather than the
   100% .settings-text carries, which is written for a full-width row. */

.settings-rename {
    background: linear-gradient(180deg, #4a9eff, #1565c0) !important;
}

.settings-rename-input {
    width: 200px;
    padding: 9px 14px;
    font-size: 17px;
}

/* Full-basis so it wraps onto a line of its own instead of squeezing the input
   and the row's label into columns a few characters wide. */
.settings-rename-error {
    flex-basis: 100%;
    padding: 6px 10px;
    border: 2px solid #b71c1c;
    border-radius: 6px;
    background: #ffe5e5;
    color: #b71c1c;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
}

.settings-rename-error[hidden] {
    display: none;
}

.settings-avatar {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-shrink: 0;
}

.settings-avatar-preview {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 3px solid #000;
    object-fit: cover;
    background: var(--surface);
    flex-shrink: 0;
}

.settings-avatar-preview--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    background: var(--surface-3);
}

/* ── Favorite article row ──
   The input reuses .settings-rename-input's sizing (it is the same kind of
   control: one line, sitting beside a button in a .settings-account row) but
   needs its own left-aligned text, since the rename input inherits nothing
   about alignment from that class either. */
.settings-fav-msg {
    font-size: 13px;
    font-weight: bold;
    color: #1c7c31;
}

.settings-fav-msg.is-bad { color: #b00020; }

.settings-logout {
    appearance: none;
    padding: 9px 18px;
    border: 3px solid #000;
    border-radius: 6px;
    background: linear-gradient(180deg, #ff6b6b, #d63030);
    color: #fff;
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.25);
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}

.settings-logout:active {
    transform: translateY(2px);
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.25);
}

.settings-mono {
    font-family: Consolas, monospace;
    font-size: 15px;
    font-weight: bold;
    color: var(--ink-mid);
    padding: 8px 14px;
    border: 2px dashed #999;
    border-radius: 6px;
    background: color-mix(in srgb, var(--surface) 80%, transparent);
}

.settings-static {
    font-size: 22px;
}

@media (max-width: 900px) {
    .settings-screen {
        flex-direction: column;
    }

    .settings-tabs {
        position: static;
        width: 100%;
        flex-direction: row;
        flex-wrap: wrap;
    }

    .settings-tab {
        flex: 1 1 auto;
    }
}

/* ===========================
   UI THEMES
   =========================== */

/* Midnight.
   This used to be `filter: invert(1) hue-rotate(180deg)` on main, with images
   inverted a second time to cancel it out. That is why the shop looked wrong:
   an inverter has no idea what it is looking at, so every swatch, every piece
   of cosmetic art and every status colour came out as its opposite — a green
   ON pill read red.

   It repaints the palette instead, which is the mirror of what the theme
   colour already does. tintHex() mixes a chosen colour toward white and never
   reaches it, so a pitch-black pick lands on grey; these mix toward black on
   the same ramp and never reach it, so the lightest surface in the app lands
   on grey rather than turning into a hole. Nothing is inverted, so a green
   pill stays green and the shop shows the art the player actually bought. */
html.theme-midnight {
    --surface: #4d5163;
    /* The steps carry on downward, mirroring how classic steps up from white.
       The floor is still nowhere near black — a panel is meant to read as a
       panel, not as a hole cut in the page. */
    --surface-2: #434857;
    --surface-3: #363a49;
    --ink: #eceef7;
    /* Even the strongest ink stops short of pure white — the ceiling applies
       to text as much as to panels. */
    --ink-strong: #f5f7ff;
    /* The text scale runs the other way here, but keeps its order: what was
       darkest stays the most prominent. */
    --ink-navy: #dfe3f2;
    --ink-mid: #c6ccdd;
    --ink-muted: #a9b0c4;
    --ink-faint: #8f96aa;

    /* Mirrors the light gradient's move: a step down and a shade bluer from top
       to bottom. Both ends sit well above the #0c0e1a page so a panel still
       reads as a panel rather than as a hole. */
    --panel-top: rgba(77, 81, 99, 0.97);
    --panel-bottom: rgba(58, 62, 79, 0.97);

    /* Same hue, lifted until it reads on the panels above. */
    --ink-link: #8fb0ff;

    --danger: #ff8787;
    --bad: #ff8787;

    /* Slightly more opaque than classic's 0.72: these cards are translucent
       over the wallpaper, and light text needs more backing behind it than dark
       text does before a bright patch of photo starts eating the letters. */
    --theme-card: rgba(34, 37, 53, 0.78);
    --theme-tint: #2b2f42;
    --theme-tint2: #1f2331;
    --theme-wash: #161927;
    --theme-wash2: #11131f;

    background: #0c0e1a;
}

/* ===========================
   CUSTOM MODE 2.0
   =========================== */

.custom-form--builder {
    max-width: 720px;
    align-items: stretch;
    gap: 20px;
}

.cm-section {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 16px 18px;
    border: 3px solid #000;
    border-radius: 7px;
    background: color-mix(in srgb, var(--surface) 72%, transparent);
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.15);
}

.cm-section-title {
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: 900;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: var(--ink-navy);
}

.cm-input-row {
    display: flex;
    gap: 8px;
}

.cm-input-row .custom-input {
    flex: 1;
}

.cm-dice {
    appearance: none;
    flex-shrink: 0;
    width: 52px;
    border: 3px solid #000;
    border-radius: 6px;
    background: linear-gradient(180deg, #ffe082, #f6d83f);
    font-size: 22px;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.22);
}

.cm-dice:hover { filter: brightness(1.06); }
.cm-dice:disabled { opacity: 0.6; cursor: default; }
.cm-dice.is-rolling { animation: cmDiceSpin 0.5s linear infinite; }

@keyframes cmDiceSpin {
    to { transform: rotate(360deg); }
}

.cm-rule {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    flex-wrap: wrap;
}

.cm-rule-label {
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: bold;
    color: var(--ink-mid);
}

.cm-rule-label small {
    display: block;
    font-size: 11px;
    font-weight: normal;
    color: var(--ink-faint);
}

.cm-chips {
    display: flex;
    border: 3px solid #000;
    border-radius: 6px;
    overflow: hidden;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.2);
}

.cm-chip {
    appearance: none;
    border: 0;
    border-left: 2px solid #000;
    padding: 8px 14px;
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: bold;
    cursor: pointer;
    background: var(--surface-3);
    color: var(--ink-muted);
}

.cm-chip:first-child { border-left: 0; }

.cm-chip.is-active {
    background: #0032ff;
    color: #fff;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}

.cm-presets {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.cm-preset {
    appearance: none;
    flex: 1 1 auto;
    padding: 9px 12px;
    border: 3px solid #000;
    border-radius: 6px;
    background: linear-gradient(180deg, var(--panel-top), var(--panel-bottom));
    font-family: var(--game-font);
    font-size: 22.4px;
    font-weight: bold;
    color: var(--ink-navy);
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.2);
}

.cm-preset:hover { background: #e3f2fd; }
.cm-preset:active { transform: translateY(2px); box-shadow: 0 2px 0 rgba(0, 0, 0, 0.2); }

.cm-hazards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 8px;
}

.cm-hazard {
    appearance: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border: 3px solid #000;
    border-radius: 6px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 20px;
    font-weight: bold;
    color: var(--ink-mid);
    text-align: left;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.18);
}

.cm-hazard:hover { background: #f0f4ff; }

.cm-hazard.is-active {
    background: var(--hz, #0032ff);
    color: #fff;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
}

.cm-hazard-icon { font-size: 18px; }

.cm-hazard-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── In-run HUD ── */

.cm-hud {
    position: fixed;
    top: 86px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 9050;
    display: flex;
    gap: 8px;
    pointer-events: none;
}

.cm-hud-chip {
    /* inline-flex + gap rather than a literal space in the markup, so a chip
       whose ICONS slot is still empty renders with no stray leading gap. */
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    border: 3px solid #000;
    border-radius: 7px;
    background: color-mix(in srgb, var(--surface) 95%, transparent);
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: 900;
    color: var(--ink-navy);
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.25);
    white-space: nowrap;
}

.cm-hud-chip.is-danger {
    background: #d63030;
    color: #fff;
    animation: cmDangerPulse 0.7s ease-in-out infinite;
}

@keyframes cmDangerPulse {
    50% { transform: scale(1.08); }
}

.cm-hud-chip--rainbow {
    background: linear-gradient(90deg,
        #ff3d3d, #ff9f1a, #ffe93d, #3dff5e, #3dc9ff, #a03dff, #ff3dcf, #ff3d3d);
    background-size: 300% 100%;
    color: #fff;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
    animation: mpRainbowSlide 3s linear infinite;
}

/* ── Toast & fail overlay ── */

.cm-toast {
    position: fixed;
    bottom: 130px;
    left: 50%;
    transform: translateX(-50%) translateY(16px);
    z-index: 9350;
    padding: 12px 22px;
    border: 3px solid #000;
    border-radius: 8px;
    background: #17233b;
    color: #fff;
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: bold;
    box-shadow: 0 6px 0 rgba(0, 0, 0, 0.35);
    opacity: 0;
    transition: opacity 0.25s ease-out, transform 0.25s ease-out;
    pointer-events: none;
}

.cm-toast.is-visible {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

.cm-fail-overlay {
    position: fixed;
    inset: 0;
    z-index: 9300;
    display: grid;
    place-items: center;
    background: rgba(10, 14, 34, 0.7);
    animation: mpFadeIn 0.3s ease-out;
}

.cm-fail-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    width: min(92vw, 460px);
    padding: 32px 36px;
    border: 5px solid #000;
    border-radius: 14px;
    background: var(--surface-2);
    box-shadow: 0 12px 0 rgba(0, 0, 0, 0.35);
    text-align: center;
    font-family: var(--game-font);
}

.cm-fail-icon { font-size: 54px; }
.cm-fail-icon:empty { display: none; }

.cm-fail-title {
    font-size: 30px;
    font-weight: 900;
    letter-spacing: 2px;
    color: #d63030;
    text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15);
}

.cm-fail-stats {
    font-size: 15px;
    line-height: 1.5;
    color: var(--ink-mid);
}

.cm-fail-btns {
    display: flex;
    gap: 12px;
    margin-top: 6px;
}

.cm-fail-btn {
    appearance: none;
    padding: 11px 20px;
    border: 3px solid #000;
    border-radius: 7px;
    background: var(--surface-3);
    color: var(--ink-mid);
    font-family: var(--game-font);
    font-size: 25.6px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.25);
}

.cm-fail-btn--retry {
    background: linear-gradient(180deg, #2bd158, #12a53c);
    color: #fff;
    text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}

.cm-fail-btn:active {
    transform: translateY(2px);
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.25);
}

/* ─── Cinematic winner replay (ReplayCinema) ─────────────────────────────────── */

/* Floating chrome (the profile card) sits above 9400 — hide it while the
   cinema is on screen. */
body.rc-open .profile-btn,
body.rc-open .discord-btn { display: none; }

.rc-overlay {
    position: fixed;
    inset: 0;
    z-index: 9400;
    display: flex;
    flex-direction: column;
    background: #000;
    font-family: var(--game-font);
}

.rc-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    border-bottom: 3px solid #000;
    background: linear-gradient(180deg, #262a33, #14161b);
    color: #fff;
}

.rc-film { font-size: 18px; }

.rc-title {
    font-size: 16px;
    font-weight: bold;
    white-space: nowrap;
}

.rc-course {
    min-width: 0;
    color: #9fd0ff;
    font-size: 13px;
    overflow-wrap: anywhere;
}

.rc-step {
    margin-left: auto;
    color: #ffd95e;
    font-size: 13px;
    font-weight: bold;
    white-space: nowrap;
}

.rc-btn {
    appearance: none;
    padding: 6px 12px;
    border: 2px solid #000;
    border-radius: 8px;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.35);
}

.rc-btn:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}

.rc-btn--big {
    padding: 11px 20px;
    font-size: 16px;
}

.rc-stage {
    position: relative;
    flex: 1;
    overflow: hidden;
}

/* The article layer: never scrolls or reacts — the run pans it via transform. */
.rc-scroll {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    background: var(--wiki-bg);
    transition: opacity 0.3s;
}

.rc-scroll.is-warping { opacity: 0.12; }

.rc-pan {
    min-height: 100%;
    will-change: transform;
}

/* The winner's cursor, locked at the centre of the stage. */
.rc-cursor {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 5;
    pointer-events: none;
    
    transition: transform 0.12s;
}

.rc-cursor.is-press { transform: scale(0.8); }

.rc-cursor img { display: block; }

.rc-cursor img { image-rendering: pixelated; }

.rc-cursor-emoji {
    display: block;
    font-size: 30px;
    line-height: 1;
}

.rc-toast {
    position: absolute;
    left: 50%;
    top: 22%;
    z-index: 6;
    transform: translateX(-50%);
    padding: 10px 18px;
    border: 2px solid #000;
    border-radius: 10px;
    background: var(--surface);
    color: var(--ink);
    font-size: 15px;
    font-weight: bold;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.3);
}

.rc-toast[hidden] { display: none; }

.rc-finish {
    position: absolute;
    inset: 0;
    z-index: 7;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(10, 12, 18, 0.72);
}

.rc-finish[hidden] { display: none; }

.rc-finish-card {
    max-width: min(520px, 88vw);
    padding: 26px 38px;
    border: 3px solid #000;
    border-radius: 14px;
    background: var(--surface);
    color: var(--ink);
    text-align: center;
    box-shadow: 0 8px 0 rgba(0, 0, 0, 0.35);
}

.rc-finish-flag { font-size: 44px; }

.rc-finish-title {
    margin: 6px 0 2px;
    font-size: 20px;
    font-weight: bold;
}

.rc-finish-sub {
    margin-bottom: 16px;
    color: var(--ink-muted);
    font-size: 14px;
}

.rc-finish-btns {
    display: flex;
    gap: 10px;
    justify-content: center;
}

/* Victory-banner replay button (training / custom / challenges). */
.training-replay-btn {
    appearance: none;
    justify-self: start;
    margin-top: 4px;
    padding: 8px 16px;
    border: 3px solid #000;
    border-radius: 7px;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--game-font);
    font-size: 24px;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.22);
}

.training-replay-btn:active {
    transform: translateY(2px);
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.22);
}

/* ═══ POTATO graphics ════════════════════════════════════════════════════════
   The lowest "Graphics" level. Strips every expensive effect — blur, shadows,
   gradients, rounded corners, animations, the animated card previews and the
   photo wallpaper — down to a flat, ultra-primitive UI for the weakest laptops.
   Deliberately plain. Toggled by applyGraphics() adding body.gfx-potato. */
body.gfx-potato *,
body.gfx-potato *::before,
body.gfx-potato *::after {
    animation: none !important;
    transition: none !important;
    box-shadow: none !important;
    text-shadow: none !important;
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    filter: none !important;
    border-radius: 0 !important;
    mask-image: none !important;
    -webkit-mask-image: none !important;
    -webkit-mask: none !important;
    mask: none !important;
}

body.gfx-potato,
body.gfx-potato .menu-stage { background: #6b6b8a !important; }

/* Menu cards → flat grey tiles. (The card art they used to carry was a
   copyrighted GIF, removed 2026-08-01.) */
body.gfx-potato .blur { background: #dcdcdc !important; border: 2px solid #000 !important; }
body.gfx-potato .blue-card,
body.gfx-potato .goldframe,
body.gfx-potato .goldframe2,
body.gfx-potato .goldframe3 { background: #c4c4c4 !important; border: 1px solid #000 !important; }

/* Profile → a flat square badge */
body.gfx-potato .discord-card { background: #dcdcdc !important; border: 2px solid #000 !important; }
body.gfx-potato .discord-card { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }

/* Potato also flattens the multiplayer and in-race screens: drop the article's
   themed background art and knock decorative panels back to flat fills. The
   universal rule above already strips blur/shadow/radius/animation everywhere,
   so only leftover background art needs handling here. */
body.gfx-potato .wiki-skin { background: #ffffff !important; }
body.gfx-potato .article-stage,
body.gfx-potato .mp-article-stage { background: #8a8aa0 !important; }
body.gfx-potato .mp-side-block,
body.gfx-potato .mp-rival,
body.gfx-potato .mp-lobby-row,
body.gfx-potato .mp-item-slot { background: #dcdcdc !important; border: 1px solid #000 !important; }

/* The result screen dropped its box entirely (see .mp-result-card), so the
   general .mp-lobby-row potato fill above would put it right back — undo
   that here and keep the plain white-on-black text potato gets everywhere
   else on this screen. */
body.gfx-potato .mp-result-list .mp-lobby-row { background: none !important; border: none !important; }

/* ═══ LOW / MINIMAL graphics ═════════════════════════════════════════════════
   Two lighter tiers that trim the most expensive effects without going fully
   primitive. LOW just drops backdrop-blur (the priciest GPU effect). MINIMAL
   also drops the animated 1.9 MB card previews and all CSS transitions. Both
   keep colours, gradients, shadows, rounded corners and the wallpaper. */
body.gfx-low *,
body.gfx-low *::before,
body.gfx-low *::after,
body.gfx-minimal *,
body.gfx-minimal *::before,
body.gfx-minimal *::after {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}

body.gfx-minimal *,
body.gfx-minimal *::before,
body.gfx-minimal *::after {
    transition: none !important;
}

/* ===========================
   ICONS
   =========================== */

/* Drawn glyphs from public/icons.js, in place of the emoji the UI used to
   print. Sized in em so each one tracks whatever text it sits beside — the
   containers that already set a font-size for their old emoji (.badge-icon at
   27px, .mp-item-icon at 26px, and so on) keep working untouched. Colour comes
   from currentColor, so an icon follows the player's accent without a variant. */
.wd-icon {
    width: 1em;
    height: 1em;
    display: inline-block;
    vertical-align: -0.14em;
    flex: 0 0 auto;
}

/* An icon slot with no art renders nothing (see public/icons.js), which leaves
   these wrappers empty. Collapse them so a text-only label sits flush instead
   of carrying a blank gap where a picture used to be. */
.badge-icon:empty,
.settings-tab-icon:empty,
.trophy-toast-icon:empty,
.wch-recent-mode:empty,
.cm-hazard-icon:empty,
.ch-icon:empty,
.ch-intro-icon:empty,
.mp-item-icon:empty,
.cos-emoji:empty {
    display: none;
}

/* Only the plain-icon variant of .cos-mini (an ICONS.markup() span) can
   legitimately end up empty, when the id it was given doesn't resolve to a
   drawn icon. The sprite variant is an <img> — a void element, so it always
   has zero children and would always match :empty regardless of whether its
   src actually loaded — and the swatch variant carries its content as a
   background style, not children, so it's childless by design. A bare
   `.cos-mini:empty` rule here caught both and hid every sprite/swatch cursor
   icon outright; scoping it to `span` and excluding both fixes that without
   losing the original fallback for a broken plain icon. */
span.cos-mini:empty:not(.cos-mini--sprite):not(.cos-mini--swatch):not(.cos-mini--lens-slot) {
    display: none;
}

/* ── /reset: where the emailed recovery link lands ─────────────────────────
   A standalone page, not the game, so it borrows the join card's look rather
   than its markup — same face, same frame, same buttons, on its own body. */
.reset-page {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    margin: 0;
    padding: 24px;
    background: var(--surface-3);
    font-family: var(--game-font);
}

.reset-card {
    display: flex;
    flex-direction: column;
    /* The base `body > main { flex: 1 }` would stretch this to the full height
       of the viewport; it is a card, not a page. */
    flex: 0 0 auto;
    width: min(92vw, 440px);
    padding: 32px 32px 24px;
    border: 5px solid #000;
    border-radius: 16px;
    background: var(--surface-2);
    box-shadow: 0 12px 0 rgba(0, 0, 0, 0.35);
    text-align: center;
}

.reset-logo {
    margin: 0;
    color: var(--ink-link);
    font-size: 34px;
    font-style: italic;
    font-weight: bold;
    text-shadow: 2px 2px 0 rgba(176, 176, 255, 0.9);
}

.reset-title {
    margin: 6px 0 10px;
    color: var(--ink-strong);
    font-size: 20px;
    font-weight: bold;
    letter-spacing: 0.5px;
}

.reset-note {
    margin: 0 0 16px;
    color: var(--ink-muted);
    font-size: 14px;
    line-height: 1.45;
}

.reset-label {
    margin-bottom: 4px;
    color: var(--ink);
    font-size: 13px;
    font-weight: bold;
    text-align: left;
}

.reset-input {
    box-sizing: border-box;
    width: 100%;
    margin-bottom: 12px;
    padding: 10px 12px;
    border: 3px solid #000;
    border-radius: 8px;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--game-font);
    font-size: 25.6px;
}

.reset-error {
    margin: 0 0 10px;
    color: #b00020;
    font-size: 13px;
    font-weight: bold;
}

.reset-btn {
    display: block;
    margin-top: 8px;
    padding: 12px 16px;
    border: 3px solid #000;
    border-radius: 10px;
    background: #35c04a;
    box-shadow: 0 4px 0 rgba(0, 0, 0, 0.3);
    color: #fff;
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: bold;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
}

.reset-btn:disabled { opacity: 0.6; cursor: default; }

.reset-btn--ghost {
    background: var(--surface);
    color: var(--ink);
}

/* ── Recovery email dialog (profile menu) ── */
.recovery-card { width: min(92vw, 460px); }

.recovery-body { padding: 18px 20px 20px; }

.recovery-note {
    margin: 0 0 12px;
    color: var(--ink-muted);
    font-size: 14px;
    line-height: 1.45;
}

.recovery-note--fine { margin: 12px 0 0; font-size: 12px; }

.recovery-input {
    box-sizing: border-box;
    width: 100%;
    padding: 10px 12px;
    border: 3px solid #000;
    border-radius: 8px;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--game-font);
    font-size: 25.6px;
}

.recovery-msg {
    margin: 10px 0 0;
    color: #1c7c31;
    font-size: 13px;
    font-weight: bold;
}

.recovery-msg.is-bad { color: #b00020; }

/* Admin-only Refresh / Reroll beside the wallet (shop-screen.js
   adminRefreshHtml). Quieter than the wallet: a workshop tool that happens to
   live on a player-facing screen. */
.shopx-refresh {
    font-size: .8cqw;
    line-height: 1;
    color: #9a9a9a;
}

.shopx-refresh svg { width: .9cqw; height: .9cqw; }

.shopx-refresh:hover:not(:disabled) {
    background: #3d3d3d;
    color: #d6d6d6;
}

.shopx-refresh:disabled {
    opacity: 0.6;
    cursor: default;
}

/* The reroll badge doubles as the reset button. Loud on purpose: while it is
   on screen every player's shop is on an admin-dealt hand. */
.shopx-refresh--preview {
    background: #d68c2a;
    color: #1a1a1a;
}

.shopx-refresh--preview:hover:not(:disabled) {
    background: #f8d08f;
    color: #1a1a1a;
}

/* ── Settings: an explanatory line above a group of rows ──────────────────
   The menu-card rows need one sentence of orientation before six identical
   URL boxes, which a per-row description cannot carry without repeating
   itself six times. */
.settings-note {
    margin: 0 0 14px;
    padding: 10px 14px;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface);
    font-family: var(--game-font);
    font-size: 20.8px;
    line-height: 1.55;
    color: var(--ink);
}

/* ── Settings: a named group inside one tab ───────────────────────────────
   Deliberately subordinate to .settings-section-title above it: same family
   and weight so it reads as the same system, two thirds the size and with a
   rule above it so it reads as a division of that section rather than as a
   second section. */
.settings-subhead {
    margin: 26px 0 10px;
    padding: 14px 4px 0;
    border-top: 2px solid rgba(0, 0, 0, 0.35);
    font-family: var(--game-font);
    font-size: 27.2px;
    font-weight: 900;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: #fff;
    text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55);
}

/* ── Room playlist dock ───────────────────────────────────────────────────────
   Fixed to the bottom-left and parented to <body>, never to a screen. Two
   reasons, and both are load-bearing:

   · An <iframe> reloads when it is reparented, and the lobby re-renders on
     every settings message. Living outside those trees is what lets the music
     survive a settings change and the whole lobby → race transition.
   · YouTube's embed terms want the player visible at 200×200 or larger and not
     covered by anything, for as long as it is playing. A fixed corner with its
     own stacking context is the only place that can be promised.

   Bottom-LEFT specifically: during a race .mp-side is already fixed to the
   bottom-right, and these two must not overlap.                              */
.room-music-dock {
    position: fixed;
    left: 12px;
    bottom: 12px;
    width: 224px;
    z-index: 320;
    border: 3px solid #000;
    border-radius: 10px;
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    box-shadow: 0 5px 0 rgba(0, 0, 0, 0.22);
    padding: 8px 10px 10px;
}

.room-music-dock[hidden] { display: none; }

.room-music-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 8px;
}

.room-music-title {
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: 900;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--ink-muted);
}

.room-music-hide {
    appearance: none;
    border: 2px solid #000;
    border-radius: 6px;
    background: var(--surface-3);
    font-size: 14px;
    font-weight: 900;
    line-height: 1;
    padding: 2px 7px 4px;
    color: var(--ink);
    cursor: pointer;
}

.room-music-what {
    font-size: 13px;
    font-weight: 700;
    color: var(--ink);
    margin: 0 0 4px;
}

/* The sentence naming the third party. Deliberately not styled down to a
   whisper — it is the disclosure the privacy page promises, so it reads at the
   same size as everything else in the card. */
.room-music-warn {
    font-size: 12px;
    line-height: 1.4;
    color: var(--ink-muted);
    margin: 0 0 8px;
}

.room-music-go,
.room-music-no {
    appearance: none;
    width: 100%;
    padding: 8px;
    border: 2px solid #000;
    border-radius: 8px;
    font-family: var(--game-font);
    font-size: 20.8px;
    font-weight: 900;
    cursor: pointer;
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.25);
}

.room-music-go {
    background: linear-gradient(180deg, #ffd66b, #f7b733);
    color: #5b3a00;
}

.room-music-no {
    margin-top: 6px;
    background: var(--surface-3);
    color: var(--ink-muted);
    box-shadow: none;
}

/* 200×200 is a floor set by YouTube's terms, not a look. Do not shrink this to
   fit a layout; move the dock instead. */
.room-music-frame {
    display: block;
    width: 200px;
    height: 200px;
    border: 0;
    border-radius: 6px;
    background: #000;
}

/* Transport. These exist because YouTube's own controls are inside the iframe
   and go inert with it: when an item is blocked the embed shows "Video
   unavailable" and ignores every click, so without a row of our own there is
   no way out of a dead track. Kept outside .room-music-body, which the builder
   empties when it swaps the iframe in. */
.room-music-transport {
    display: flex;
    gap: 6px;
    margin-top: 8px;
}

.room-music-btn {
    appearance: none;
    flex: 1;
    padding: 6px 0 7px;
    border: 2px solid #000;
    border-radius: 7px;
    background: var(--surface-3);
    color: var(--ink);
    font-size: 12px;
    font-weight: 900;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.2);
}

.room-music-btn:active {
    transform: translateY(2px);
    box-shadow: none;
}

/* The box and its button on one line. Adding music is an explicit press, so
   the button has to sit where the eye already is after typing. */
.mp-playlist-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: stretch;
}

.mp-playlist-actions .mp-set-text {
    flex: 1 1 180px;
    min-width: 0;
}

.mp-playlist-go,
.mp-playlist-off {
    appearance: none;
    flex: 0 0 auto;
    padding: 0 12px;
    border: 2px solid #000;
    border-radius: 7px;
    font-family: var(--game-font);
    font-size: 19.2px;
    font-weight: 900;
    cursor: pointer;
    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.22);
}

.mp-playlist-go {
    background: linear-gradient(180deg, #ffd66b, #f7b733);
    color: #5b3a00;
}

.mp-playlist-off {
    background: var(--surface-3);
    color: var(--ink-muted);
}

.mp-playlist-go:disabled,
.mp-playlist-off:disabled {
    opacity: 0.5;
    cursor: default;
    box-shadow: none;
}

.mp-playlist-go:not(:disabled):active,
.mp-playlist-off:not(:disabled):active {
    transform: translateY(2px);
    box-shadow: none;
}

.mp-playlist-msg { min-height: 1.2em; }
.mp-playlist-msg.is-bad { color: var(--bad, #c0392b); font-weight: 700; }

@media (max-width: 900px) {
    /* The item slot becomes a fixed bar across the bottom on small screens, so
       the dock steps above it rather than under it. Still 200×200, still
       uncovered — shrinking it here would break the embed terms. */
    .room-music-dock {
        bottom: 78px;
    }
}

#mpSpyStage .mp-ghost-cursor { transition: transform 90ms linear; }
.mp-spy-banner [data-spy-close] { margin-left: 8px; cursor: pointer; }

/* ===========================
   BOOT SPLASH
   =========================== */

/* Sits above literally everything (profile card, Discord button, header) so
   the very first frame is just black, no matter what has already rendered
   underneath it. BootSplash in script.js adds .boot-done once real boot work
   is done, which fades this out and then removes it — after that there is
   nothing left in the DOM to keep off z-index or pointer-events. */
#bootSplash {
    position: fixed;
    inset: 0;
    /* Just under .join-overlay (100000): the guest/login card has to paint
       on top of this black backdrop rather than the colorful menu it's
       hiding, so the card doesn't visibly appear "on the menu" for the one
       or two frames before this fades. Still above everything else on the
       page, including the wallpaper credit line's own outranking — that
       line (100001) deliberately survives every overlay, this one included. */
    z-index: 99999;
    background: #000;
    opacity: 1;
    transition: opacity 0.5s ease;
}

#bootSplash.boot-done {
    opacity: 0;
    pointer-events: none;
}

/* The same loader used everywhere else in the game (article fetches, the
   multiplayer queue) — this is just its biggest, most prominent showing,
   tucked in the corner the way a console splash tucks its build logo.
   wd-loader sizes itself off --loader-size (see the wd-loader rules above),
   not off this wrapper, so the size has to be set on .boot-loader itself.
   Column layout stacks .boot-status directly above it. BootSplash.hideLoader()
   adds .is-hidden once boot is done (no messages left to show), fading this
   corner out on its own — the plain black #bootSplash behind it stays up
   until dismiss() decides the join flow is done (see script.js). */
.boot-logo-corner {
    position: absolute;
    right: 20px;
    bottom: 16px;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 12px;
    opacity: 1;
    transition: opacity 0.3s ease;
}

.boot-logo-corner.is-hidden {
    opacity: 0;
    pointer-events: none;
}

/* One line per in-flight boot step (see BootSplash.start() in script.js). A
   line appears the moment its step starts and is removed the moment it ends
   — no checkmark, nothing lingers — so several can be stacked here at once
   for however briefly their steps overlap, same as watching a real log. */
.boot-status {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
}

.boot-status-line {
    font-family: var(--game-font);
    font-size: 13px;
    letter-spacing: 0.02em;
    color: #fff;
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.22s ease, transform 0.22s ease;
}

.boot-status-line.is-shown {
    opacity: 1;
    transform: none;
}

wd-loader.boot-loader { --loader-size: 84px; }

@media (max-width: 640px) {
    .boot-logo-corner {
        right: 12px;
        bottom: 12px;
    }

    wd-loader.boot-loader { --loader-size: 60px; }
}

/* Quick Play uses the same article and rail as a multiplayer match. Keep the
   run controls compact so the article remains the main surface. */
.ir-room #irClock {
    display: block;
    margin: 2px 0 6px;
    color: var(--wiki-fg);
    font: 700 28px/1.1 monospace;
    font-variant-numeric: tabular-nums;
}
.ir-room #irClock.is-low, .ir-room #irError { color: #c92a2a; }
.ir-room #irStats, .ir-room .ir-racer-status, .ir-room .mp-rival small {
    display: block;
    color: var(--wiki-muted);
    font: 13px/1.4 sans-serif;
}
.ir-room #irStats { margin-bottom: 10px; }
.ir-room .ir-run-block .mp-play-btn,
.ir-room .ir-run-block .mp-back-btn { width: 100%; margin: 0 0 6px; padding: 6px 8px; font-size: 16px; }
.ir-room #irResult:not(:empty), .ir-room #irError:not(:empty) { margin: 7px 0 0; font: 13px/1.4 sans-serif; }
.ir-room .ir-weekly { font: 13px/1.4 sans-serif; }
.ir-room .ir-weekly summary { cursor: pointer; font-weight: bold; }
.ir-room .ir-weekly[open] summary { margin-bottom: 8px; }
.ir-room #irWeek { color: var(--wiki-muted); }
.ir-room .mp-rival { padding: 5px 7px; }
.ir-room .mp-quit-btn { width: 100%; }
.ir-room .wiki-link-inline:disabled { cursor: default; }
.ir-room .route-target-desc { font-size: 13px; line-height: 1.2; white-space: nowrap; }

@media (max-width: 768px) {
    .ir-room .wiki-skin { padding-bottom: 130px; }
    .ir-room .mp-side .ir-run-block {
        position: fixed;
        inset: auto 0 0;
        z-index: 330;
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        gap: 4px 10px;
        padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
        border-top: 1px solid var(--wiki-border);
        background: var(--wiki-bg);
    }
    .ir-room .ir-run-block .mp-side-title { display: none; }
    .ir-room #irClock { grid-row: 1 / 3; margin: 0; font-size: 24px; }
    .ir-room #irStats { grid-column: 2; margin: 0; font-size: 12px; }
    .ir-room .ir-run-block .mp-play-btn,
    .ir-room .ir-run-block .mp-back-btn { grid-column: 3; grid-row: 1 / 3; margin: 0; white-space: nowrap; }
    .ir-room #irResult:not(:empty), .ir-room #irError:not(:empty) { grid-column: 1 / -1; }
}

/* Keep brief meetup messages visible above the covered in-race chat. */
.mp-meetup-log {
    width: 100%;
    flex-shrink: 0;
    font-family: var(--game-font);
    height: 3.8em;
    overflow-y: auto;
    margin: 10px 0;
    font-size: 12px;
    line-height: 1.8;
    text-align: left;
    opacity: 0.8;
}

/* Quickmatch podium ownership applies to the square and its step. */
.mp-podium-step { position: relative; }
.mp-podium-zone {
    position: absolute;
    bottom: 100%;
    left: -4px;
    width: calc(100% + 8px);
    aspect-ratio: 1;
    pointer-events: none;
}
.mp-lobby-cursor.is-podium-hidden { visibility: hidden; }
body.mp-podium-blocked, body.mp-podium-blocked * { cursor: not-allowed !important; }
body.mp-podium-blocked .cos-lens-outline,
body.mp-podium-blocked .cos-lens,
body.mp-podium-blocked .cos-lens-accessory,
body.mp-podium-blocked .cos-glass-outline,
body.mp-podium-blocked .cos-glass-fill { visibility: hidden; }

/* Quick Match: shared dice draw and staggered difficulty reels. */
.arena-draw {
  --draw-easy: #84efbd; --draw-medium: #ffd478; --draw-hard: #ff8b91; --draw-fully-random: #cf9aff;
  position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center;
  overflow: auto; padding: 24px; background: #000; color: #f7f9ff; font-family: var(--game-font);
}
/* The cursor layer is a sibling spanning the whole overlay, so bots can roam
   the screen, and people are measured against that whole screen too: the vote
   panels are pinned to its corners, so a hover on red lands on red for
   everyone whatever their window size. */
.arena-draw-panel { position: relative; width: min(100%, 960px); text-align: center; }
.arena-draw-eyebrow { font-size: 11px; font-weight: 800; letter-spacing: .25em; color: #aac2de; }
.arena-draw h2 { margin: 14px 0 10px; font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; color: #fff; }
.arena-die-stage { height: 205px; display: grid; place-items: center; perspective: 650px; }
.arena-die { position: relative; width: 94px; height: 94px; transform-style: preserve-3d; transform: rotateX(var(--rx)) rotateY(var(--ry)); animation: arena-dice-roll 2400ms cubic-bezier(.15,.65,.25,1) both; }
.arena-die-face { position: absolute; inset: -0.5px; display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr); gap: 9px; padding: 15px; box-sizing: border-box; background: #e6272f; backface-visibility: hidden; }
.arena-die-face i { border-radius: 0 !important; }
/* A pixel circle on a 10x10 grid: a cell is lit if its centre falls inside the
   circle. Drawn as an image rather than a clip-path so it tilts with its face
   like paint; Chrome clips after the 3D transform and frays the steps. */
.arena-die-face .pip { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath fill='%23fff' d='M3 0h4v1h2v2h1v4h-1v2h-2v1h-4v-1h-2v-2h-1v-4h1v-2h2z'/%3E%3C/svg%3E") center / contain no-repeat; }
/* A solid red core: three planes through the centre, so anything Chrome's
   edge antialiasing lets through between two faces is red, not background. */
.arena-die-core, .arena-die-core::before, .arena-die-core::after { position: absolute; inset: 1px; background: #e6272f; }
.arena-die-core::before, .arena-die-core::after { content: ""; inset: -1px; }
.arena-die-core { transform-style: preserve-3d; }
.arena-die-core::before { transform: rotateY(90deg); }
.arena-die-core::after { transform: rotateX(90deg); }
.arena-die-face--1 { transform: translateZ(47px); }
.arena-die-face--2 { transform: rotateY(90deg) translateZ(47px); }
.arena-die-face--3 { transform: rotateX(90deg) translateZ(47px); }
.arena-die-face--4 { transform: rotateX(-90deg) translateZ(47px); }
.arena-die-face--5 { transform: rotateY(-90deg) translateZ(47px); }
.arena-die-face--6 { transform: rotateY(180deg) translateZ(47px); }
.arena-draw-result { font-size: 21px; font-weight: 800; margin-bottom: 26px; animation: arena-draw-in 350ms 2400ms both; }
.arena-reels { display: grid; grid-template-columns: repeat(var(--reels), minmax(0, 1fr)); gap: 12px; }
/* Once ArenaReels has the canvas, the CSS row is only the placeholder that keeps
   the panel's layout; its height is set from the canvas scale. */
.arena-reels.is-pixi { visibility: hidden; display: block; }
/* Horizontal paper stacks the reels, and they need the room the vote panels'
   reserve would take: the panels sit in the corners, clear of the centred rows. */
.arena-draw.arena-reels-h { padding-bottom: 24px; }
.arena-reel { min-width: 0; padding: 16px 6px 12px; border: 1px solid #ffffff25; border-radius: 16px; background: #ffffff08; animation: arena-draw-in 350ms var(--delay) both, arena-reel-hit 550ms calc(var(--delay) + 1800ms) both; }
.arena-reel-label { font-size: 10px; letter-spacing: .18em; color: #b2c2d9; font-weight: 800; }
.arena-reel-window { height: 56px; margin: 10px 0; overflow: hidden; border-block: 1px solid #ffffff18; background: #030b1766; }
.arena-reel-strip { animation: arena-reel-spin 1800ms var(--delay) cubic-bezier(.12,.66,.16,1) both; }
.arena-reel-strip > div { height: 56px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; padding: 4px 6px; line-height: 1.15; overflow-wrap: anywhere; overflow: hidden; color: #f7f9ff; font-size: clamp(11px, 1.4vw, 18px); font-weight: 900; }
.arena-reel-lock { display: block; color: var(--rarity); font-size: 9px; font-weight: 800; letter-spacing: .05em; animation: arena-draw-in 200ms calc(var(--delay) + 1800ms) both; }
.arena-draw-ready { margin: 26px 0 0; color: #b9d1ec; animation: arena-draw-in 300ms var(--ready) both; }
.arena-draw-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
/* The two vote panels sit in the bottom corners of the screen, big enough to
   hold a crowd of pointers. The split itself only fades: a transform on it
   would pin the fixed halves to it instead of to the screen. --vote-h is also
   reserved under the draw so the reels never sit behind a panel. */
.arena-draw { --vote-w: min(40vw, 460px); --vote-h: min(34vh, 300px); padding-bottom: calc(var(--vote-h) + 36px); }
.arena-rewards { display: flex; justify-content: center; gap: 12px; margin-top: 18px; opacity: 0; translate: 0 10px; transition: opacity 320ms ease, translate 320ms ease; }
.arena-rewards.is-visible { opacity: 1; translate: 0 0; }
.arena-reward { display: flex; flex-direction: column; align-items: center; min-width: 110px; padding: 10px 16px; border-radius: 14px; background: #ffffff14; border: 2px solid var(--medal, #b9d1ec); color: #fff; }
.arena-reward--1 { --medal: #f5c542; }
.arena-reward--2 { --medal: #c9d3de; }
.arena-reward--3 { --medal: #d08a4e; }
.arena-reward-place { font-size: 12px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--medal); }
.arena-reward-xp { margin-top: 4px; font-size: clamp(20px, 3vw, 30px); font-weight: 900; line-height: 1; font-variant-numeric: tabular-nums; }
.arena-reward--1 .arena-reward-xp { font-size: clamp(24px, 3.6vw, 36px); }
@media (max-width: 620px) {
  .arena-rewards { gap: 6px; margin-top: 10px; }
  .arena-reward { min-width: 0; flex: 1; padding: 8px 6px; }
}
.arena-vote-split { opacity: 0; pointer-events: none; transition: opacity 320ms ease; }
.arena-vote-split.is-visible { opacity: 1; pointer-events: auto; }
.arena-vote-half { position: fixed; bottom: 18px; z-index: 3; box-sizing: border-box; width: var(--vote-w); height: var(--vote-h); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 18px; border-radius: 24px; text-align: center; cursor: pointer; user-select: none; translate: 0 12px; transition: translate 320ms ease, transform 150ms ease, box-shadow 150ms ease, filter 150ms ease; }
.arena-vote-split.is-visible .arena-vote-half { translate: 0 0; }
.arena-vote-half:hover { transform: translateY(-4px); }
.arena-vote-reroll { left: 18px; background: linear-gradient(145deg, #e6272f, #a80f17); box-shadow: 0 8px 26px #a80f1750; }
.arena-vote-reroll:hover { box-shadow: 0 14px 36px #e6272f80; }
.arena-vote-keep { right: 18px; background: linear-gradient(145deg, #2fbf5f, #1c8a43); box-shadow: 0 8px 26px #1c8a4350; }
.arena-vote-keep:hover { box-shadow: 0 14px 36px #2fbf5f80; }
.arena-vote-timer { position: fixed; bottom: 18px; left: 50%; translate: -50% 0; z-index: 3; height: var(--vote-h); display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; pointer-events: none; color: #fff; }
.arena-vote-timer-label { font-size: 12px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; color: #b2c2d9; }
.arena-vote-timer-num { margin-top: 6px; font-size: clamp(34px, 6vw, 64px); line-height: 1; font-weight: 900; font-variant-numeric: tabular-nums; }
.arena-vote-half.is-capped { cursor: default; pointer-events: none; filter: grayscale(.6); opacity: .55; }
.arena-vote-label { display: block; font-weight: 900; letter-spacing: .06em; font-size: clamp(26px, 4vw, 44px); line-height: 1; color: #fff; text-transform: uppercase; }
.arena-vote-count { display: block; margin-top: 10px; font-size: clamp(34px, 6vw, 64px); line-height: 1; color: #fff; font-weight: 900; }
.arena-vote-left { display: block; margin-top: 8px; font-size: 14px; color: #ffffffc0; font-weight: 700; }
.arena-vote-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 14px; max-height: 30%; overflow: hidden; }
.arena-vote-chip { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 12px; background: #00000030; font-size: 13px; font-weight: 700; color: #fff; }
.arena-vote-chip.is-bot { opacity: .85; }
@media (max-width: 620px) {
  .arena-draw { --vote-w: calc(50vw - 18px); --vote-h: min(26vh, 200px); }
  .arena-vote-timer { bottom: calc(var(--vote-h) + 20px); height: auto; flex-direction: row; gap: 10px; }
  .arena-vote-timer-num { margin-top: 0; font-size: 28px; }
  .arena-vote-reroll { left: 12px; }
  .arena-vote-keep { right: 12px; }
  .arena-vote-half { bottom: 12px; padding: 12px; }
}
@keyframes arena-dice-roll { 0% { transform: translateY(-100px) rotateX(-650deg) rotateY(-700deg) scale(.5); } 65% { transform: translateY(10px) rotateX(calc(var(--rx) + 25deg)) rotateY(calc(var(--ry) - 30deg)); } 82% { transform: translateY(-14px) rotateX(calc(var(--rx) - 12deg)) rotateY(calc(var(--ry) + 10deg)); } 100% { transform: translateY(0) rotateX(var(--rx)) rotateY(var(--ry)); } }
@keyframes arena-draw-in { from { opacity: 0; translate: 0 12px; } to { opacity: 1; translate: 0 0; } }
@keyframes arena-reel-spin { from { transform: translateY(0); } to { transform: translateY(-616px); } }
@keyframes arena-reel-hit { 0% { border-color: var(--rarity); box-shadow: 0 0 28px color-mix(in srgb, var(--rarity) 40%, transparent); } 100% { border-color: var(--rarity); box-shadow: 0 0 12px color-mix(in srgb, var(--rarity) 12%, transparent); } }
@media (max-width: 620px) {
  .arena-draw { padding: 18px 18px calc(var(--vote-h) + 24px); }
  .arena-reels { grid-template-columns: repeat(min(var(--reels), 3), minmax(0, 1fr)); gap: 8px; }
  .arena-die-stage { height: 170px; }
  .arena-reel { padding-top: 10px; }
}
@media (prefers-reduced-motion: reduce) {
  .arena-die { animation: none; }
  .arena-reel-strip { animation: none; transform: translateY(-616px); }
  .arena-draw-result, .arena-reel, .arena-reel-lock, .arena-draw-ready { animation-duration: 1ms; }
}

/* ===========================
   MAIN MENU SCENE (main-menu.js)
   =========================== */

/* The backdrop: one fixed layer behind every screen. The stage is
   a 16:9 box scaled to cover the window, so the horizon sits at the same
   height on any screen shape. Until the PixiJS sunset is up, and for good if
   WebGL or the CDN is blocked, the gradient on the stage is the sky. */
.mm-scene {
    position: fixed;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
}
.mm-scene-stage {
    position: absolute;
    left: 50%;
    top: 50%;
    width: max(100vw, 177.7778vh);
    aspect-ratio: 16 / 9;
    transform: translate(-50%, -50%);
    background: linear-gradient(#b43f42, #ea5c44 25%, #f7863f 45%, #f7c887 60%, #e3f6ee 66.9%,
        #d98a4e 67%, #a8522f 80%, #7b3a2c);
}
.mm-sunset-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
/* The hovered shop theme as the wallpaper, over the sunset (shop-screen.js). */
.shopx-wallpaper {
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.6s ease;
}
.shopx-wallpaper.is-on { opacity: 1; }
.theme-preview-pick { cursor: pointer; }
/* The preview's own Text and Width radios, same sizes as the real
   .wiki-skin[data-text]/[data-width] rules. */
.theme-preview-skin[data-text="small"] .theme-preview-page p { font-size: 13px; }
.theme-preview-skin[data-text="large"] .theme-preview-page p { font-size: 16.5px; }
.theme-preview-skin[data-width="wide"] { width: 100%; }

/* The six destinations, three by two. Two classes so this beats the plain
   .grid-container rules, including the responsive ones further up. */
/* The stage's top/bottom padding was room for the header and footer bands,
   which are gone. Left in, MenuFit counted it as lost height and shrank the
   menu to ~84% of the prototype on a 1080p screen. */
.menu-stage:has(> .mm-menu) { padding: 0; }
.grid-container.mm-menu {
    grid-template-columns: repeat(3, 240px);
    gap: 113px 200px;
    width: auto;
    align-items: center;
    justify-content: center;
    /* The sun sits low; lifting the menu a little keeps it clear of the
       horizon the way the prototype framed it. A translate, not a margin: in
       the centred stage a negative margin only moves it by half. */
    translate: 0 -7vh;
}
.mm-item {
    position: relative;
    display: block;
    width: 100%;
    height: 220px;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    cursor: pointer;
    user-select: none;
    transition: transform .2s;
}
.mm-item:hover, .mm-item:focus-visible { transform: scale(1.06); }
.mm-item:focus-visible { outline: 3px solid #ffc477; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .mm-item { transition: none; } }
.mm-art img { display: block; width: 100%; }

/* The label hangs under each picture. It is real text for screen readers
   and the fallback. */
.mm-label {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    font: 700 22px/1 'Mono', Consolas, monospace;
    text-transform: none;
    color: #fff3df;
    text-shadow: 0 2px 0 #271e29, 0 0 10px #271e29aa;
    pointer-events: none;
}

/* Single Player: a lone figure on a chair. */
.mm-singleplayer { --s: 160px; }
.mm-single { position: absolute; left: 50%; top: 50%; width: var(--s); aspect-ratio: 964 / 1027; transform: translate(-50%, -50%); }
.mm-singleplayer .mm-label { top: calc(50% + var(--s) * 1027 / 964 / 2 + 4px); }

/* Multiplayer: the earth, twice the loader's orbit, with racers circling it.
   The canvas is square and may spill past its column. */
.mm-multiplayer { --orbit: 440px; }
.mm-item.mm-multiplayer { height: var(--orbit); }
.mm-earth { display: block; height: 100%; aspect-ratio: 1; margin-left: 50%; transform: translateX(-50%); }
/* The globe's radius is 94/440 of the orbit box. */
.mm-multiplayer .mm-label { top: calc(50% + var(--orbit) * 94 / 440 + 4px); }

/* Settings: two meshing gears. gear.png has 36 teeth and gear2.png 18, so the
   small one is drawn at half the pitch radius (0.518 of the big box) and turns
   twice as fast the other way; the 7deg start drops its teeth into the gaps. */
.mm-settings { --g: 150px; }
.mm-gears { position: absolute; left: 50%; top: 50%; width: var(--g); height: var(--g); margin: calc(var(--g) * -0.5 + var(--g) * 0.129) 0 0 calc(var(--g) * -0.629); }
.mm-gear { position: absolute; display: block; animation: mm-gear-spin 16s linear infinite; }
.mm-gear-big { inset: 0; width: 100%; }
/* Two classes so the width beats .mm-art img's 100%. */
.mm-gears .mm-gear-small { left: calc(var(--g) * 0.74); top: calc(var(--g) * -0.258); width: calc(var(--g) * 0.518); animation-duration: 8s; animation-direction: reverse; rotate: 7deg; }
@keyframes mm-gear-spin { to { transform: rotate(360deg); } }
.mm-settings .mm-label { top: calc(50% + var(--g) * 0.629 + 4px); }

/* Leaderboards: the podium, with a cursor bobbing over each step. The canvas
   has headroom above the podium for the bob. */
.mm-wikichannel { --p: 230px; }
.mm-podium { position: absolute; left: 50%; top: 50%; width: var(--p); aspect-ratio: 1464 / 566; transform: translate(-50%, -50%); }
.mm-podium-cursors { position: absolute; left: -30%; top: -250%; width: 160%; height: 350%; pointer-events: none; }
.mm-wikichannel .mm-label { top: calc(50% + var(--p) * 566 / 1464 / 2 + 4px); }

/* Shop: the gumball machine, plus room under it for a dropped cursor. */
.mm-gumball { position: absolute; left: 0; top: 0; width: 100%; height: 190px; }
.mm-shop .mm-label { top: 194px; }

/* About: a stack of papers. */
.mm-about { --a: 190px; }
.mm-papers { position: absolute; left: 50%; top: 50%; width: var(--a); aspect-ratio: 812 / 551; transform: translate(-50%, -50%); }
.mm-about .mm-label { top: calc(50% + var(--a) * 551 / 812 / 2 + 4px); }

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

/* Screen changes slide the pictures out and in the way the cards did. */
.grid-container.is-exiting .mm-item {
    animation: cardSlideOutLeft 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
    animation-delay: var(--row-delay);
}
.grid-container.is-entering .mm-item {
    animation: cardSlideInRight 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
    animation-delay: var(--row-delay);
}
.grid-container.is-exiting-back .mm-item {
    animation: cardSlideOutRight 260ms cubic-bezier(0.7, 0, 1, 0.35) forwards;
    animation-delay: var(--row-delay);
}
.grid-container.is-entering-back .mm-item {
    animation: cardSlideInLeft 260ms cubic-bezier(0.04, 0.82, 0.18, 1) backwards;
    animation-delay: var(--row-delay);
}

/* Narrow or short windows: two columns and smaller pictures, then MenuFit
   shrinks whatever still does not fit. */
@media (max-width: 900px) {
    .grid-container.mm-menu { grid-template-columns: repeat(2, minmax(0, 200px)); gap: 40px 16px; width: min(416px, calc(100vw - 32px)); translate: none; }
}
@media (max-width: 540px), (max-height: 540px) {
    .mm-item { height: 150px; }
    .mm-label { font-size: 16px; }
    .mm-multiplayer { --orbit: 240px; }
    .mm-settings { --g: 100px; }
    .mm-wikichannel { --p: 150px; }
    .mm-singleplayer { --s: 105px; }
    .mm-about { --a: 120px; }
    .mm-gumball { height: 128px; }
    .mm-shop .mm-label { top: 130px; }
}

/* ===========================
   MULTIPLAYER HUB (multiplayer-screen.js renderMenu)
   =========================== */

/* Four pictures on the sunset, placed one by one like the main menu, so the
   hub takes the whole window instead of a centred column. */
.menu-stage:has(> .mp-hub-mode) { padding: 0; }
.grid-container.multiplayer-container.mp-hub-mode {
    width: 100%;
    max-width: none;
    /* The window less <main>'s 20px padding above and below. */
    height: calc(100vh - 40px);
}
.mp-hub-mode .multiplayer-screen { height: 100%; padding: 0; }
.mp-hub {
    position: relative;
    width: 100%;
    height: 100%;
    text-transform: none;
}
.mp-hub > .mp-error {
    position: absolute;
    top: 12px;
    left: 50%;
    translate: -50% 0;
}

/* x/y in the layout are the picture's centre; width comes inline. `translate`
   centres it so `transform` stays free for the hover grow. */
.mp-pic {
    position: absolute;
    display: block;
    padding: 0;
    border: 0;
    background: none;
    translate: -50% -50%;
    cursor: pointer;
    user-select: none;
    transition: transform .2s;
}
.mp-pic:hover, .mp-pic:focus-visible { transform: scale(1.06); }
.mp-pic:focus-visible { outline: 3px solid #ffc477; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .mp-pic { transition: none; } }
.mp-pic-img { display: block; width: 100%; pointer-events: none; }
/* Join Room is Create Room's dollhouse, mirrored and in negative. */
.mp-pic[data-pic="join"] .mp-pic-img { filter: invert(1); scale: -1 1; }

/* Same label as the main menu's (.mm-label): real text under the picture. */
.mp-pic-label {
    position: absolute;
    top: calc(100% + 18px);
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    font: 700 30px/1 'Mono', Consolas, monospace;
    color: #fff3df;
    text-shadow: 0 2px 0 #271e29, 0 0 10px #271e29aa;
    pointer-events: none;
}

/* The live counts (updateMenuBadges) sit just off the picture's top right
   corner, clear of anything drawn in it (Ranked's name tags). */
.mp-pic .mp-row-badge {
    position: absolute;
    top: 0;
    left: 100%;
    translate: 8px -50%;
    padding: 5px 9px;
    border: 0;
    border-radius: 0;
    background: #271e29;
    font: 700 20px/1 'Mono', Consolas, monospace;
    color: #fff3df;
}

/* The fake counts (updateMenuBadges). WikiDerby's pair sits inside the
   picture's top right corner; Ranked's single one stays just off its top
   right, clear of the name tags drawn in the picture. */
.mp-pic-counts {
    position: absolute;
    top: 10px;
    right: 10px;
    display: flex;
    gap: 6px;
}
.mp-pic[data-pic="ranked"] .mp-pic-counts {
    top: 0;
    right: auto;
    left: 100%;
    translate: 8px -50%;
}
.mp-pic-count {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 9px;
    background: #271e29;
    font: 700 20px/1 'Mono', Consolas, monospace;
    color: #fff3df;
}
.mp-pic-count[hidden] { display: none; }
.mp-pic-count img { display: block; height: 18px; width: auto; }

/* Layout mode (/?mplayout=1). */
.mp-hub.is-editing .mp-pic { cursor: move; touch-action: none; outline: 2px dashed #fff3df; }
.mp-hub.is-editing .mp-pic:hover { transform: none; }
.mp-hub-tools {
    position: fixed;
    left: 50%;
    bottom: 24px;
    translate: -50% 0;
    z-index: 50;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    background: #271e29;
    font: 700 16px/1 'Mono', Consolas, monospace;
    color: #fff3df;
}
.mp-hub-tools button {
    padding: 6px 12px;
    border: 0;
    border-radius: 0;
    background: #00ff40;
    font: inherit;
    color: #0a3312;
    cursor: pointer;
}

/* Shared menu chrome. Keep the reading surface and storefront independent:
   no global font/palette changes and no selectors inside the wiki skin. */
:root {
    --ui-face: Arial, 'Liberation Sans', sans-serif;
    --ui-line: color-mix(in srgb, var(--ink) 22%, transparent);
    --ui-panel: linear-gradient(145deg,
        color-mix(in srgb, var(--theme-tint) 94%, transparent),
        color-mix(in srgb, var(--theme-tint2) 94%, transparent));
    --ui-shadow: 0 8px 24px #0002, inset 0 0 0 1px #ffffff50;
    --ui-selected: linear-gradient(135deg, var(--accent-soft), var(--accent));
}

/* The heading face follows the menu; chat keeps its own face. Scoped variables
   let existing component rules inherit the new typography without touching
   article typography, cosmetics, or the shop. */
body:not(.wiki-reading) #menuGrid:not(.shop-container):not(.main-menu),
:is(.join-card, .reset-card, .badges-card, .mp-trade, .profile-menu,
    .friends-panel, .notif-card, .stats-notice, .mp-result-card,
    .cm-fail-card, .rc-finish-card, .room-music-dock, .arena-draw) {
    --game-font: var(--ui-face);
    font-family: var(--ui-face);
}

body:not(.wiki-reading) #menuGrid :is(
    .settings-row, .about-card, .custom-form, .cm-section, .challenge-card,
    .challenge-daily-strip, .ch-intro, .wch-panel, .wch-topbar,
    .channel-topbar, .mp-home:not(.mp-home--bare), .mp-set-panel,
    .mp-chat, .mp-lobby-roster, .pp-tile, .wch-record) {
    border: 1px solid var(--ui-line);
    border-top: 3px solid var(--accent);
    background: var(--ui-panel);
    box-shadow: var(--ui-shadow);
    color: var(--ink);
}

:is(.join-card, .reset-card, .badges-card, .mp-trade, .profile-menu,
    .friends-panel, .notif-card, .stats-notice, .cm-fail-card,
    .rc-finish-card, .room-music-dock) {
    border: 1px solid var(--ui-line);
    border-top: 3px solid var(--accent);
    background: var(--ui-panel);
    box-shadow: var(--ui-shadow);
    color: var(--ink);
}

body:not(.wiki-reading) #menuGrid :is(
    .mp-lobby-row, .mp-room-row, .wch-row:not(.has-banner), .wch-recent-row,
    .about-person, .credit-row, .badge-card:not(.is-secret), .mp-trade-side) {
    border: 1px solid var(--ui-line);
    background: color-mix(in srgb, var(--surface) 88%, transparent);
    box-shadow: none;
    color: var(--ink);
}
body:not(.wiki-reading) #menuGrid .wch-row:nth-child(-n+3) {
    border-inline-start: 4px solid var(--accent-deep);
}

/* Deliberate component lists preserve menu card artwork, cosmetic previews,
   colour swatches, status indicators, and the entire in-race article UI. */
body:not(.wiki-reading) #menuGrid :is(
    .settings-tab, .settings-toggle, .settings-choice button, .settings-logout,
    .cm-preset, .sr-swap, .mp-select-btn, .mp-bot-add, .mp-trade-open, .about-tab,
    .wch-tab, .wch-subtab, .mp-set-tab, .mp-play-btn, .mp-back-btn,
    .custom-play-btn, .challenge-play-btn, .story-back-btn, .story-begin-btn,
    .cm-dice, .cm-chips button, .mp-set-toggle, .mp-set-choice button) {
    border: 1px solid var(--ui-line);
    background: var(--ui-panel);
    color: var(--ink);
    box-shadow: inset 0 0 0 1px #ffffff40;
    font-family: var(--ui-face);
    font-size: 16px;
    font-weight: 800;
    text-shadow: none;
    text-transform: uppercase;
    min-height: 42px;
    transition: background 140ms ease, border-color 140ms ease;
}
body:not(.wiki-reading) #menuGrid :is(
    .settings-tab, .settings-toggle, .settings-choice button, .settings-logout,
    .cm-preset, .sr-swap, .mp-select-btn, .mp-bot-add, .mp-trade-open, .about-tab,
    .wch-tab, .wch-subtab, .mp-set-tab, .mp-play-btn, .mp-back-btn,
    .custom-play-btn, .challenge-play-btn, .story-back-btn, .story-begin-btn,
    .cm-dice, .cm-chips button, .mp-set-toggle, .mp-set-choice button):hover:not(:disabled) {
    border-color: var(--accent-deep);
    background: color-mix(in srgb, var(--accent-soft) 24%, var(--surface));
}
body:not(.wiki-reading) #menuGrid :is(
    .settings-tab.is-active, .settings-toggle.is-on, .settings-choice button.is-active,
    .about-tab.is-active, .about-tab.on, .wch-tab.is-active, .wch-subtab.is-active,
    .mp-set-tab.is-active, .cm-chips button.is-active,
    .mp-set-toggle.is-on, .mp-set-choice button.is-active,
    .custom-play-btn, .challenge-play-btn, .story-begin-btn) {
    background: var(--ui-selected);
    border-color: var(--accent-deep);
    color: #111;
    box-shadow: inset 0 -3px 0 #0002;
}
body:not(.wiki-reading) #menuGrid :is(.settings-choice, .cm-chips) {
    border: 0;
    box-shadow: none;
    flex-wrap: wrap;
    gap: 3px;
}

body:not(.wiki-reading) #menuGrid :is(
    .settings-text, .settings-select, .custom-input, .mp-name-input,
    .mp-code-input, .mp-set-text, .mp-set-select, .mp-chat-input),
:is(.join-input, .reset-input, .recovery-input, .friends-input) {
    border: 1px solid var(--ui-line);
    border-bottom: 2px solid var(--accent-deep);
    background-color: var(--surface);
    color: var(--ink);
    box-shadow: none;
    font-family: var(--ui-face);
    font-size: 16px;
    min-height: 42px;
    max-width: 100%;
}
body:not(.wiki-reading) #menuGrid :is(button, input, select, a):focus-visible,
:is(.join-card, .badges-card, .mp-trade, .profile-menu, .friends-panel,
    .reset-card, .stats-notice) :is(button, input, select, a):focus-visible {
    outline: 3px solid var(--accent-deep);
    outline-offset: 3px;
}

body:not(.wiki-reading) #menuGrid :is(.about-title, .mp-title, .mp-lobby-title,
    .settings-section-title, .channel-brand, .ch-intro-name) {
    font-family: var(--ui-face);
    font-size: clamp(28px, 3.6vw, 48px);
    font-weight: 900;
    font-style: italic;
    letter-spacing: -.04em;
    line-height: 1.1;
    color: var(--ink);
    text-shadow: none;
    overflow-wrap: anywhere;
}
body:not(.wiki-reading) #menuGrid :is(.settings-section-title, .mp-home--bare .mp-title) {
    color: #fff;
    text-shadow: 0 2px 3px #000c;
}
body:not(.wiki-reading) #menuGrid :is(.settings-label, .custom-label,
    .cm-section-title, .cm-rule-label, .wch-panel-title, .mp-roomlist-title) {
    font-family: var(--ui-face);
    font-size: 18px;
    font-weight: 800;
    letter-spacing: .02em;
    text-shadow: none;
    line-height: 1.3;
}
body:not(.wiki-reading) #menuGrid :is(.settings-desc, .mp-subtitle, .ch-desc,
    .settings-note, .custom-err, .channel-error, .channel-loading) {
    font-size: 14px;
    line-height: 1.5;
    text-shadow: none;
}

/* Settings now fits the same visual scale as the storefront controls. */
.settings-container { max-width: min(1120px, calc(100vw - 40px)); }
.settings-screen { gap: 24px; }
.settings-tabs { width: 224px; top: calc(var(--chrome-h) + 20px); }
.settings-tab { padding: 14px 16px; }
.settings-row { padding: 20px; gap: 20px; flex-wrap: wrap; }
.settings-info { flex: 1 1 190px; min-width: 0; }
.settings-list { gap: 12px; }
.settings-section-title { margin-bottom: 10px; }
.settings-desc { text-transform: none; }
.settings-choice button { padding: 10px 12px; }
.settings-slider { max-width: 100%; }
.settings-subhead { border-bottom-color: var(--accent-deep); }

/* Shared dialog chrome, including inventory, achievements and recovery. */
:is(.badges-head, .profile-menu-head, .friends-head) {
    background: color-mix(in srgb, var(--surface-2) 88%, transparent);
    color: var(--ink);
    border-bottom: 1px solid var(--ui-line);
    text-shadow: none;
}
:is(.join-logo, .reset-logo, .badges-title, .profile-modal-name, .mp-trade-title) {
    font-family: var(--ui-face);
    font-style: italic;
    font-weight: 900;
    color: var(--ink);
    text-shadow: none;
}
:is(.join-btn, .reset-btn, .recovery-save, .mp-trade-btn, .inv-btn,
    .profile-modal-viewfull, .cm-fail-btn, .stats-notice-ok) {
    font-family: var(--ui-face);
    border: 1px solid var(--accent-deep);
    background: var(--ui-selected);
    color: #111;
    box-shadow: inset 0 -3px 0 #0002;
    text-shadow: none;
}
:is(.badges-close, .mp-trade-close, .profile-menu-item, .friends-close) {
    font-family: var(--ui-face);
    border-color: var(--ui-line);
    box-shadow: none;
}
.join-card, .reset-card { padding: 32px; }
.join-input { font-size: 20px; }
.join-checkerboard { background: repeating-conic-gradient(var(--accent) 0 25%, transparent 0 50%) 0 0 / 14px 14px; }

/* Results and the pre-race draw use the menu's dark banner palette. */
.mp-result-overlay, .arena-draw {
    background: radial-gradient(ellipse at 50% 0%, #283a60, #090f20 75%);
}
.mp-result-card { text-shadow: none; }
.mp-result-card .mp-lobby-row {
    background: #ffffff0d;
    border: 1px solid #ffffff30;
    color: #fff;
}
.mp-result-card :is(.mp-play-btn, .mp-back-btn) {
    font-family: var(--ui-face);
    font-size: 16px;
    border: 1px solid var(--accent-deep);
    background: var(--ui-selected);
    color: #111;
    box-shadow: none;
}
.arena-draw h2 { font-family: var(--ui-face); font-style: italic; }
.arena-reel { border-top: 3px solid var(--rarity, var(--accent)); }

@media (max-width: 900px) {
    .settings-screen { flex-direction: column; }
    .settings-tabs { position: static; width: 100%; flex-direction: row; flex-wrap: wrap; gap: 6px; }
    .settings-tab { flex: 1 1 150px; }
    .settings-list { width: 100%; }
}
@media (max-width: 600px) {
    .settings-row { padding: 16px; gap: 12px; }
    .settings-info { flex-basis: auto; }
    .settings-tab { flex-basis: 130px; padding: 10px; }
    .about-card { padding: 22px 18px; }
    .mp-home:not(.mp-home--bare) { padding: 24px 18px; }
    .mp-join-form { flex-wrap: wrap; }
    .wch-topbar { padding: 12px; }
    .wch-panel { padding: 16px 12px; }
    .wch-row { gap: 8px; padding: 10px 8px; flex-wrap: wrap; }
    .wch-player { min-width: 0; overflow-wrap: anywhere; }
    .join-card, .reset-card { padding: 28px 20px; }
}
@media (prefers-reduced-motion: reduce) {
    .settings-tab, .wch-tab, .wch-subtab { transition: none; }
}
body.gfx-potato :is(.settings-row, .about-card, .cm-section, .wch-panel,
    .badges-card, .join-card, .mp-trade) { box-shadow: none; }

body:not(.wiki-reading) #menuGrid :is(.settings-account-name, .settings-kbd,
    .challenge-pill, .mp-room-code, .mp-queue-seat, .mp-room-rule) {
    border: 1px solid var(--ui-line);
    background: var(--surface);
    color: var(--ink);
    box-shadow: none;
    font-size: 16px;
}
body:not(.wiki-reading) #menuGrid .settings-logout:not(.settings-rename):not(.settings-register) {
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 40%, transparent);
}
body:not(.wiki-reading) #menuGrid .ch-card {
    border: 1px solid var(--ui-line);
    background: var(--ui-panel);
    box-shadow: var(--ui-shadow);
    text-transform: uppercase;
}
body:not(.wiki-reading) #menuGrid .ch-card:hover {
    border-color: var(--ch-accent, var(--accent));
    box-shadow: 0 8px 24px #0003;
}
body:not(.wiki-reading) #menuGrid .ch-name { font-size: clamp(20px, 2vw, 28px); }
body:not(.wiki-reading) #menuGrid .ch-desc { text-transform: none; }
body:not(.wiki-reading) #menuGrid :is(.channel-loading, .channel-error) {
    border: 1px dashed var(--ui-line);
    color: var(--ink-muted);
    background: color-mix(in srgb, var(--surface) 70%, transparent);
    padding: 20px;
    text-align: center;
}
body:not(.wiki-reading) #menuGrid :is(.custom-divider, .challenge-eyebrow, .wch-record-value) {
    color: var(--ink);
}
/* Dialog content uses the same light/dark surfaces, while item art and rarity
   colours remain owned by the cosmetics renderer. */
:is(.badges-card, .mp-trade) :is(.badge-card:not(.is-secret), .inv-bundle, .mp-trade-side) {
    background: var(--surface);
    border: 1px solid var(--ui-line);
    box-shadow: none;
}
@media (max-width: 600px) {
    .settings-account { flex-shrink: 1; gap: 8px; max-width: 100%; }
    .settings-choice { max-width: 100%; flex-shrink: 1; }
    .cm-input-row .custom-input { min-width: 0; }
    .cm-presets { flex-wrap: wrap; }
    .cm-chips { max-width: 100%; }
}
/* These legacy variants used !important; keep the override scoped to settings. */
body:not(.wiki-reading) #menuGrid :is(.settings-register, .settings-rename) {
    background: var(--ui-selected) !important;
    color: #111;
}
@media (max-width: 600px) {
    /* The floating profile card reaches below the short header on phones. */
    .menu-stage:has(> :is(.settings-container, .about-container,
        .wikichannel-container, .custom-container, .challenges-container)) {
        padding-top: 140px;
    }
}

body:not(.wiki-reading) #menuGrid :is(.mp-lobby-left, .mp-lobby-right) {
    border: 1px solid var(--ui-line);
    border-top: 3px solid var(--accent);
    background: var(--ui-panel);
    box-shadow: var(--ui-shadow);
    padding: 12px;
}
body:not(.wiki-reading) #menuGrid .mp-lobby-right .mp-lobby-title {
    color: var(--ink);
    text-shadow: none;
    font-size: clamp(24px, 2.8vw, 38px);
}
body:not(.wiki-reading) #menuGrid .mp-set-label { font-size: 16px; }
body:not(.wiki-reading) #menuGrid .mp-set-desc {
    font-size: 13px;
    line-height: 1.4;
    color: var(--ink-muted);
    text-transform: none;
}
body:not(.wiki-reading) #menuGrid .mp-set-row { border-bottom-color: var(--ui-line); }
@media (max-width: 700px) {
    body:not(.wiki-reading) .mp-wide .mp-lobby-cols { grid-template-columns: minmax(0, 1fr); }
    body:not(.wiki-reading) .mp-wide :is(.mp-lobby-left, .mp-lobby-right) { height: auto; }
    body:not(.wiki-reading) .mp-wide .mp-set-panel { max-height: none; overflow: visible; }
    body:not(.wiki-reading) .mp-wide .mp-set-row { flex-wrap: wrap; }
    body:not(.wiki-reading) .mp-wide .mp-set-info { flex: 1 1 170px; }
    body.mp-room:not(.wiki-reading) .menu-stage { padding-top: 130px; }
    body:not(.wiki-reading) .mp-wide .mp-lobby-meta { position: static; transform: none; height: auto; }
    body:not(.wiki-reading) .mp-wide .mp-lobby-btns { position: static; height: auto; padding: 16px 0; }
}
.settings-slider { flex-wrap: wrap; }
.settings-slider-end { font-size: 11px; letter-spacing: .04em; }
.settings-slider input[type="range"] { width: 120px; min-width: 60px; accent-color: var(--accent-deep); }
.settings-slider-val {
    min-width: 52px;
    font-size: 14px;
    border: 1px solid var(--ui-line);
    color: var(--ink);
}

/* One font everywhere: Mono, wiki article pane included. !important on
   purpose: it has to beat every per-screen font-family rule without editing
   each one. */
*, *::before, *::after {
    font-family: 'Mono', Consolas, monospace !important;
}

/* A secret nobody has found yet: dark and dashed, so it reads as a sealed
   slot rather than an ordinary locked trophy with its text greyed out. */
.badge-card.is-secret {
    border-style: dashed;
    border-color: #4a4f58;
    background: #2b2e34;
}

.badge-card.is-secret .badge-name,
.badge-card.is-secret .badge-desc {
    color: #9aa1ab;
}

.preview-environment { position: fixed; top: 0; left: 50%; transform: translateX(-50%); z-index: 9999; margin: 0; padding: 5px 12px; background: #18233a; color: #fff; font-size: 12px; }

/* No shadows anywhere in the UI. */
*, *::before, *::after { box-shadow: none !important; text-shadow: none !important; }

/* ===========================
   THE ROOM UNDER THE TETR CHROME
   The room was built for the old flat header and footer bands: white panels,
   the code pinned into the header, Start pinned into the footer. Under the
   home-menu banner and infobar it is three dark plates like the Home buttons
   (fill, edge, racing stripes) filling all the space between the two. The
   code is in the banner's centre plate and Start in the infobar's centre
   (home-menu.js, Chrome.roomCode and Chrome.action).
   Sizes in vw because the banner and infobar are drawn in the window's width
   (their cqw is the window's). The banner's sides reach 5.7vw down, the
   infobar's corner tabs rise 5.6vw up.
   =========================== */
/* One gap (--mp-side) all round the three columns: the banner's visible bottom is
   5.677vw down, the infobar tabs' tops 5.6vw up, and the sides are the screen edge. */
body.wdm-on.mp-room:not(.wiki-reading) main { padding: 0 var(--mp-side, .5vw); }
body.wdm-on.mp-room:not(.wiki-reading) .menu-stage { padding: calc(5.677vw * var(--wdm-k, 1) + var(--mp-side, .5vw)) 0 calc(5.6vw * var(--wdm-k, 1) + var(--mp-side, .5vw)); }

/* Dark tokens for everything the room's panels already read, so the rows,
   inputs, selects and the article picker all follow without a rule each.
   The picker and an open select can be parented to <body>, hence listed. */
html:not(.theme-light) body.wdm-on.mp-room:not(.wiki-reading) :is(#menuGrid.mp-wide, .mp-select-menu, .wiki-picker) {
    --surface: #1c1c1c;
    --surface-2: #242424;
    --surface-3: #2e2e2e;
    --ink: #e2e2e2;
    --ink-strong: #f2f2f2;
    --ink-navy: #e2e2e2;
    --ink-mid: #cfcfcf;
    --ink-muted: #9a9a9a;
    --ink-faint: #7a7a7a;
    --ink-link: #f4a9cb;
    --danger: #ff8787;
    --bad: #ff8787;
    --panel-top: #1c1c1c;
    --panel-bottom: #161616;
    --theme-tint: #1c1c1c;
    --theme-tint2: #161616;
    --theme-card: #1c1c1c;
    --line: #2c2c2c;
    --ui-line: #2c2c2c;
    scrollbar-color: #3a3a3a transparent;
}

/* The same dark tokens for every other menu screen (Race vs Bot, custom match,
   settings, stats, challenges, ...) and the dialogs that sit over them
   (inventory, badges, profile, friends, notifications, trade). --ui-panel is
   a var() resolved on :root, so it is restated here or the tokens above would
   never reach it. Main menu and shop keep their own looks. */
html:not(.theme-light) body.wdm-on:not(.wiki-reading) :is(
    #menuGrid:not(.main-menu):not(.shop-container):not(.mp-wide),
    .join-card, .reset-card, .badges-card, .mp-trade, .profile-menu,
    .friends-panel, .notif-card, .stats-notice, .cm-fail-card, .rc-finish-card) {
    --surface: #1c1c1c;
    --surface-2: #242424;
    --surface-3: #2e2e2e;
    --ink: #e2e2e2;
    --ink-strong: #f2f2f2;
    --ink-navy: #e2e2e2;
    --ink-mid: #cfcfcf;
    --ink-muted: #9a9a9a;
    --ink-faint: #7a7a7a;
    --ink-link: #f4a9cb;
    --danger: #ff8787;
    --bad: #ff8787;
    --panel-top: #1c1c1c;
    --panel-bottom: #161616;
    --theme-tint: #1c1c1c;
    --theme-tint2: #161616;
    --theme-card: #1c1c1c;
    --line: #2c2c2c;
    --ui-line: #2c2c2c;
    --ui-panel: linear-gradient(145deg, #1c1c1c, #161616);
    --ui-shadow: 0 8px 24px #0006, inset 0 0 0 1px #ffffff12;
    /* Buttons and selections are the BACK button's red. */
    --accent: #d8302c;
    --accent-soft: #e4504c;
    --accent-deep: #a82320;
    --ui-selected: #d8302c;
    scrollbar-color: #3a3a3a transparent;
}

/* No accent stripe across the top of dark panels and dialogs. */
html:not(.theme-light) body.wdm-on:not(.wiki-reading) :is(
    #menuGrid:not(.main-menu):not(.shop-container):not(.mp-wide) :is(
        .settings-row, .about-card, .custom-form, .cm-section, .challenge-card,
        .challenge-daily-strip, .ch-intro, .wch-panel, .wch-topbar,
        .channel-topbar, .mp-home:not(.mp-home--bare), .mp-set-panel,
        .mp-chat, .mp-lobby-roster, .pp-tile, .wch-record),
    .join-card, .reset-card, .badges-card, .mp-trade, .profile-menu,
    .friends-panel, .notif-card, .stats-notice, .cm-fail-card, .rc-finish-card) {
    border-top: 1px solid var(--ui-line);
}
html:not(.theme-light) body.wdm-on:not(.wiki-reading) #menuGrid:not(.main-menu):not(.shop-container):not(.mp-wide) .settings-subhead {
    border-bottom-color: var(--ui-line);
}

/* White text on the red selections and buttons. */
html:not(.theme-light) body.wdm-on:not(.wiki-reading) :is(
    #menuGrid:not(.main-menu):not(.shop-container):not(.mp-wide),
    .join-card, .reset-card, .badges-card, .mp-trade, .profile-menu,
    .friends-panel, .notif-card, .stats-notice, .cm-fail-card, .rc-finish-card)
  :is(.settings-tab.is-active, .cm-chip.is-active, .cm-hazard.is-active,
    .inv-btn, .join-btn, .reset-btn, .recovery-save, .mp-trade-btn,
    .profile-modal-viewfull, .cm-fail-btn, .stats-notice-ok) {
    color: #fff;
}

/* Multiplayer's colour, the one the banner's title plate already wears. */
body.wdm-on.mp-room:not(.wiki-reading) :is(#menuGrid.mp-wide, .mp-select-menu, .wiki-picker) {
    --accent: #c8457f;
    --accent-soft: #f4a9cb;
    --accent-deep: #c8457f;
    --ui-selected: #c8457f;
}

/* The three columns: plates. */
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide :is(.mp-lobby-left, .mp-lobby-right, .mp-chat) {
    --d: linear-gradient(var(--wdm-deco), var(--wdm-deco));
    position: relative;
    padding: .8vw;
    border: 2px solid var(--wdm-edge);
    background:
        var(--wdm-fill);
    color: var(--ink);
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-lobby-cols { gap: .9vw; }
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-lobby-home { gap: .9vw; }

/* Inside the settings plate the panel is just the list, not a box in a box. */
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-set-panel {
    border: 0;
    background: none;
    padding: 0 .4vw;
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-set-row { border-bottom: 1px solid var(--wdm-deco); }

body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-lobby-right .mp-lobby-title {
    color: var(--ink);
    font-size: clamp(22px, 2vw, 38px);
    font-style: normal;
    letter-spacing: .02em;
}

/* Tabs: flat plates, the open one in the colour. */
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-set-tab {
    border: 0;
    background: var(--surface-2);
    color: var(--ink-muted);
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-set-tab:hover:not(.is-active) {
    background: var(--surface-3);
    color: var(--ink);
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-set-tab.is-active,
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-set-toggle.is-on {
    background: var(--accent);
    color: #fff;
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide :is(.mp-set-toggle, .mp-select-btn) {
    border: 1px solid var(--wdm-deco);
    background: var(--surface-2);
    color: var(--ink);
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide :is(.mp-set-toggle, .mp-select-btn):hover:not(:disabled) {
    border-color: var(--accent);
    background: var(--surface-3);
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-select-caret { color: var(--accent-soft); }
body.wdm-on.mp-room:not(.wiki-reading) .mp-select-option { border-bottom-color: var(--wdm-deco); }
body.wdm-on.mp-room:not(.wiki-reading) :is(.mp-select-option:hover, .mp-select-option:focus, .mp-select-option.is-selected) {
    background: var(--accent);
    color: #fff;
}

/* Roster rows. */
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-lobby-row {
    border: 0;
    border-left: 0; /* no accent stripe: it also pushed the banner scrim in from the edge */
    background: var(--surface-2);
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-lobby-row--empty {
    border: 1px dashed var(--wdm-deco);
    background: none;
    color: var(--ink-faint);
}
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-lobby-name { color: var(--ink); }
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-lobby-level { color: var(--ink-muted); }

/* Chat. */
body.wdm-on:not(.wiki-reading) #menuGrid.mp-wide .mp-chat-input {
    border: 1px solid var(--wdm-deco);
    border-bottom: 2px solid var(--accent);
    background: var(--surface-2);
    color: var(--ink);
}

/* Room highlights (active tab, toggles, selected options, hover borders, own
   row marker) take the back button's red instead of the player's accent. */
body.wdm-on.mp-room:not(.wiki-reading) #menuGrid.mp-wide,
body.wdm-on.mp-room:not(.wiki-reading) .mp-select-menu {
    --accent: #d8302c;
    --accent-deep: #a82320;
    --accent-soft: #f08a87;
}

/* Room sliders (rarity, etc.): the native control's accent is not --accent. */
body.wdm-on.mp-room:not(.wiki-reading) .mp-set-slider input { accent-color: #d8302c; }

/* ===========================
   BELL DRAWERS (SocialDrawers in script.js)
   Friends off the left edge, notifications off the right, each the full height
   of the window. Square, like the banner they hang off.
   =========================== */
.sd-scrim {
    position: fixed;
    inset: 0;
    z-index: 10040;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    transition: opacity 0.22s ease;
}
.sd-scrim.is-in { opacity: 1; }

.sd-drawer {
    position: fixed;
    top: 0;
    bottom: 0;
    width: min(380px, 88vw);
    z-index: 10041;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    background: #1a1a1a;
    color: #e2e2e2;
    transition: transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sd-left { left: 0; border-right: 4px solid #d8302c; transform: translateX(-102%); }
.sd-right { right: 0; border-left: 4px solid #d8302c; transform: translateX(102%); }
.sd-drawer.is-in { transform: none; }

.sd-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 18px 20px 14px;
    background: #303030;
    border-bottom: 2px solid #141414;
}
.sd-title { font-size: 20px; letter-spacing: 0.08em; text-transform: uppercase; }
.sd-close {
    appearance: none;
    border: 0;
    background: transparent;
    color: inherit;
    font-size: 28px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
}
.sd-close:hover { color: #d8302c; }
.sd-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 16px 20px; }
/* The friends list is drawn by the Friends module with its own classes; it
   scrolls with the drawer, so its inner scroller must not cap its height. */
.sd-friends { display: flex; flex-direction: column; }
.sd-friends .friends-panel-head { display: none; }
.sd-friends .friends-scroll { overflow: visible; }

.sd-drawer :is(.friends-add input, .friends-btn-sm) { border-radius: 0; }
.sd-drawer .friends-add input { background: #262626; border-color: #3a3a3a; color: #e2e2e2; }
.sd-drawer .friends-btn-sm { background: #d8302c; border-color: #d8302c; color: #fff; }
.sd-drawer .friends-btn-sm.is-quiet { background: transparent; border-color: #4a4a4a; color: #cfcfcf; }
.sd-drawer .friends-row { border-top-color: #2e2e2e; }
.sd-drawer .friends-dot { border-radius: 0; background: #555; }
.sd-drawer .friends-dot.is-online { background: #3fbf5f; box-shadow: none; }
.sd-drawer .friends-error { color: #ff7a6e; }

.sd-note { padding: 12px 14px; margin-bottom: 8px; background: #242424; border-left: 4px solid #3a3a3a; }
.sd-note.is-new { background: #2c2424; border-left-color: #d8302c; }
.sd-note-top { display: flex; justify-content: space-between; gap: 10px; }
.sd-note-title { font-size: 14px; font-weight: 700; }
.sd-note-time { font-size: 11px; opacity: 0.55; flex: 0 0 auto; }
.sd-note-body { font-size: 13px; line-height: 1.45; opacity: 0.85; margin-top: 4px; }
.sd-empty { font-size: 13px; line-height: 1.5; opacity: 0.6; }

html.theme-light .sd-drawer { background: #f4f2ee; color: #2c2b29; }
html.theme-light .sd-head { background: #e6e3de; border-bottom-color: #a9a6a0; }
html.theme-light .sd-drawer .friends-add input { background: #fff; border-color: #a9a6a0; color: #2c2b29; }
html.theme-light .sd-drawer .friends-btn-sm.is-quiet { border-color: #a9a6a0; color: #2c2b29; }
html.theme-light .sd-drawer .friends-row { border-top-color: #d6d3cd; }
html.theme-light .sd-drawer .friends-dot { background: #bbb; }
html.theme-light .sd-note { background: #e9e6e1; border-left-color: #c8c5bf; }
html.theme-light .sd-note.is-new { background: #f1e3e1; border-left-color: #d8302c; }

body.wiki-reading :is(.sd-scrim, .sd-drawer) { display: none; }
