:root {
    --primary-color: #00ff9d;
    --secondary-color: #00cc8f;
    --background-color: #000000;
    --text-color: #c8ccd0;
    --error-color: #ca4754;
    --correct-color: #00ff9d;
    --neutral-color: #646669;
    --caret-color: #00ff9d;
    --sub-color: #8b8e92;
    /* Panel and control outlines. Derived from the neutral so it tracks each
       theme, rather than being a sixth colour to keep in step by hand. */
    --border-color: color-mix(in srgb, var(--neutral-color) 55%, transparent);
    /* Unified top offset below fixed header/toggle */
    /* Reserved height for the top advertising banner, and the amount the
       header moves down when one is shown. --ad-banner-h is set on the root
       element by mountTopAdBanner and is 0px whenever the banner is hidden,
       so nothing below has to know whether a banner exists. */
    --ad-banner-height: 3.75rem;
    --ad-banner-h: 0px;
    /* Set from the measured top rail by renderTopRail, because the rail is
       one row signed in and two signed out. The fallback covers the moment
       before the first measurement lands. */
    --top-rail-h: 2.5rem;
    /* Where content starts. The banner sits beside the rail rather than above
       it, so this clears whichever of the two is taller, not their sum. */
    --header-offset: calc(1rem + max(var(--top-rail-h), var(--ad-banner-h)) + 0.75rem);
    /* App padding variable so we can compensate where needed */
    --app-padding: 1rem;
    /* Room at the foot of every screen for the fixed footer and the corner
       rails, so normal-flow content never ends up underneath them. */
    --bottom-chrome: 5.5rem;

    /* Controls and the surfaces they sit on. The stylesheet had five radii
       in use (0, 2, 4, 6, 8 and 999px) with no rule for which applied where,
       so a button and its own input looked like different components. */
    --radius: 4px;
    --radius-panel: 8px;
    --radius-pill: 999px;
    /* One border weight. Buttons were 2px and inputs 1px, so a field and the
       button next to it read as different weights of the same idea. The two
       start buttons keep 2px, where it is doing real work. */
    --border-width: 1px;
    --record-color: #ffd166;
    --warning-color: #ffb454;
    --player-2: #4ea3ff;
    --player-3: #ff9f43;
    --player-4: #c678dd;
    --player-5: #ff6b8a;
}

[data-theme="light"] {
    --primary-color: #067a4e;
    --secondary-color: #05663f;
    --background-color: #f6f6f4;
    --text-color: #2b2b2b;
    --error-color: #b3323f;
    --correct-color: #067a4e;
    --neutral-color: #8a8d90;
    --caret-color: #067a4e;
    --sub-color: #5c5f61;
    --record-color: #9a6b00;
    --warning-color: #a35b00;
    --player-2: #1667c2;
    --player-3: #b45309;
    --player-4: #7c3aed;
    --player-5: #be123c;
}

[data-theme="dark"] {
    --primary-color: #00ff9d;
    --secondary-color: #00cc8f;
    --background-color: #000000;
    --text-color: #c8ccd0;
    --error-color: #ca4754;
    --correct-color: #00ff9d;
    --neutral-color: #646669;
    --caret-color: #00ff9d;
    --sub-color: #8b8e92;
    --record-color: #ffd166;
    --warning-color: #ffb454;
    --player-2: #4ea3ff;
    --player-3: #ff9f43;
    --player-4: #c678dd;
    --player-5: #ff6b8a;
}

[data-theme="dracula"] {
    --primary-color: #bd93f9;
    --secondary-color: #ff79c6;
    --background-color: #282a36;
    --text-color: #f8f8f2;
    --error-color: #ff5555;
    --correct-color: #bd93f9;
    --neutral-color: #6272a4;
    --caret-color: #bd93f9;
    --sub-color: #9aa5c4;
    --record-color: #ffd166;
    --warning-color: #ffb454;
    --player-2: #4ea3ff;
    --player-3: #ff9f43;
    --player-4: #c678dd;
    --player-5: #ff6b8a;
}

[data-theme="nord"] {
    --primary-color: #88c0d0;
    --secondary-color: #81a1c1;
    --background-color: #2e3440;
    --text-color: #d8dee9;
    --error-color: #bf616a;
    --correct-color: #88c0d0;
    --neutral-color: #4c566a;
    --caret-color: #88c0d0;
    --sub-color: #93a1b8;
    --record-color: #ffd166;
    --warning-color: #ffb454;
    --player-2: #4ea3ff;
    --player-3: #ff9f43;
    --player-4: #c678dd;
    --player-5: #ff6b8a;
}

[data-theme="solarized"] {
    --primary-color: #268bd2;
    --secondary-color: #2aa198;
    --background-color: #002b36;
    --text-color: #93a1a1;
    --error-color: #dc322f;
    --correct-color: #268bd2;
    --neutral-color: #586e75;
    --caret-color: #268bd2;
    --sub-color: #839496;
    --record-color: #ffd166;
    --warning-color: #ffb454;
    --player-2: #4ea3ff;
    --player-3: #ff9f43;
    --player-4: #c678dd;
    --player-5: #ff6b8a;
}

[data-theme="gruvbox"] {
    --primary-color: #fabd2f;
    --secondary-color: #b8bb26;
    --background-color: #282828;
    --text-color: #ebdbb2;
    --error-color: #fb4934;
    --correct-color: #fabd2f;
    --neutral-color: #928374;
    --caret-color: #fabd2f;
    --sub-color: #bdae93;
    --record-color: #ffd166;
    --warning-color: #ffb454;
    --player-2: #4ea3ff;
    --player-3: #ff9f43;
    --player-4: #c678dd;
    --player-5: #ff6b8a;
}

[data-theme="monokai"] {
    --primary-color: #a6e22e;
    --secondary-color: #66d9ef;
    --background-color: #272822;
    --text-color: #f8f8f2;
    --error-color: #f92672;
    --correct-color: #a6e22e;
    --neutral-color: #75715e;
    --caret-color: #a6e22e;
    --sub-color: #b0a98d;
    --record-color: #ffd166;
    --warning-color: #ffb454;
    --player-2: #4ea3ff;
    --player-3: #ff9f43;
    --player-4: #c678dd;
    --player-5: #ff6b8a;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

[hidden] {
    display: none !important;
}

body {
    margin: 0;
    padding: 0;
    font-family: 'JetBrains Mono', monospace;
    background-color: var(--background-color);
    color: var(--text-color);
    height: 100vh;
    /* Vertical scroll is allowed (and only appears when actually needed) --
       a fixed "overflow: hidden" here silently clipped any screen whose
       content got taller than the viewport (a wide-but-short window, a
       longer passage, extra stat rows) with literally no way to reach the
       clipped part. Horizontal stays clipped since nothing should ever
       need to scroll sideways. */
    overflow-x: hidden;
    overflow-y: auto;
    /* Cursor is set per-theme in JS (theme.js) -- a hardcoded green crosshair
       here would look wrong on Dracula/Nord/etc. */
}

.app {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start; /* avoid vertical centering that pushes content down */
    height: 100vh;
    padding: var(--app-padding);
    overflow: hidden;
}

/* The mark and wordmark together, fixed at the top-left of every screen. */
.logo {
    position: fixed;
    top: 1rem;
    left: 1rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1.15rem;
    font-family: 'JetBrains Mono', monospace;
    text-transform: uppercase;
    letter-spacing: 2px;
    cursor: pointer;
    line-height: 1;
}

.logo-mark {
    width: 1.5em;
    height: 1.5em;
    flex-shrink: 0;
    overflow: visible;
}

.logo-mark-chevron {
    stroke: var(--primary-color);
    fill: none;
}

.logo-mark-cursor {
    fill: var(--primary-color);
}

/* Plain inline, not a flex container. As inline-flex the two halves became
   flex items, which blockifies them, and the name read as "TYPER" then
   "PUNK" on separate lines to anything using innerText: a screen reader, a
   copy-paste, or a test. */
.logo-word {
    display: inline;
    white-space: nowrap;
}

.logo-word-a {
    color: var(--text-color);
}

.logo-word-b {
    color: var(--primary-color);
}

.logo:hover .logo-word-a {
    color: var(--primary-color);
}

.logo:hover .logo-word-b {
    color: var(--secondary-color, var(--primary-color));
}

/* The cursor blinks, at the same rate as the typing caret, because it is the
   same idea. Anyone who has asked not to see motion gets it held still. */
@keyframes logo-cursor-blink {
    0%, 50% { opacity: 1; }
    50.01%, 100% { opacity: 0; }
}

/* Only the menu's own title blinks. The corner mark is on every screen, and
   something blinking in the corner of every screen is a distraction rather
   than a flourish. */
.logo-hero .logo-mark-cursor {
    animation: logo-cursor-blink 1.06s steps(1, end) infinite;
}

@media (prefers-reduced-motion: reduce) {
    .logo-hero .logo-mark-cursor {
        animation: none;
    }
}

.logo:hover {
    color: var(--secondary-color);
}

/* Main Menu */
.main-menu {
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Pinned near the top rather than vertically centered -- centered left
       barely more room below the Start button than the mode popover needs,
       so on anything but a tall viewport the list ran past the bottom edge.
       Fixed higher, there's always real room underneath it. */
    justify-content: flex-start;
    /* Plus the banner, which is fixed over the top of the page. Without the
       addition the title cleared it by 8px. */
    padding-top: calc(6rem + var(--ad-banner-h));
    min-height: 100vh;
    width: 100%;
    max-width: 700px;
    margin: 0 auto;
    padding-left: 2rem;
    padding-right: 2rem;
    padding-bottom: 2rem;
    overflow-y: auto;
    box-sizing: border-box;
}

/* The menu's own title is the full lockup at size. It is not fixed to the
   corner like the one every other screen carries: the menu had both, so the
   name appeared twice on the same screen. */
.logo-corner {
    cursor: default;
    opacity: 0.55;
}

.logo-corner:hover .logo-word-a {
    color: var(--text-color);
}

.logo-corner:hover .logo-word-b {
    color: var(--primary-color);
}

.logo-hero {
    position: static;
    /* Sized to the buttons beneath it rather than to nothing in particular:
       the menu buttons are 250px wide at 1.2rem, and this sits above them. */
    font-size: 2.5rem;
    gap: 0;
    margin-bottom: 2rem;
    cursor: default;
    /* Three columns, the third empty and the same width as the first. That
       puts the wordmark in the exact centre of the row with the mark to its
       left, which is what has to line up with the buttons underneath.
       Centring the mark and the name together as one flex box put the name
       16px right of everything below it. */
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    width: 100%;
}

.logo-hero .logo-mark {
    grid-column: 1;
    justify-self: end;
    margin-right: 0.4em;
    width: 1.05em;
    height: 1.05em;
}

.logo-hero .logo-word {
    grid-column: 2;
}

.logo-hero:hover .logo-word-a {
    color: var(--text-color);
}

.logo-hero:hover .logo-word-b {
    color: var(--primary-color);
}

.settings-row-group {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem;
}

.settings-row-group .menu-button.small {
    width: 230px;
}

.menu-stat .stat-label {
    font-size: 0.7rem;
    color: var(--sub-color);
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 0.2rem;
}

.menu-stat .stat-value {
    font-size: 1.3rem;
    color: var(--primary-color);
    font-weight: bold;
}

/* Stats Screen */
.stats-screen {
    width: 100%;
    max-width: 700px;
    margin: 0 auto;
    padding: 2rem;
    margin-top: calc(var(--header-offset) - var(--app-padding));
    min-height: calc(100vh - var(--header-offset));
    box-sizing: border-box;
    padding-bottom: var(--bottom-chrome);
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Vertically centred when the content is short -- these screens were
       top-heavy, ending around y=510 on a 900px viewport with the rest of
       the page empty. "safe" keeps it flex-start once the content is taller
       than the viewport, so a long list is never cut off above the scroll
       origin; browsers that do not know the keyword ignore the declaration
       and keep the old flex-start behaviour. */
    justify-content: safe center;
    overflow-y: auto;
}

.stats-screen h2 {
    font-size: 1.8rem;
    color: var(--primary-color);
    text-transform: uppercase;
    letter-spacing: 2px;
    margin: 1.5rem 0 1.5rem;
}

.stats-screen h3 {
    font-size: 1rem;
    color: var(--sub-color);
    text-transform: uppercase;
    letter-spacing: 2px;
    align-self: flex-start;
    margin: 2rem 0 0.75rem;
}

.stats-summary {
    width: 100%;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1.5rem;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 1.25rem 1rem;
    text-align: center;
}

/* Centred, not flex-start. These screens are a centred column, so a
   left-hugging empty state sat out of line with the button underneath it --
   most visible on Friends, where "Sign in to add friends" and its Sign In
   button were on two different axes. */
.stats-empty {
    color: var(--sub-color);
    font-size: 0.9rem;
    align-self: center;
    text-align: center;
    max-width: 32rem;
}

.stats-pb-list {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.stats-pb-row {
    display: flex;
    align-items: center;
    gap: 1rem;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
    padding: 0.6rem 1rem;
}

.stats-pb-mode {
    flex: 1;
    text-align: left;
}

.stats-pb-wpm {
    color: var(--primary-color);
    font-weight: bold;
}

.stats-pb-date {
    color: var(--sub-color);
    font-size: 0.8rem;
    min-width: 90px;
    text-align: right;
}

.leaderboard-modes {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.4rem;
    margin-bottom: 0.5rem;
}

/* No width of its own, so it shrink-wrapped to 498px inside the 636px
   content column and the table sat narrower than everything above it. */
.leaderboard-results {
    width: 100%;
}

.leaderboard-list {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.leaderboard-row {
    display: flex;
    align-items: center;
    gap: 1rem;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
    padding: 0.6rem 1rem;
}

.leaderboard-rank {
    color: var(--sub-color);
    min-width: 2.5rem;
}

.leaderboard-name {
    flex: 1;
    text-align: left;
    /* A <button> now (links to the player's public profile) -- reset away
       the browser's default button chrome so it still just reads as text. */
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    /* Vertical padding rather than none: at 21px tall this was under the
       24x24 minimum target size, and it is the only way to reach a player's
       profile. The negative inline margin keeps the text where it was so the
       column still lines up with the rows above and below. */
    padding: 0.25rem 0.35rem;
    margin-left: -0.35rem;
    border-radius: var(--radius);
    cursor: pointer;
}

.leaderboard-name:hover {
    color: var(--primary-color);
    text-decoration: underline;
}

.leaderboard-wpm {
    color: var(--primary-color);
    font-weight: bold;
}

.leaderboard-acc {
    color: var(--sub-color);
    min-width: 3.5rem;
    text-align: right;
}

.leaderboard-date {
    color: var(--sub-color);
    font-size: 0.8rem;
    min-width: 90px;
    text-align: right;
}

.store-swatch {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: var(--radius);
    border: var(--border-width) solid var(--neutral-color);
}

.store-flair-swatch {
    color: var(--caret-color);
    padding: 0.25rem;
}

.store-flair-swatch svg {
    width: 100%;
    height: 100%;
}

/* Marks a result typed on a touchscreen, which is not comparable with a
   desktop one. It sat in the same grey as ordinary secondary text, so the
   caveat read as decoration -- this is the second and last use of
   --warning-color. */
.leaderboard-device-badge {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--warning-color);
    border: 1px solid color-mix(in srgb, var(--warning-color) 45%, transparent);
    border-radius: var(--radius);
    padding: 0.1rem 0.35rem;
}

.leaderboard-flair {
    display: inline-flex;
    width: 0.9em;
    height: 0.9em;
    margin-left: 0.35em;
    vertical-align: -0.05em;
    color: var(--caret-color);
}

.leaderboard-flair svg {
    width: 100%;
    height: 100%;
}

.stats-placeholder {
    width: 100%;
    border: 1px dashed var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 2rem 1rem;
    text-align: center;
    color: var(--sub-color);
    font-size: 0.85rem;
}

.stats-chart {
    width: 100%;
    height: 200px;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 0.5rem;
    box-sizing: border-box;
}

.stats-chart canvas {
    width: 100%;
    height: 100%;
    display: block;
}

.stats-screen .menu-button {
    margin-top: 2rem;
}

.menu-options {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
}

.start-group {
    position: relative;
    /* Fixed, not content-sized -- a content-sized button changed width (and
       shifted everything else in the centered column sideways with it)
       every time you picked a mode with a longer or shorter name. */
    width: 360px;
}

/* Sits directly above the Start button it describes, replacing the old
   bottom-right corner hint -- the keyboard shortcut is more useful read
   right next to the button it triggers than in a corner of the screen. */
/* Fixed width via .start-group above -- ellipsis covers a very long custom
   text name that would otherwise force the box wider. The chevron overlays
   the button's own right edge rather than squeezing the label into a
   smaller box, so padding-right reserves space for it -- without this, a
   long mode name ("Start: Literature") ran its text straight under the
   chevron instead of stopping short of it. */
/* Bigger and bolder than the other menu buttons -- now that its label is
   just the mode name ("Timed", not "Start: Timed"), its size has to carry
   "this is the one you click" on its own instead of the word "Start". */
/* The two entry points. Single-line labels naming the action, nothing else
   inside the box -- what a Single Player run will actually start (mode plus
   its rules) sits in the .mode-bar below, so the button reads as one clear
   word and the mode stays changeable without redrawing the button. */
.start-main {
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Symmetric. Nothing overlays either button now that the mode picker is
       its own control above them, so both labels centre on the same axis. */
    padding-left: 2rem;
    padding-right: 2rem;
    font-size: 1.5rem;
    padding-top: 1.3rem;
    padding-bottom: 1.3rem;
    border-width: 2px;
    letter-spacing: 2px;
}

/* Mode + rules for the Single Player button. Hidden (not removed) while the
   mode popover is open, so the layout does not jump. */
.start-caption {
    margin-top: 0.6rem;
    font-size: 0.85rem;
    color: var(--sub-color);
    text-transform: uppercase;
    letter-spacing: 1px;
    text-align: center;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Split button: most of the width starts the game, the chevron on the right
   edge (overlaid, not squeezing the label) opens the mode list. Mode used to
   be its own separate full-width button -- merged here since Start already
   displays the current mode in its own label ("Start: Timed"). */
/* The mode picker opens from the Single Player button itself: choosing what
   to type and starting it are one action. This control has been through a
   chevron notched into that button, a caption between the two buttons, a pill
   above them and a chip row below them -- as the button's own menu it needs
   no separate real estate at all, and the grouped two-column list stays
   scannable. */





/* What Start will actually generate, visible without opening Settings. */
/* Every mode listed as a direct pick when the button is clicked, instead of
   cycling blind through them one click at a time -- Custom and the text
   categories were otherwise invisible unless you happened to click through
   the whole list first.

   position:fixed with JS-computed coordinates (not CSS position:absolute
   below the button) -- with 8 modes listed, the panel is taller than the
   gap between the button and the bottom of the viewport on an ordinary
   900px-tall screen, and a fixed "opens downward" placement ran the last
   1-2 items off the bottom edge with no way to reach them. JS picks
   whichever of above/below actually has room, and this only falls back to
   its own internal scroll (never the page's) if neither direction has
   enough space at all. */
/* Just the topic packs now -- the generated modes are chips in .mode-bar, so
   this is one column of eleven rather than the 585px wall of eighteen mixed
   entries it used to be. */
.mode-popover {
    position: fixed;
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    gap: 0.5rem;
    background: var(--background-color);
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 0.5rem;
    z-index: 10;
    max-height: calc(100vh - 2rem);
    overflow-y: auto;
}

.mode-popover-group {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    flex: 1 1 0;
    min-width: 0;
}

/* Same small dim caption as the Settings panel's "Rules"/"Typing" headings,
   so the two panels read as the same kind of surface. */
.mode-popover-heading {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--sub-color);
    padding: 0.25rem 0.7rem 0.35rem;
    border-bottom: 1px solid var(--neutral-color);
    margin-bottom: 0.25rem;
}

.mode-popover-item {
    background: none;
    border: none;
    color: var(--text-color);
    font-family: inherit;
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    padding: 0.45rem 0.7rem;
    border-radius: var(--radius);
    cursor: pointer;
    text-align: left;
    flex-shrink: 0;
}

.mode-popover-item:hover {
    background: rgba(128, 128, 128, 0.15);
}

.mode-popover-item.active {
    color: var(--primary-color);
    font-weight: bold;
}

.menu-button {
    background: none;
    border: var(--border-width) solid var(--primary-color);
    border-radius: var(--radius);
    color: var(--text-color);
    padding: 1rem 2rem;
    font-size: 1.2rem;
    cursor: pointer;
    transition: all 0.2s ease;
    font-family: inherit;
    text-transform: uppercase;
    letter-spacing: 2px;
    min-width: 250px;
    /* min-width has no effect on an inline element -- a plain <button>
       defaults to inline-block already, but this class is also used on the
       Spotify "Connect" <a> link (an OAuth login has to be a real page
       navigation, not a fetch), which needs these spelled out explicitly. */
    display: inline-block;
    text-align: center;
    text-decoration: none;
    box-sizing: border-box;
}

.menu-button:hover {
    background-color: var(--primary-color);
    color: var(--background-color);
}

.menu-button.small {
    padding: 0.6rem 1.2rem;
    font-size: 0.9rem;
    min-width: 0;
}

/* Secondary actions. There used to be a .ghost modifier here as well, which
   rendered identically to .quiet once the hierarchy was defined, so the two
   were the same control under two names. .quiet is the one with a stated
   role, and is defined with the rest of the hierarchy below. */

/* Left aligned, and as wide as the panel. Centred at 340px inside a 480px
   panel it broke into three ragged lines that were harder to read than the
   sentence deserved. */
.custom-hint {
    font-size: 0.8rem;
    line-height: 1.5;
    color: var(--sub-color);
    text-align: left;
    width: 100%;
}

.settings-panel {
    width: 100%;
    max-width: 620px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.55rem;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 0.85rem;
    margin: 1rem auto 0 auto;
    text-align: center;
}

/* The settings controls are buttons, but .quiet removes their border, so a
   panel of them read as a list of plain text labels with no sign that any of
   it could be clicked. Inside this panel they keep an outline. */
.settings-panel .menu-button.quiet,
.custom-panel .menu-button.quiet {
    border-color: var(--border-color);
    color: var(--text-color);
}

.settings-panel .menu-button.quiet:hover,
.custom-panel .menu-button.quiet:hover {
    border-color: var(--primary-color);
}

/* The checkbox was the browser's default control in a panel where nothing
   else was. */
.settings-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.85rem;
    color: var(--sub-color);
    cursor: pointer;
}

.settings-row input[type="checkbox"] {
    appearance: none;
    width: 1rem;
    height: 1rem;
    margin: 0;
    /* A dark border on a dark panel was invisible. This is a control, so it
       has to look like one before it is checked, not only after. */
    border: var(--border-width) solid var(--sub-color);
    border-radius: 2px;
    background: transparent;
    cursor: pointer;
    flex-shrink: 0;
}

.settings-row input[type="checkbox"]:checked {
    background: var(--primary-color);
    border-color: var(--primary-color);
}

.settings-row:hover input[type="checkbox"] {
    border-color: var(--primary-color);
}

.settings-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.9rem;
    cursor: pointer;
}

.settings-hint {
    font-size: 0.75rem;
    color: var(--neutral-color);
    max-width: 340px;
}

.theme-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
    gap: 0.5rem;
    width: 100%;
}

.theme-swatch {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35rem;
    background: transparent;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
    padding: 0.5rem;
    cursor: pointer;
    font-family: inherit;
}

.theme-swatch.active {
    border-color: var(--primary-color);
}

.theme-swatch-preview {
    display: block;
    width: 100%;
    height: 1.5rem;
    border-radius: var(--radius);
}

.theme-swatch-label {
    font-size: 0.7rem;
    color: var(--text-color);
}

.theme-swatch-preview[data-swatch="dark"] { background: linear-gradient(90deg, #000000 60%, #00ff9d 60%); }
.theme-swatch-preview[data-swatch="light"] { background: linear-gradient(90deg, #f6f6f4 60%, #067a4e 60%); }
.theme-swatch-preview[data-swatch="dracula"] { background: linear-gradient(90deg, #282a36 60%, #bd93f9 60%); }
.theme-swatch-preview[data-swatch="nord"] { background: linear-gradient(90deg, #2e3440 60%, #88c0d0 60%); }
.theme-swatch-preview[data-swatch="solarized"] { background: linear-gradient(90deg, #002b36 60%, #268bd2 60%); }
.theme-swatch-preview[data-swatch="gruvbox"] { background: linear-gradient(90deg, #282828 60%, #fabd2f 60%); }
.theme-swatch-preview[data-swatch="monokai"] { background: linear-gradient(90deg, #272822 60%, #a6e22e 60%); }

/* A dialog says what it is. Both of these opened with no title at all. */
.panel-title {
    font-size: 1rem;
    color: var(--text-color);
    text-transform: uppercase;
    letter-spacing: 2px;
    text-align: left;
}

.custom-panel {
    width: 100%;
    max-width: 480px;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 1rem;
    margin: 1.5rem auto 0 auto;
}

/* Both of these are dialogs, and neither was built as one.
   
   They sat in the menu's normal flow, below the Start buttons, which put the
   settings panel at y=506 with a height of 437 on a 900px viewport: it ran
   140px off the bottom, and .app is overflow:hidden so there was nothing to
   scroll. Worse, neither had a background, so the fixed footer links rendered
   through the middle of them and, being higher in the stacking order,
   swallowed the clicks. Playwright could not press the settings panel's own
   Close button: the footer intercepted every attempt.

   Centred, opaque, above the fixed chrome, and scrolling internally when the
   content is taller than the screen. */
.settings-panel:not([hidden]),
.custom-panel:not([hidden]) {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 120;
    margin: 0;
    max-height: min(82vh, 44rem);
    overflow-y: auto;
    background: var(--background-color);
    border-color: var(--border-color);
    box-sizing: border-box;
    /* Dims the page behind without needing a separate backdrop element. */
    box-shadow: 0 0 0 100vmax color-mix(in srgb, var(--background-color) 78%, transparent);
}

/* The settings controls are buttons, but .quiet removes their border, so a
   panel of them read as a list of plain text labels with nothing to suggest
   any of it could be clicked. Inside these panels they keep an outline. */
.settings-panel .menu-button.quiet,
.custom-panel .menu-button.quiet,
.rail-settings-panel .menu-button.quiet {
    border-color: var(--border-color);
    color: var(--text-color);
}

.settings-panel .menu-button.quiet:hover,
.custom-panel .menu-button.quiet:hover,
.rail-settings-panel .menu-button.quiet:hover {
    border-color: var(--primary-color);
    color: var(--primary-color);
}

.custom-textarea {
    width: 100%;
    min-height: 120px;
    resize: vertical;
    background: transparent;
    color: var(--text-color);
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
    padding: 0.75rem;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9rem;
    transition: border-color 0.15s ease;
}

.custom-textarea.drag-over {
    border-color: var(--primary-color);
    border-style: dashed;
}

.custom-stats {
    font-size: 0.8rem;
    color: var(--sub-color);
    text-align: center;
}

.custom-panel-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.file-label {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

.custom-file-name {
    font-size: 0.8rem;
    color: var(--neutral-color);
    word-break: break-all;
}

.custom-error {
    font-size: 0.8rem;
    color: var(--error-color);
    min-height: 1em;
}

/* Account screen */
.account-panel {
    width: 100%;
    max-width: 360px;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin: 1rem auto 0 auto;
}

.account-tabs {
    display: flex;
    gap: 0.5rem;
    justify-content: center;
}

.account-form {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.account-input {
    background: transparent;
    color: var(--text-color);
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
    padding: 0.65rem 0.75rem;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9rem;
}

.account-input:focus {
    outline: none;
    border-color: var(--primary-color);
}

.account-signed-in {
    text-align: center;
    font-size: 1rem;
}

.account-spotify-status {
    font-size: 0.85rem;
    color: var(--sub-color);
    text-align: center;
}

/* Typing Game */
.typing-game {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    max-width: 1300px;
    margin: 0 auto;
    padding: 1rem;
    position: relative;
    height: 100vh;
    overflow: hidden;
}

.text-container {
    position: relative;
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem 2rem 0 2rem;
    margin-top: var(--header-offset); /* unified: just below header/toggle */
}

.text-display {
    font-family: 'JetBrains Mono', monospace;
    font-size: 1.5rem;
    line-height: 1.5;
    min-height: 6rem;
    color: var(--text-color);
    position: relative;
    white-space: pre-wrap;
    word-break: keep-all;
    overflow-wrap: break-word;
    /* A whole number of characters wide, not fit-content. When the passage
       wraps, fit-content resolves to the full available width -- which was
       78.9 character cells, so the leftover 0.9 cell could never be filled
       and always sat on the right. The box measured perfectly centred while
       the text inside it read as right-heavy. An exact character count means
       a full line reaches both edges. (border-box is global, hence the
       padding in the calc.) */
    width: min(100%, calc(76ch + 2rem));
    margin: 0 auto;
    tab-size: 4;
    hyphens: none;
    text-align: left;
    padding: 1rem;
}

.text-display span {
    white-space: pre;
    position: relative;
    display: inline-block;
}

.text-display span.correct {
    color: var(--correct-color);
}

.text-display span.incorrect {
    color: var(--error-color);
}

.text-display span.neutral {
    color: var(--neutral-color);
}

/* Blind Mode: a typed character just looks "passed", no red/green judgment
   -- distinct from .neutral (not yet reached) so the caret's progress is
   still visible, just without correctness feedback. */
.text-display span.typed {
    color: var(--text-color);
}

.text-display span.current {
    position: relative;
}

.text-display span.current::after {
    content: '▏';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    color: var(--caret-color);
    animation: blink 1s step-end infinite;
}

.typing-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    z-index: 1;
    cursor: text;
    caret-color: transparent;
    background: transparent !important;
    color: transparent !important;
    border: none !important;
    outline: none !important;
    box-shadow: none !important;
}

.typing-input:focus {
    background: transparent !important;
    color: transparent !important;
    border: none !important;
    outline: none !important;
    box-shadow: none !important;
}

.theme-toggle {
    position: fixed;
    top: 1rem;
    right: 1rem;
    background: transparent;
    border: var(--border-width) solid var(--primary-color);
    outline: none;
    box-shadow: none;
    color: var(--primary-color);
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    cursor: pointer;
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
    border-radius: var(--radius);
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
}

.theme-toggle:focus,
.theme-toggle:active {
    outline: none;
    box-shadow: none;
}

.theme-toggle:hover {
    background-color: var(--primary-color);
    color: var(--background-color);
}

/* Wrapper for themeButton.js's standalone control (every screen besides the
   main menu, which builds its own .theme-corner-group inline). Fixed itself
   so its child button can drop to position:static and still lay out
   correctly as the popover's positioned ancestor -- same reasoning as
   .corner-rail-left .corner-icon-button. */
/* Kept fixed for any caller that still mounts it standalone; inside .top-rail
   it drops to static so the row lays it out (see .top-rail .theme-control). */
.theme-control {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 100;
}

.theme-control .theme-toggle {
    position: static;
}

/* Same box as .theme-toggle, sitting just to its left -- Tab already
   restarts the current test, but that shortcut isn't discoverable on its
   own, so it gets a visible button too. */
.restart-toggle {
    background: transparent;
    border: var(--border-width) solid var(--primary-color);
    outline: none;
    box-shadow: none;
    color: var(--primary-color);
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    cursor: pointer;
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
    border-radius: var(--radius);
    font-size: 1.3rem;
    line-height: 1;
    appearance: none;
}

.restart-toggle:hover {
    background-color: var(--primary-color);
    color: var(--background-color);
}

/* Zen has no timer and no fixed length, so unlike every other mode nothing
   ever ends it automatically -- this is the only way to finish one, so it
   gets a real labeled button (not just another small icon square) sitting
   in the same top-right cluster as Restart/Theme. Deliberately not placed
   near TIME at bottom-center: that space is reserved to stay visually
   consistent with the end screen's graph position, not a place to add
   controls. */
.zen-finish-button {
    background: transparent;
    border: var(--border-width) solid var(--primary-color);
    outline: none;
    box-shadow: none;
    color: var(--primary-color);
    height: 2.5rem;
    padding: 0 1rem;
    cursor: pointer;
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
    border-radius: var(--radius);
    font-family: inherit;
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    appearance: none;
}

.zen-finish-button:hover {
    background-color: var(--primary-color);
    color: var(--background-color);
}

.theme-toggle svg {
    width: 1.5rem;
    height: 1.5rem;
    transition: color 0.2s;
}

/* Same visual language as .theme-toggle (which stays a separate,
   self-contained class since it's also used stand-alone on the typing/end
   screens) -- these are the main menu's other two corner buttons. */
.corner-icon-button {
    position: fixed;
    background: transparent;
    border: var(--border-width) solid var(--primary-color);
    outline: none;
    box-shadow: none;
    color: var(--primary-color);
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    cursor: pointer;
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
    border-radius: var(--radius);
    appearance: none;
}

.corner-icon-button:hover {
    background-color: var(--primary-color);
    color: var(--background-color);
}

.corner-icon-button svg {
    width: 1.5rem;
    height: 1.5rem;
}

/* The icon rail. Two bottom corners, identical on every screen -- the main
   menu builds the same two clusters inline (it owns its own Settings panel),
   cornerRail.js injects them everywhere else. This used to be two different
   layouts: bottom corners on the menu, a vertical top-right stack elsewhere,
   so the icons jumped position on every navigation.

   Left cluster is identity and behavior (Account, Settings, then Friends and
   Store); right cluster is numbers (Stats, Leaderboard, Multiplayer).
   Horizontal, not vertical -- a bottom corner has little headroom before
   hitting the screen edge. */
/* Bottom-left: what customises the app (Settings, Store). The top-left is the
   wordmark's alone -- beside it these read as part of the branding, and under
   it they pushed into the space the passage needs. */
.corner-rail-left {
    position: fixed;
    bottom: 1rem;
    left: 1rem;
    z-index: 100;
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.75rem;
}

/* How many friends are online, beside the Friends control in the top-right
   rail. Readable text rather than a corner badge, so it can say what it
   counts. */
/* How many friends are online, under the Friends icon. A bare number: the
   icon above it already says what is being counted. */
.online-count {
    min-width: 1.1rem;
    padding: 0 0.25rem;
    border-radius: var(--radius-pill, 999px);
    background: var(--primary-color);
    color: var(--background-color);
    font-size: 0.65rem;
    font-weight: 700;
    line-height: 1.25;
    text-align: center;
    white-space: nowrap;
}

.online-count[hidden] {
    display: none;
}

.corner-rail-right {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    z-index: 100;
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.75rem;
}

/* .corner-icon-button is position:fixed by default -- inside these flex rows
   it needs to drop back to a normal flow position, or every button ignores
   the layout entirely and stacks on top of the others at whatever its
   unstyled static position happens to be. */
.corner-rail-left .corner-icon-button,
.corner-rail-right .corner-icon-button {
    position: static;
}

/* Only the main menu still draws one, between its two top-left icons. A
   horizontal hairline, since that cluster is a column. */
.rail-divider {
    align-self: stretch;
    height: 1px;
    background: var(--neutral-color);
    opacity: 0.4;
}

.rail-settings-group {
    position: relative;
}

.rail-settings-panel {
    position: absolute;
    bottom: calc(100% + 0.6rem);
    left: 0;
    background: var(--background-color);
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 0.75rem;
    width: 280px;
}

.theme-gallery-popover {
    position: absolute;
    top: calc(100% + 0.6rem);
    right: 0;
    background: var(--background-color);
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 0.75rem;
    width: 320px;
}

/* Lifetime average over best single result, stacked so both read as one
   figure block beside the Stats icon rather than competing for corner room. */
.stats-corner-figures {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.05rem;
    line-height: 1.2;
}

.stats-corner-avg,
.stats-corner-pb {
    font-size: 0.85rem;
    color: var(--sub-color);
    text-transform: uppercase;
    letter-spacing: 1px;
    white-space: nowrap;
}


@keyframes blink {
    0%, 100% { opacity: 1; }
    50% { opacity: 0; }
}

/* Mobile adjustments */
@media (max-width: 768px) {
    .logo-hero {
        font-size: 2.1rem;
        margin-bottom: 1.5rem;
    }

    .menu-button {
        padding: 0.8rem 1.5rem;
        font-size: 1rem;
        min-width: 200px;
    }

    .typing-game {
        padding: 1rem;
    }

    .text-container {
        padding: 1rem;
        margin-top: var(--header-offset);
    }

    .wpm-stat {
        left: 1rem;
    }

    .acc-stat {
        right: 1rem;
    }

    .time-stat {
        bottom: 0.5rem;
        font-size: 1rem;
    }

    .text-display {
        font-size: 1.2rem;
    }

    .graph-container {
        height: 200px;
        padding: 1rem;
    }

    .graph-axis.x {
        left: 1rem;
        right: 1rem;
    }

    .graph-axis.y {
        bottom: 1rem;
    }

    .end-screen-buttons {
        bottom: 1rem;
    }
}

@media (max-height: 600px) {
    .wpm-stat,
    .acc-stat {
        top: 50%;
    }

    .time-stat {
        bottom: 0.25rem;
        font-size: 0.9rem;
    }

    .text-container {
        margin-top: 4.5rem;
        padding: 1rem;
    }

    .text-display {
        font-size: 1.2rem;
        min-height: 4.5rem;
    }

    .graph-container {
        height: 200px;
    }
}

@media (max-width: 600px) {
    .typing-game {
        padding: 0.5rem;
        height: 100vh;
        min-width: 0;
    }
    .text-container {
        padding: 0.5rem 0.5rem 0 0.5rem;
        margin-top: var(--header-offset);
        min-width: 0;
    }
    .text-display {
        font-size: 1rem;
        padding: 0.5rem;
        min-height: 3rem;
        max-width: 100vw;
        word-break: break-word;
    }
}

.attribution {
    max-width: 700px;
    width: 100%;
    margin: 0.5rem auto 1.75rem auto;
    text-align: center;
    color: var(--neutral-color);
    font-size: 0.95rem;
}

.segment-progress {
    position: fixed;
    top: 1rem;
    left: 50%;
    transform: translateX(-50%);
    font-size: 0.8rem;
    color: var(--neutral-color);
    text-transform: uppercase;
    letter-spacing: 1px;
}

.text-display.code {
    text-align: left;
    background: rgba(127, 127, 127, 0.06);
    border-radius: var(--radius-panel);
}

.text-display span.neutral.syn-keyword { color: var(--secondary-color); }
.text-display span.neutral.syn-string { color: #d9a441; }
.text-display span.neutral.syn-comment { color: var(--sub-color); font-style: italic; }
.text-display span.neutral.syn-number { color: #6f9ee8; }

/* Passive mode */
.passive-mode {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    width: 100%;
    max-width: 900px;
    margin: 0 auto;
    padding: 2rem;
    position: relative;
    text-align: center;
}

.passive-source {
    color: var(--neutral-color);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 2rem;
}

.passive-lines {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    width: 100%;
    margin-bottom: 2rem;
}

.passive-line {
    font-family: 'JetBrains Mono', monospace;
    white-space: pre-wrap;
    word-break: break-word;
}

.passive-line.prev,
.passive-line.next {
    color: var(--neutral-color);
    opacity: 0.5;
    font-size: 1.1rem;
}

.passive-line.active {
    font-size: 1.6rem;
    line-height: 1.5;
    min-height: 2.4rem;
}

.passive-input {
    position: relative;
    width: 100%;
    max-width: 500px;
    background: transparent;
    color: var(--text-color);
    border: none;
    border-bottom: 2px solid var(--neutral-color);
    outline: none;
    font-family: 'JetBrains Mono', monospace;
    font-size: 1.1rem;
    text-align: center;
    padding: 0.5rem;
    caret-color: var(--caret-color);
}

.passive-input:focus {
    border-bottom-color: var(--primary-color);
}

.passive-stats {
    margin-top: 1.5rem;
}

.passive-summary {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.5rem;
    margin-top: 2rem;
}

.passive-summary-title {
    font-size: 1.3rem;
    color: var(--primary-color);
    text-transform: uppercase;
    letter-spacing: 2px;
}

.passive-summary-stats {
    display: flex;
    gap: 3rem;
}

.passive-summary-stats > div {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
}

/* Fixed, matching width on both sides -- "CONSISTENCY" is a much longer
   label than "CHARS"/"ERR", so left to auto-size these two blocks end up
   different widths, which pushes the graph between them (a flex-grow child
   filling the remaining space-between gap) visibly off-center. */
.endscreen-side-stat.wpm,
.endscreen-side-stat.acc {
    flex: 0 0 170px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.9rem;
}

.loading {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    font-size: 1.5rem;
    color: var(--primary-color);
    font-family: 'JetBrains Mono', monospace;
    text-transform: uppercase;
    letter-spacing: 2px;
}

#root {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* Typing Game */
.stats-bar {
    display: none;
}

.end-screen {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    max-width: 1300px;
    margin: 0 auto;
    /* Same padding and same --header-offset margin as .typing-game +
       .text-container, so the passage lands on the identical baseline on
       both screens and does not jump when the test ends. This claimed to
       "match .text-container exactly" while subtracting --app-padding and
       using a 0.25rem pad, which put the end screen's passage 1.75rem
       higher than the one the player had just been reading. */
    padding: 1rem;
    margin-top: var(--header-offset);
    /* min-height must subtract the margin-top above, not just be 100vh --
       margin sits outside the box, so 100vh + this margin always overflowed
       the viewport by exactly the margin amount. Invisible before (body was
       overflow:hidden), but a real, permanent scrollbar once that changed
       to allow scrolling for genuinely tall content. */
    min-height: calc(100vh - var(--header-offset));
    position: relative;
    /* Without this, Play Again sits flush against the very bottom edge of
       the viewport whenever the passage/graph/time content happens to fill
       most of the screen -- the auto-fit cascade below still shrinks this
       away first if a genuinely tall passage would otherwise force a
       scrollbar, so it never fights that logic, it just guarantees normal
       breathing room in the common case. */
    padding-bottom: var(--bottom-chrome);
    box-sizing: border-box;
}

.end-screen-buttons {
    /* Normal flow, not fixed-to-viewport -- fixed positioning stranded this
       right at the bottom of the viewport regardless of content height,
       leaving a large dead gap above it whenever the passage/graph/time
       block didn't happen to fill the screen. */
    margin-top: 1.5rem;
    display: flex;
    gap: 1.5rem;
}

.end-screen-button {
    background: none;
    border: var(--border-width) solid var(--primary-color);
    color: var(--text-color);
    padding: 1rem 2rem;
    font-size: 1.2rem;
    cursor: pointer;
    transition: all 0.2s ease;
    font-family: inherit;
    text-transform: uppercase;
    letter-spacing: 2px;
    min-width: 250px;
    border-radius: var(--radius-panel);
}

.end-screen-button:hover {
    background-color: var(--primary-color);
    color: var(--background-color);
    transform: translateY(-2px);
}

/* Character styling */
.correct {
    color: var(--correct-color);
}

.incorrect {
    color: var(--error-color);
    text-decoration: underline;
}

.current {
    /* A hardcoded rgba(0,0,0,...) darkening tint is a no-op against dark
       mode's pure-black background (you can't darken black further) --
       mixing --text-color (already the correct direction per theme: dark
       tint on a light background, light tint on a dark one) into
       transparent instead makes this actually visible in every theme. */
    background-color: color-mix(in srgb, var(--text-color) 18%, transparent);
    border-radius: var(--radius);
}

.error-screen {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding: 2rem;
    text-align: center;
}

.error-screen h2 {
    margin-bottom: 1rem;
}

.error-screen p {
    margin-bottom: 2rem;
}

/* WPM/TIME/ACC used to each be position:fixed to a different edge of the
   viewport (left-middle, bottom-center, right-middle) -- three related
   numbers scattered to opposite corners with the text floating alone above
   a canyon of empty space between them. They're one dashboard, so they sit
   in normal flow as one row directly under the text instead. */
/* WPM sits left, ACC sits right, TIME sits bottom-center, leaving the
   center/center-bottom of the screen clear -- that's deliberate, not
   incidental: it's the same zone the end screen's graph occupies (flanked
   left/right by its own WPM/ACC), so the layout doesn't jump around when a
   test finishes and the graph appears there. Don't "fix" this into one
   grouped row; the empty center is reserved space, not dead space. */
.wpm-stat {
    position: fixed;
    left: 2rem;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    width: 6rem;
}

/* "Live Stats: Off" -- WPM/ACC hidden while typing, revealed on the end
   screen as normal. TIME stays visible since time mode needs it to know
   when the test ends. */
.typing-game.hide-live-stats .wpm-stat,
.typing-game.hide-live-stats .acc-stat {
    display: none;
}

/* "Blink Caret: Off" -- the current-character caret stays solid instead of
   blinking (see .text-display span.current::after's animation). */
.typing-game.no-caret-blink .text-display span.current::after {
    animation: none;
}

.acc-stat {
    position: fixed;
    right: 2rem;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    width: 6rem;
}

.time-stat {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: 1rem;
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}

/* Multiplayer: opponents' live progress during a race. Anchored below the
   logo (the only thing already in the top-left corner) rather than the
   center, which the typing/end screens keep empty on purpose to match the
   end screen's graph zone. */
/* The live standings, in the middle of the screen below the passage -- the
   space the end screen's graph occupies, kept clear for exactly this. It used
   to be a 200px strip pinned to the top-left corner showing opponents only,
   which put the thing you are racing against in your peripheral vision and
   left out the one bar you most need to see: your own. */
.mp-racers {
    position: fixed;
    top: 56%;
    left: 50%;
    transform: translateX(-50%);
    z-index: 90;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    /* Wider than it was. The field is the thing you look up at between
       glances at the passage, so it gets the room. */
    width: min(1040px, 94vw);
}

.mp-racer-row {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    font-size: 0.9rem;
    color: var(--racer-color, var(--sub-color));
}

/* Your own row reads a step louder than the rest. */
.mp-racer-row.me .mp-racer-name,
.mp-racer-row.me .mp-racer-wpm {
    font-weight: 700;
}

.mp-racer-name {
    min-width: 9rem;
    max-width: 9rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-align: right;
}

/* The track the sprite travels along. Tall enough to hold the sprite, with
   the trail drawn behind it. */
.mp-racer-track {
    position: relative;
    flex: 1 1 auto;
    height: 2rem;
    border-bottom: var(--border-width) dashed color-mix(in srgb, var(--neutral-color) 45%, transparent);
}

/* How far this racer has come. Sits at the foot of the track, on the dashed
   line, so the line reads as road ahead and the trail as road behind. */
.mp-racer-trail {
    position: absolute;
    left: 0;
    bottom: -1px;
    height: 2px;
    background: var(--racer-color, var(--primary-color));
    transition: width 0.15s ease;
}

/* The sprite rides the track. The position is the fraction of the way along,
   scaled by the room left after the sprite's own width, so at 100 percent the
   sprite sits at the finish rather than overhanging it. */
.mp-racer-sprite {
    position: absolute;
    bottom: 0.15rem;
    left: calc(var(--p, 0) * (100% - 1.9rem));
    width: 1.9rem;
    height: 1.9rem;
    transition: left 0.15s ease;
}

.mp-racer-row.me .mp-racer-sprite {
    filter: drop-shadow(0 0 4px var(--racer-color, var(--primary-color)));
}

.mp-racer-percent,
.mp-racer-wpm {
    min-width: 3rem;
    text-align: right;
    font-family: 'JetBrains Mono', monospace;
    font-variant-numeric: tabular-nums;
}

.mp-racer-percent {
    color: var(--sub-color);
    font-size: 0.8rem;
}

/* The one genuinely time-critical state in the app: seconds left to get your
   hands on the keys. It read in the same accent as every idle figure on the
   screen, so it carried no urgency. --warning-color is used here and on the
   device badge, nowhere else. */
.mp-countdown {
    font-size: 6rem;
    line-height: 1;
    font-weight: 700;
    color: var(--warning-color);
    text-align: center;
    margin: 0.5rem 0 1rem;
    font-family: 'JetBrains Mono', monospace;
}

.stat-label {
    font-size: 0.8rem;
    color: var(--sub-color);
    text-transform: uppercase;
    letter-spacing: 2px;
    margin-bottom: 0.5rem;
}

.stat-value {
    font-size: 2rem;
    color: var(--primary-color);
    font-weight: bold;
}

/* Custom tooltip for [data-tooltip] elements (see tooltip.js) -- replaces
   the browser's own native title="" popup, which can't be styled at all and
   reads as an unpolished default sitting next to an otherwise fully custom
   terminal-styled UI. */
/* Floats over the page, so it has to read as a layer rather than as part of
   the layout. It shared the page background with a hairline border, which
   made it look like a panel that belonged where it landed -- particularly
   when it covered the control above the one it describes. The lifted
   background and shadow separate it from whatever it overlaps. */
.app-tooltip {
    position: fixed;
    background: color-mix(in srgb, var(--text-color) 10%, var(--background-color));
    color: var(--text-color);
    border: 1px solid var(--primary-color);
    border-radius: var(--radius);
    padding: 0.4rem 0.6rem;
    font-size: 0.8rem;
    font-family: 'JetBrains Mono', monospace;
    max-width: 260px;
    line-height: 1.4;
    pointer-events: none;
    z-index: 1000;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.55);
}

.chart-tooltip {
    position: absolute;
    display: none;
    transform: translate(-50%, -100%);
    margin-top: -10px;
    background: rgba(20, 20, 20, 0.95);
    color: #fff;
    border: 1px solid var(--primary-color);
    border-radius: var(--radius);
    padding: 0.3rem 0.6rem;
    font-size: 0.8rem;
    font-family: 'JetBrains Mono', monospace;
    pointer-events: none;
    white-space: nowrap;
    z-index: 20;
}

.graph-container canvas {
    width: 100%;
    height: 100%;
}

.graph-axis {
    position: absolute;
    color: var(--text-color);
    font-size: 0.8rem;
    font-family: 'JetBrains Mono', monospace;
}

.graph-axis.x {
    bottom: 0;
    left: 2rem;
    right: 2rem;
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 0;
}

.graph-axis.y {
    top: 0;
    bottom: 2rem;
    left: 0;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 0 0.5rem;
    width: 3rem;
    text-align: right;
}

body, .app, #root {
    overflow-x: hidden;
    scrollbar-width: none; /* Firefox */
}
body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar {
    display: none; /* Chrome, Safari, Opera */
}

.future-modes-placeholder {
    width: 100%;
    min-height: 3rem;
    background: transparent;
    margin-top: 1.5rem;
}

.end-screen-text {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto 0.5rem auto;
    font-family: 'JetBrains Mono', monospace;
    font-size: 1.2rem;
    color: var(--text-color);
    /* See .current's comment -- a flat rgba(0,0,0,...) tint only darkens,
       so it did nothing at all against dark mode's pure-black background. */
    background: color-mix(in srgb, var(--text-color) 6%, transparent);
    border-radius: var(--radius);
    /* Side padding matches .text-container's 2rem so the centred passage
       lands on the same left edge as it had on the typing screen. */
    padding: 1rem 2rem;
    text-align: left;
    word-break: break-word;
}

.end-screen-graph-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    max-width: 1200px;
    margin: 0 auto 0.5rem auto;
    /* Matches .end-screen-text's own 2rem side padding, so the WPM/ACC blocks
       line up with the actual text edges above them rather than with the text
       box's outer padded edge. Symmetric: the extra 4.5rem that used to sit
       on the right cleared a fixed icon rail that ran up the right edge, and
       that rail now lives in the bottom corners. The asymmetry was pulling
       the whole graph 20px left of centre. */
    padding: 0 2rem;
}

.graph-container {
    flex: 1 1 0;
    min-width: 0;
    margin: 0;
    padding: 1rem 0;
    height: 240px;
    position: relative;
    background: color-mix(in srgb, var(--text-color) 3%, transparent);
    border-radius: var(--radius-panel);
}

.end-screen-graph-row.end-screen-time-row {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    margin: 0 auto 2rem auto;
}

.end-screen-rawwpm {
    width: 100%;
    max-width: 900px;
    margin: 0 auto 2rem auto;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 1.5rem;
    font-size: 1.3rem;
}
.end-screen-rawwpm .stat-label {
    font-size: 1rem;
    color: var(--sub-color);
    text-transform: uppercase;
    letter-spacing: 2px;
    margin-right: 0.5rem;
}
.end-screen-rawwpm .stat-value {
    font-size: 2rem;
    color: var(--primary-color);
    font-weight: bold;
}

.end-screen-time {
    width: 100%;
    max-width: 1100px;
    margin: 0 auto;
    text-align: center;
    padding: 1rem 0;
}

.end-screen-time .stat-label {
    font-size: 0.9rem;
    margin-bottom: 0.3rem;
}

.end-screen-time .stat-value {
    font-size: 1.5rem;
}

/* Width-only now -- this used to also trigger on short viewports (max-height:
   800px) to keep normal-flow content clear of the Play Again button, back
   when that button was pinned with position:fixed and could overlap it. It's
   normal flow now (see .end-screen-buttons), and the page scrolls when
   content is taller than the viewport, so there's nothing left for a height
   trigger to protect against -- it was only ever collapsing the side-by-side
   stat layout on perfectly normal laptop screens (1366x768, 1280x800 both
   hit max-height:800px despite being completely ordinary resolutions). */
@media (max-width: 900px) {
    .end-screen-graph-row {
        flex-direction: column !important;
        align-items: stretch !important;
        max-width: 100vw !important;
        position: relative !important;
        /* The extra right clearance is only needed for the desktop
           side-by-side layout sharing horizontal space with the fixed icon
           rail -- stacked vertically here, nothing competes for that
           space, so keep it symmetric like every other stacked block. */
        padding-right: 1.5rem !important;
    }
    .endscreen-side-stat {
        position: static !important;
        left: unset !important;
        right: unset !important;
        top: unset !important;
        transform: none !important;
        width: 100% !important;
        margin-bottom: 0.75rem !important;
        z-index: 10;
        /* Each block is 3 label+value pairs (WPM/RAW/CONSISTENCY or
           ACC/ERR/CHARS) -- row direction here was flattening them into one
           spread-out line (WPM ... 805 ... RAW ... 805 ...) edge to edge.
           They need to stay stacked, just centered instead of the desktop
           layout's left/right-anchored alignment. */
        display: flex !important;
        flex-direction: column !important;
        align-items: center !important;
        text-align: center !important;
    }
    .graph-container {
        flex: none !important;
        max-width: 100vw !important;
        min-width: 0 !important;
        width: 100% !important;
        overflow-x: auto !important;
        margin: 0 auto 1rem auto !important;
        height: 220px !important;
    }
    .end-screen-buttons {
        position: static !important;
        left: unset !important;
        bottom: unset !important;
        transform: none !important;
        width: 100% !important;
        margin: 1.5rem 0 0 0 !important;
        flex-direction: column !important;
        gap: 1rem !important;
        z-index: 10;
    }
    .end-screen-time {
        position: static !important;
        left: unset !important;
        transform: none !important;
        bottom: unset !important;
        width: 100% !important;
        margin-top: 1rem !important;
    }
}

@media (max-width: 600px) {
    .end-screen-graph-row {
        padding: 0 0.5rem !important;
    }
    .graph-container {
        height: 200px !important;
        padding: 0.5rem !important;
    }
    .endscreen-side-stat.wpm, .endscreen-side-stat.acc {
        font-size: 1rem !important;
        padding: 0.5rem !important;
    }
    .end-screen-time .stat-value {
        font-size: 1.2rem !important;
    }
}

@media (max-width: 900px) {
    .text-container {
        padding: 0.5rem !important;
        min-height: 120px !important;
    }
}
@media (max-width: 600px) {
    .text-container {
        min-height: 80px !important;
        font-size: 1rem !important;
    }
}

@media (max-width: 700px) {
    :root {
        --header-offset: 2.5rem;
    }
    body, #root, .app, .end-screen {
        overflow-y: auto !important;
        overflow-x: hidden !important;
    }
    .end-screen {
        min-height: 0 !important;
        height: auto !important;
        padding: 0.5rem 0.75rem !important;
        padding-top: var(--header-offset) !important; /* align with game text */
        padding-bottom: 0.5rem !important;
        display: flex !important;
        flex-direction: column !important;
    }
    .end-screen-buttons {
        margin-top: auto !important;
        margin-bottom: 0 !important;
        width: 100% !important;
    }
}
/* Room code + Join on one line: they are a single action split across two
   controls, and stacking them read as two unrelated steps. */
.mp-code-row {
    display: flex;
    gap: 0.5rem;
    width: 100%;
    align-items: stretch;
}

.mp-code-row .account-input {
    flex: 1 1 auto;
    min-width: 0;
}

/* A room code is five characters, so the field is sized for five characters.
   Stretched across the row it was a 94px-tall box with a clipped placeholder,
   twice the height of the name field directly above it and the largest
   control on the screen for the smallest input on it.

   align-items: center rather than stretch: stretch is what inflated it, and
   nothing in this row wants to match its neighbour's height by growing. */
.mp-code-row {
    justify-content: center;
    align-items: center;
}

.mp-code-row .account-input {
    flex: 0 0 auto;
    width: 9ch;
    height: 2.75rem;
    padding: 0 0.5rem;
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 2px;
    font-size: 1.1rem;
}

.mp-code-row .menu-button {
    flex: 0 0 auto;
    min-width: 0;
    height: 2.75rem;
    padding: 0 1.5rem;
    font-size: 0.95rem;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
}

/* A labelled rule, so the two ways into a race read as alternatives rather
   than a list of buttons. */
.mp-divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    margin: 0.5rem 0;
    color: var(--sub-color);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.mp-divider::before,
.mp-divider::after {
    content: '';
    flex: 1 1 auto;
    height: 1px;
    background: var(--neutral-color);
    opacity: 0.4;
}

/* The passage's attribution, repeated on the end screen so a quote still
   says who wrote it once the test is over. Same centred look as the typing
   screen's, just tighter -- the graph row follows immediately below. */
.end-screen-attribution {
    max-width: 1200px;
    margin: 0.4rem auto 0.5rem auto;
    padding: 0 2rem;
}

/* Rules and Typing side by side. Stacked, the panel ran past the bottom of a
   900px viewport and had to be scrolled to reach the last toggle, with every
   row an identically-weighted full-width button. */
.settings-columns {
    display: flex;
    gap: 1.25rem;
    width: 100%;
    align-items: flex-start;
}

.settings-column {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    flex: 1 1 0;
    min-width: 0;
}

.settings-column .menu-button.small {
    min-width: 0;
    width: 100%;
}

/* One column on narrow windows, where side-by-side would squeeze the labels. */
@media (max-width: 700px) {
    .settings-columns {
        flex-direction: column;
        gap: 0.5rem;
    }
}

/* Correct/incorrect split under a headline figure -- the keystroke count is
   the interesting number, the breakdown is the footnote. */
.stat-breakdown {
    display: block;
    font-size: 0.7rem;
    color: var(--sub-color);
    letter-spacing: 1px;
    margin-top: 0.15rem;
}


/* The top-right rail: typing language, theme, and identity. Everything here
   configures the app or says who you are, as opposed to the bottom rail's
   navigation. Screens with their own top-right controls (Restart, Zen Finish)
   hand them in as `extras` so they join this row rather than fixed-positioning
   themselves into it. */
.top-rail {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 100;
    /* Two rows: the icons, and the sign-in links under them. As one row the
       two text links made the rail 307px wide, which is what collided with
       the wordmark on a phone. */
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.25rem;
}

.top-rail-icons {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    gap: 0.6rem;
}

.top-rail .theme-control,
.top-rail .corner-icon-button,
.top-rail .restart-toggle,
.top-rail .zen-finish-button {
    position: static;
}

.lang-group {
    position: relative;
}

/* Opens downward from a top-anchored control, unlike the bottom rail's
   Settings panel which opens upward. */
.lang-popover {
    position: absolute;
    top: calc(100% + 0.6rem);
    right: 0;
    background: var(--background-color);
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 0.5rem;
    width: 210px;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    z-index: 120;
    /* Scrolls once the list outgrows the viewport -- the heading and the note
       stay put because only the item list between them scrolls. */
    max-height: calc(100vh - 6rem);
    overflow-y: auto;
}

.lang-heading {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--sub-color);
    padding: 0.25rem 0.6rem 0.35rem;
    border-bottom: 1px solid var(--neutral-color);
    margin-bottom: 0.25rem;
}

.lang-item {
    background: none;
    border: none;
    color: var(--text-color);
    font-family: inherit;
    font-size: 0.9rem;
    text-align: left;
    padding: 0.4rem 0.6rem;
    border-radius: var(--radius);
    cursor: pointer;
}

.lang-item:hover {
    background: rgba(128, 128, 128, 0.15);
}

.lang-item.active {
    color: var(--primary-color);
    font-weight: bold;
}

.lang-note {
    font-size: 0.7rem;
    color: var(--sub-color);
    padding: 0.45rem 0.6rem 0.15rem;
    line-height: 1.35;
}

.auth-control {
    display: flex;
    align-items: center;
    gap: 0.3rem;
}

.auth-control[hidden] {
    display: none;
}

.top-rail-sep {
    color: color-mix(in srgb, var(--sub-color) 55%, transparent);
    font-size: 0.75rem;
}

/* Friends sits above its count, so the count does not widen the icon row. */
.friends-control,
.account-icon-control {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
}

/* The account's own picture once signed in. There is no upload, so it is the
   initial over a colour derived from the whole username. */
.rail-avatar {
    background: var(--avatar-colour, var(--primary-color));
    border-color: transparent;
    color: #fff;
}

.rail-avatar:hover {
    border-color: var(--text-color);
}

.rail-avatar-initial {
    font-size: 0.85rem;
    font-weight: 700;
    line-height: 1;
}

/* Text links, not bordered boxes -- three outlined squares plus two outlined
   buttons in one row was far too much chrome for a corner. */
.top-rail-link {
    background: none;
    border: none;
    color: var(--sub-color);
    font-family: inherit;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    cursor: pointer;
    padding: 0.5rem 0.4rem;
}

.top-rail-link:hover {
    color: var(--text-color);
}

.top-rail-link.accent,
.top-rail-link.signed-in {
    color: var(--primary-color);
}

.top-rail-link.accent:hover,
.top-rail-link.signed-in:hover {
    color: var(--secondary-color);
}

/* Live counts on the Friends and Multiplayer icons. Positioned against the
   button's own box, which is why those buttons opt in with .has-badge. */
.corner-icon-button.has-badge {
    position: relative;
    overflow: visible;
}

.corner-rail-left .corner-icon-button.has-badge,
.corner-rail-right .corner-icon-button.has-badge {
    position: relative;
}

.rail-badge {
    position: absolute;
    top: -0.45rem;
    right: -0.45rem;
    min-width: 1.05rem;
    height: 1.05rem;
    padding: 0 0.2rem;
    border-radius: var(--radius-pill);
    background: var(--background-color);
    border: var(--border-width) solid var(--neutral-color);
    color: var(--sub-color);
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.6rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}

/* People available to race right now is the one count worth drawing the eye. */
.rail-badge.accent {
    border-color: var(--primary-color);
    color: var(--primary-color);
}

.mp-caption {
    color: var(--primary-color);
}


.friends-control {
    display: flex;
    align-items: center;
}


/* Keyboard shortcuts, at the foot of the menu rather than wedged between the
   title and the buttons where it competed with them for attention. */
.menu-key-hint {
    position: fixed;
    bottom: 3.25rem;
    left: 50%;
    transform: translateX(-50%);
    font-size: 0.75rem;
    color: var(--sub-color);
    letter-spacing: 1px;
    z-index: 90;
}


/* Every figure on this screen is the same shape: a dim label directly over
   its value, the two centred on each other. Grouped by what they qualify --
   RAW and PB under WPM, ERR and CONSISTENCY under ACC, KEYSTROKES and
   CHARACTERS either side of TIME -- so each secondary number sits with the
   headline it belongs to instead of in an undifferentiated row. */
.end-stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
}

.end-stat .stat-label {
    font-size: 0.7rem;
    letter-spacing: 1px;
    color: var(--sub-color);
    text-align: center;
}

.end-stat .stat-value {
    font-size: 1.25rem;
    color: var(--primary-color);
    font-family: 'JetBrains Mono', monospace;
    line-height: 1.15;
    text-align: center;
}

/* The three figures that lead their group. */
.end-stat.headline .stat-value {
    font-size: 2.75rem;
}

.end-stat.headline .stat-label {
    font-size: 0.8rem;
}

.end-stat.pb-new .stat-value {
    color: var(--record-color);
    font-weight: 700;
    font-size: 1rem;
}

/* KEYSTROKES and CHARACTERS flank TIME, which leads them, and the whole group
   is centred under the graph. */
.end-screen-stat-row {
    /* A grid, not a flex row: equal side columns put TIME on the exact centre
       line of the graph above it. Flexed, its position drifted with however
       wide KEYSTROKES and CHARACTERS happened to be. */
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    column-gap: 3rem;
    margin-top: 1.25rem;
    width: 100%;
    max-width: 1200px;
    padding: 0 2rem;
}

.end-screen-stat-row > .end-stat:first-child {
    justify-self: end;
}

.end-screen-stat-row > .end-stat:last-child {
    justify-self: start;
}

@media (max-width: 700px) {
    .end-screen-stat-row {
        grid-template-columns: 1fr;
        row-gap: 1rem;
        justify-items: center;
    }
    .end-screen-stat-row > .end-stat:first-child,
    .end-screen-stat-row > .end-stat:last-child {
        justify-self: center;
    }
}

@media (max-width: 900px) {
    .end-screen-stat-row {
        gap: 1.25rem 1.75rem;
        padding: 0 1rem;
    }
}

/* What a code snippet actually does. Shown under the passage while you type in
   single player -- the middle of the screen is free there, and reading the
   explanation as you work through the line is the point of the mode. In
   multiplayer that space belongs to the live standings, so it waits for the
   end screen instead, where it appears in both cases. */
.code-explainer {
    max-width: 640px;
    margin: 0.75rem auto 0 auto;
    padding: 0.75rem 1rem;
    border-left: 2px solid color-mix(in srgb, var(--primary-color) 45%, transparent);
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--sub-color);
    text-align: left;
}

.code-explainer-label {
    display: block;
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--primary-color);
    margin-bottom: 0.3rem;
}


/* A disabled control must not still look like the live primary action. The
   lobby's "Waiting for players..." button kept the filled hover state and
   read as the thing to press. */
.menu-button:disabled,
.menu-button[disabled] {
    opacity: 0.55;
    cursor: default;
}

.menu-button:disabled:hover,
.menu-button[disabled]:hover {
    background: none;
    color: var(--sub-color);
}

/* Where everyone placed, on the end screen after a race. A multiplayer result
   used to end on your own numbers alone, with no way to see who beat you or
   by how much. */
.mp-standings {
    width: 100%;
    max-width: 560px;
    margin: 1.75rem auto 0 auto;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.mp-standings-heading {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--sub-color);
    text-align: center;
    margin-bottom: 0.35rem;
}

.mp-standings-row {
    display: grid;
    grid-template-columns: 1.75rem 1fr 3rem 3rem 4rem;
    align-items: center;
    gap: 0.75rem;
    padding: 0.4rem 0.75rem;
    border: 1px solid transparent;
    border-radius: var(--radius);
    font-size: 0.85rem;
    color: var(--sub-color);
}

/* Your own row is the one being looked for. */
.mp-standings-row.me {
    border-color: color-mix(in srgb, var(--primary-color) 45%, transparent);
    color: var(--text-color);
}

.mp-standings-place {
    font-family: 'JetBrains Mono', monospace;
    color: var(--primary-color);
    text-align: center;
}

.mp-standings-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mp-standings-wpm,
.mp-standings-acc,
.mp-standings-time {
    font-family: 'JetBrains Mono', monospace;
    white-space: nowrap;
    text-align: right;
}

/* Column labels, so the three numeric columns are readable without guessing
   which is which. */
.mp-standings-head {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--sub-color);
    padding-bottom: 0.15rem;
    border: none;
}

.mp-standings-head span:nth-child(n+3) {
    text-align: right;
}

.mp-standings-max-width {
    max-width: 560px;
}

/* Still typing: no place yet, so the row shows how far along they are. */
.mp-standings-row.racing {
    opacity: 0.65;
}

.mp-standings-place {
    color: var(--racer-color, var(--primary-color));
}

/* ---- Interactive control polish -------------------------------------
   Focus was invisible almost everywhere: only the two text inputs and the
   theme toggle had a :focus rule, while .corner-icon-button, .theme-toggle,
   .restart-toggle and .zen-finish-button all set `outline: none` outright.
   Tabbing through the app showed nothing at all.

   A box-shadow ring rather than an outline, so it works regardless of those
   existing `outline: none` declarations, and :focus-visible rather than
   :focus so it appears for keyboard users without ringing every mouse
   click. */
.menu-button:focus-visible,
.corner-icon-button:focus-visible,
.theme-toggle:focus-visible,
.restart-toggle:focus-visible,
.zen-finish-button:focus-visible,
.top-rail-link:focus-visible,
.mode-popover-item:focus-visible,
.lang-item:focus-visible,
.theme-swatch:focus-visible,
.end-screen-button:focus-visible,
.account-input:focus-visible,
.custom-textarea:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--background-color), 0 0 0 4px var(--primary-color);
    border-radius: var(--radius);
}

/* Placeholders were inheriting the input's own text colour at whatever
   opacity the browser chose, which differed between engines. */
.account-input::placeholder,
.custom-textarea::placeholder {
    color: var(--sub-color);
    opacity: 1;
}

.account-input:hover:not(:focus),
.custom-textarea:hover:not(:focus) {
    border-color: color-mix(in srgb, var(--primary-color) 45%, var(--neutral-color));
}

/* Inputs sat at whatever width their container gave them, which left the
   multiplayer name field and its Join row on different widths. */
.account-input {
    width: 100%;
    box-sizing: border-box;
    transition: border-color 0.15s ease;
}

/* A pressed control should acknowledge the press. Nothing in the app moved
   on :active, so a click on a button that opens a panel elsewhere on screen
   gave no feedback at the point of contact. */
.menu-button:active:not(:disabled),
/* A rail control that is switched on. The sound toggle is the only one where
   the state is not obvious from the icon alone at a glance. */
.corner-icon-button.active {
    border-color: var(--primary-color);
    color: var(--primary-color);
}

.corner-icon-button:active,
.end-screen-button:active {
    transform: translateY(1px);
}

/* Popover rows are buttons but read as list items; they need the same hover
   affordance the chips and menu buttons have. */
.mode-popover-item,
.lang-item {
    transition: background-color 0.12s ease, color 0.12s ease;
}

/* The count, over the passage, while typing is locked. Sits above the text so
   the seconds before a race are spent reading the opening words -- the lobby
   used to hold the count on a screen with nothing to read. */
.mp-race-gate {
    position: fixed;
    inset: 0;
    z-index: 95;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    pointer-events: none;
    background: color-mix(in srgb, var(--background-color) 55%, transparent);
}

.mp-race-gate-count {
    font-family: 'JetBrains Mono', monospace;
    font-size: 7rem;
    line-height: 1;
    font-weight: 700;
    color: var(--warning-color);
}

.mp-race-gate-label {
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 2px;
    color: var(--sub-color);
}

/* Shown above the catalogue when signed out. The store used to be replaced
   entirely by a sign-in prompt, so nobody could see what signing in would get
   them. */
.store-signin-note {
    display: flex;
    align-items: center;
    /* Was shrink-to-fit at 415px inside a 700px panel whose item list is
       636px, so the page had three different content widths stacked on top of
       each other and this box read as misplaced. It matches the list now. */
    width: 100%;
    box-sizing: border-box;
    justify-content: space-between;
    text-align: left;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.5rem;
    padding: 0.75rem 1rem;
    border: 1px solid color-mix(in srgb, var(--primary-color) 35%, transparent);
    border-radius: var(--radius-panel);
    font-size: 0.85rem;
    color: var(--sub-color);
}

/* Store rows. These reuse .leaderboard-row for its border and spacing, but a
   store row is a swatch, a name, a price and a control, not four columns of
   text, so the parts that differ are set here.

   The row was 93px tall for one line of content: .menu-button carries a
   vertical margin meant for a stacked menu, and it applied here too. Twenty
   six items came to roughly 2400px of scrolling. */
.store-item-row {
    padding: 0.45rem 1rem;
}

.store-item-row .menu-button.small {
    margin: 0;
    padding: 0.35rem 0.9rem;
    font-size: 0.8rem;
}

/* Fixed width so the prices form a column instead of drifting with the length
   of each number. */
.store-price {
    min-width: 4.5rem;
    text-align: right;
    color: var(--sub-color);
    font-size: 0.85rem;
}

.store-item-row .leaderboard-name {
    font-size: 0.9rem;
}

/* Equip and Buy sit in the same place in every row, so the control column has
   one width whichever state a row is in. */
.store-item-row .menu-button {
    min-width: 5.5rem;
}

/* A postal address, shown to an administrator so an order can be packed. */
.order-address {
    margin: 0.4rem 0;
    padding: 0.5rem 0.75rem;
    border-left: 2px solid var(--border-color);
    color: var(--text-color);
    font-size: 0.8rem;
    line-height: 1.5;
    white-space: normal;
}

/* Stands in for a buy control while there is no server to buy through. A
   label, not a button: there is nothing for a press to do. */
.store-soon {
    padding: 0.35rem 0.9rem;
    border: var(--border-width) dashed var(--border-color);
    border-radius: var(--radius);
    color: var(--sub-color);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    white-space: nowrap;
}

.store-offline-note {
    justify-content: center;
    text-align: center;
}

/* Physical goods. A card, like a bundle, because each one has a description,
   possibly a size to pick, and a postage line. */
.merch-note {
    text-align: left;
    width: 100%;
    max-width: none;
    margin-bottom: 0.75rem;
}

.merch-kind {
    width: 100%;
    text-align: left;
    color: var(--sub-color);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    margin: 0.75rem 0 0.4rem 0;
}

.merch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 0.75rem;
    width: 100%;
    margin-bottom: 0.75rem;
    box-sizing: border-box;
}

.merch-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.9rem 1rem;
    border: var(--border-width) solid var(--border-color);
    border-radius: var(--radius-panel);
    text-align: left;
}

.merch-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

.merch-name {
    color: var(--text-color);
    font-size: 0.95rem;
    font-weight: 600;
}

.merch-price {
    color: var(--text-color);
    font-size: 1rem;
    font-weight: 600;
    white-space: nowrap;
}

.merch-description {
    color: var(--sub-color);
    font-size: 0.8rem;
    line-height: 1.45;
}

.merch-variants {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.merch-variant {
    padding: 0.3rem 0.6rem;
    border: var(--border-width) solid var(--border-color);
    border-radius: var(--radius);
    background: none;
    color: var(--sub-color);
    font-family: inherit;
    font-size: 0.75rem;
    cursor: pointer;
}

.merch-variant:hover {
    border-color: var(--primary-color);
    color: var(--text-color);
}

.merch-variant.active {
    border-color: var(--primary-color);
    background: var(--primary-color);
    color: var(--background-color);
}

.merch-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-top: auto;
    padding-top: 0.25rem;
}

.merch-foot .menu-button.small {
    margin: 0;
    padding: 0.35rem 0.9rem;
    font-size: 0.8rem;
}

.merch-shipping {
    color: var(--sub-color);
    font-size: 0.72rem;
}

.merch-error:empty {
    display: none;
}

.merch-error {
    color: var(--error-color, var(--incorrect-color));
    font-size: 0.75rem;
}

/* The supporter offer, above the catalogue. It is the one thing in the store
   that is not cosmetic, so it does not sit in a category with the rest. */
.store-supporter {
    width: 100%;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.5rem;
    padding: 0.9rem 1.1rem;
    border: var(--border-width) solid color-mix(in srgb, var(--primary-color) 45%, transparent);
    border-radius: var(--radius-panel);
    background: color-mix(in srgb, var(--primary-color) 7%, transparent);
    text-align: left;
}

.store-supporter.is-supporter {
    border-color: color-mix(in srgb, var(--correct-color) 45%, transparent);
    background: color-mix(in srgb, var(--correct-color) 7%, transparent);
}

.store-supporter-text {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.store-supporter-text strong {
    color: var(--text-color);
    font-size: 0.95rem;
}

.store-supporter-text span {
    color: var(--sub-color);
    font-size: 0.8rem;
}

.store-supporter .menu-button.small,
.store-signin-note .menu-button.small {
    margin: 0;
}

/* Bundles. A card rather than a row: a bundle has to show what is inside it,
   which a one-line row has no space for. */
.bundle-grid {
    width: 100%;
    box-sizing: border-box;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 0.75rem;
    margin-bottom: 1.5rem;
}

.bundle-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.9rem 1rem;
    border: var(--border-width) solid var(--border-color);
    border-radius: var(--radius-panel);
    text-align: left;
}

.bundle-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

.bundle-name {
    color: var(--text-color);
    font-size: 0.95rem;
    font-weight: 600;
}

.bundle-saving {
    color: var(--correct-color);
    font-size: 0.75rem;
    white-space: nowrap;
}

.bundle-description {
    color: var(--sub-color);
    font-size: 0.8rem;
    line-height: 1.4;
}

.bundle-previews {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0.15rem 0;
}

/* An item already owned is dimmed rather than hidden, so the bundle still
   shows its full contents. */
.bundle-preview-item.owned {
    opacity: 0.35;
}

/* How many more the card did not have room to draw. */
.bundle-preview-more {
    display: flex;
    align-items: center;
    color: var(--sub-color);
    font-size: 0.75rem;
}

.bundle-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    /* Pushed to the bottom so the buy control lines up across a row of cards
       whose contents are different heights. The Lot holds 26 swatches and its
       neighbour holds six. */
    margin-top: auto;
    padding-top: 0.5rem;
}

.bundle-foot .menu-button.small {
    margin: 0;
    padding: 0.35rem 0.9rem;
    font-size: 0.8rem;
}

.bundle-prices {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
}

.bundle-price {
    color: var(--text-color);
    font-size: 1.05rem;
    font-weight: 600;
}

.bundle-full-price {
    color: var(--sub-color);
    font-size: 0.8rem;
    text-decoration: line-through;
}

.bundle-owned,
.bundle-note {
    color: var(--sub-color);
    font-size: 0.75rem;
}

/* Close, at the top-right of the content. Replaces the full-width "Back"
   button every screen carried at the bottom -- a third way to do what the
   wordmark and Escape already did, and the least reachable of the three. */
.screen-close {
    position: absolute;
    /* The content column already starts below the rail, so a small offset
       inside it is enough. The rail's own height feeds --header-offset, which
       is what puts this clear of it whether the rail has one row or two. */
    top: 0.25rem;
    right: 0;
    width: 2.25rem;
    height: 2.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    color: var(--sub-color);
    cursor: pointer;
    border-radius: var(--radius);
    transition: color 0.15s ease, background-color 0.15s ease;
}

.screen-close:hover {
    color: var(--text-color);
    background: color-mix(in srgb, var(--text-color) 10%, transparent);
}

.screen-close svg {
    width: 1.1rem;
    height: 1.1rem;
}

/* The close button anchors to the content column. */
.stats-screen {
    position: relative;
}

/* The per-key weakness data Practice mode is built on. It drove text
   generation but was never shown, so nobody could see what it had learned
   about their typing. */
.weak-key-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem;
    width: 100%;
}

.weak-key {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    min-width: 3.25rem;
    padding: 0.5rem 0.4rem;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
}

.weak-key-char {
    font-family: 'JetBrains Mono', monospace;
    font-size: 1.15rem;
    color: var(--primary-color);
}

.weak-key-err {
    font-size: 0.7rem;
    color: var(--error-color);
}

.weak-key-lat {
    font-size: 0.65rem;
    color: var(--sub-color);
}

/* Recent form against the lifetime average. */
.stat-trend {
    font-size: 0.7rem;
    margin-left: 0.3rem;
    vertical-align: middle;
}

.stat-trend.up { color: var(--primary-color); }
.stat-trend.down { color: var(--error-color); }

/* Marks a leaderboard row as a practice opponent rather than a human result.
   The board is seeded so a new player has something to measure against, and
   saying so is the difference between seeding it and faking it. */
.leaderboard-bot-badge {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--sub-color);
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
    padding: 0.1rem 0.35rem;
}

.leaderboard-name:disabled {
    cursor: default;
    opacity: 1;
}

/* A longer body than a one-line placeholder: keep its line breaks and give it
   a readable measure. */
.stats-placeholder.prose {
    white-space: pre-wrap;
    text-align: left;
    max-width: 46rem;
    line-height: 1.6;
}

/* Source, privacy and share, at the foot of the menu. One line rather than a
   page: there is little to say and a whole screen would overstate it. */
.site-footer {
    position: fixed;
    bottom: 1rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 90;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.75rem;
    color: var(--sub-color);
}

.site-footer-link {
    background: none;
    border: none;
    padding: 0.25rem;
    font-family: inherit;
    font-size: 0.75rem;
    color: var(--sub-color);
    text-decoration: none;
    cursor: pointer;
    border-radius: var(--radius);
    transition: color 0.15s ease;
}

.site-footer-link:hover {
    color: var(--primary-color);
}

.site-footer-sep,
.site-footer-version {
    opacity: 0.6;
}

/* ---- Button hierarchy -------------------------------------------------
   Everything was .menu-button, .menu-button.small or .menu-button.ghost --
   three variants that all render as an outlined box of roughly equal weight.
   A screen's one real action, a settings toggle and a filter chip were
   indistinguishable at a glance.

   Three tiers instead:
     .primary  the single action a screen exists for. Filled.
     (default) supporting actions. Outlined.
     .quiet    toggles and filters, which are settings rather than actions.
               No border until hovered or active. */
.menu-button.primary {
    background: var(--primary-color);
    color: var(--background-color);
    border-color: var(--primary-color);
    font-weight: 700;
}

.menu-button.primary:hover:not(:disabled) {
    background: var(--secondary-color);
    border-color: var(--secondary-color);
    color: var(--background-color);
}

.menu-button.quiet {
    border-color: transparent;
    color: var(--sub-color);
    min-width: 0;
}

.menu-button.quiet:hover:not(:disabled) {
    background: none;
    color: var(--text-color);
    border-color: color-mix(in srgb, var(--primary-color) 35%, transparent);
}

/* A toggle that is currently on reads as selected, not as pressable. */
.menu-button.quiet.active {
    color: var(--primary-color);
    border-color: var(--primary-color);
}

/* Community submissions: the form, your own history, and the review queue. */
.contribute-form { width: 100%; max-width: 34rem; }

.contribute-count {
    align-self: flex-end;
    font-size: 0.7rem;
    color: var(--sub-color);
    font-family: 'JetBrains Mono', monospace;
}

.contribute-len.bad { color: var(--error-color); }

.submission-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    width: 100%;
    max-width: 40rem;
}

.submission-row {
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius-panel);
    padding: 0.75rem 0.9rem;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    text-align: left;
}

.submission-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--sub-color);
}

.submission-status { margin-left: auto; }
.submission-status.status-approved { color: var(--primary-color); }
.submission-status.status-rejected { color: var(--error-color); }
.submission-status.status-pending { color: var(--warning-color); }

.submission-content {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85rem;
    color: var(--text-color);
    line-height: 1.5;
}

.submission-attribution,
.submission-reason {
    font-size: 0.75rem;
    color: var(--sub-color);
}

.submission-reason { color: var(--error-color); }

.submission-actions { display: flex; gap: 0.5rem; }

/* Imported documents, kept between visits. Notes are worked through over
   several sittings, so the list shows how far into each one you are. */
.custom-library {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    width: 100%;
    margin-bottom: 0.5rem;
}

.custom-doc {
    display: flex;
    align-items: stretch;
    gap: 0.35rem;
}

.custom-doc-open {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.15rem;
    background: none;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
    padding: 0.5rem 0.7rem;
    color: var(--text-color);
    font-family: inherit;
    cursor: pointer;
    text-align: left;
    min-width: 0;
}

.custom-doc-open:hover {
    border-color: var(--primary-color);
}

.custom-doc-name {
    font-size: 0.85rem;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.custom-doc-progress {
    font-size: 0.7rem;
    color: var(--sub-color);
    font-family: 'JetBrains Mono', monospace;
}

.custom-doc-remove {
    flex: 0 0 auto;
    width: 2rem;
    background: none;
    border: 1px solid transparent;
    border-radius: var(--radius);
    color: var(--sub-color);
    font-size: 1.1rem;
    cursor: pointer;
}

.custom-doc-remove:hover {
    color: var(--error-color);
    border-color: color-mix(in srgb, var(--error-color) 45%, transparent);
}

/* Appointing moderators. Sits with the review queue because it is the same
   job: deciding who looks after submitted passages. */
.admin-search-form { width: 100%; max-width: 34rem; }

.admin-user {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem 0.75rem;
    border: var(--border-width) solid var(--neutral-color);
    border-radius: var(--radius);
}

.admin-user-name {
    flex: 1 1 auto;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.admin-user-role {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--primary-color);
    border: 1px solid color-mix(in srgb, var(--primary-color) 45%, transparent);
    border-radius: var(--radius);
    padding: 0.1rem 0.35rem;
}


/* The end screen is a flex column, so every child shrinks by default once the
   content is taller than the viewport. That squeezed the two things the
   screen exists to show: on a 650px window the passage came out 80px tall and
   the graph 120px, both unreadable, and the passage was clipped rather than
   scrolled to. They keep their size now and the page scrolls instead, which
   is the correct trade for a screen you read rather than one you act on. */
.end-screen-text,
.end-screen-graph-row,
.end-screen-stat-row,
.end-screen-attribution,
.code-explainer,
.mp-standings,
.end-screen-buttons {
    flex-shrink: 0;
}

/* Below this the chart stops carrying information. */
.graph-container {
    min-height: 180px;
}


/* The wordmark and the top rail are both fixed to the top of the viewport and
   neither knew about the other. Measured at 390px: the wordmark ran to x=142
   and the rail started at x=67, so they overlapped by 75px. Both give ground
   here, and the close button drops below the rail rather than under it. */
@media (max-width: 700px) {
    .logo {
        font-size: 0.95rem;
        letter-spacing: 1px;
    }

    .top-rail-icons {
        gap: 0.35rem;
    }

    .top-rail-link {
        font-size: 0.7rem;
        padding: 0.5rem 0.2rem;
    }

    .screen-close {
        top: 0.25rem;
    }
}


/* The leaderboard row's columns all have fixed widths, which added up to a
   hard minimum of 461px. Measured at 390px the row ran from x=-36 to x=426
   and the date column was cut off the edge of the screen entirely. The date
   is the least useful of the six, so it goes first and the rest fit. */
@media (max-width: 700px) {
    .leaderboard-date {
        display: none;
    }

    .leaderboard-row {
        gap: 0.6rem;
        padding: 0.6rem 0.75rem;
    }

    .leaderboard-rank {
        min-width: 1.75rem;
    }
}

@media (max-height: 600px) {
    .graph-container {
        min-height: 150px;
    }
}


/* Bottom chrome on a narrow screen.
   The footer is centred and grows to the full width once the links wrap, so
   at 375px it ran straight through both corner rails and covered Play Again,
   which made the button unclickable. Stacked instead: rails on the bottom
   line, footer above them, keyboard hint above that, and the reserved space
   grows to match. */
@media (max-width: 700px) {
    :root {
        --bottom-chrome: 12rem;
    }

    .site-footer {
        bottom: 4rem;
        flex-wrap: wrap;
        justify-content: center;
        max-width: calc(100vw - 2rem);
        row-gap: 0.2rem;
    }

    /* Clears the footer even when its links wrap onto a second line, which
       they do below about 400px. */
    .menu-key-hint {
        bottom: 9.5rem;
    }

    /* The narrow-width rule above sets this to 0.5rem, which is what left the
       content underneath the chrome in the first place. */
    .end-screen {
        padding-bottom: var(--bottom-chrome) !important;
    }

    .stats-screen {
        padding-bottom: var(--bottom-chrome);
    }
}

/* Racer sprites. A race is easier to read as characters moving than as
   coloured lines with names attached, and the sprite is the same for a given
   player on every client because it comes from the server's player order. */
.mp-racer-sprite {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    color: var(--racer-color, var(--primary-color));
}

/* Beside a name in a list, rather than on a track. */
.leaderboard-name .mp-racer-sprite,
.mp-standings-name .mp-racer-sprite {
    position: static;
    width: 1.15rem;
    height: 1.15rem;
}

.mp-racer-sprite svg {
    width: 100%;
    height: 100%;
}

.leaderboard-name .mp-racer-sprite {
    margin-right: 0.5rem;
    vertical-align: middle;
}

/* Reserved advertising space. See web/src/adSlot.js: this holds the space so
   a later ad tag cannot push the page around when it loads. Nothing is
   loaded into it today. */
/* Across the top of the page, where a navigation bar usually goes. Fixed, so
   it stays put while a long screen scrolls under it, and opaque, so the
   content passing beneath does not show through. */
/* The pack a passage came from, beside its source. Quieter than the
   attribution itself, which is the thing being credited. */
.attribution-pack {
    margin-left: 0.6rem;
    padding-left: 0.6rem;
    border-left: var(--border-width) solid var(--border-color);
    color: var(--sub-color);
    font-size: 0.8em;
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* The unit itself, centred at the top of the page. Not a full-width bar: as
   one it read as a second chrome layer stretching past the content on both
   sides, and it pushed the wordmark and the icon rail down out of the corners
   they belong in. On a wide window all three share the top line. */
.ad-banner-top {
    position: fixed;
    top: 1rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 85;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ad-banner-top[hidden] {
    display: none;
}

.ad-banner-inner {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* No top margin in the banner: the bar centres it already. */
.ad-banner-inner .ad-slot {
    margin: 0 auto;
}

.ad-slot {
    width: var(--ad-w);
    height: var(--ad-h);
    max-width: 100%;
    margin: 1.25rem auto 0 auto;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: var(--border-width) dashed color-mix(in srgb, var(--neutral-color) 60%, transparent);
    border-radius: var(--radius);
    box-sizing: border-box;
}

/* Below this the wordmark, the unit and the icon rail do not fit on one line,
   so the banner drops beneath the header row instead of colliding with it,
   and the content column clears both. */
@media (max-width: 980px) {
    .ad-banner-top {
        top: calc(1rem + var(--top-rail-h) + 0.5rem);
    }

    :root {
        --header-offset: calc(1rem + var(--top-rail-h) + 0.5rem + var(--ad-banner-h) + 0.75rem);
    }
}

/* The page margins. Hidden by default and shown only on a window wide enough
   to hold them beside the content without crowding it: the widest content
   column is 1040px, and two 160px rails with gaps need 1424px before they fit
   without pushing anything. Below that they simply do not exist. */
.ad-rail {
    display: none;
}

.ad-rail[hidden] {
    display: none;
}

@media (min-width: 1424px) {
    .ad-rail:not([hidden]) {
        display: flex;
        position: fixed;
        top: 50%;
        transform: translateY(-50%);
        z-index: 80;
        align-items: center;
    }

    .ad-rail-left:not([hidden]) {
        left: 1.5rem;
    }

    .ad-rail-right:not([hidden]) {
        right: 1.5rem;
    }

    .ad-rail .ad-slot {
        margin: 0;
    }
}

/* Dashed and unfilled, which is the one place that convention is right: it is
   a placeholder, and it should not be mistaken for content. */
.ad-slot-label {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: color-mix(in srgb, var(--sub-color) 70%, transparent);
}

@media (max-width: 768px) {
    .ad-slot {
        width: var(--ad-mobile-w);
        height: var(--ad-mobile-h);
    }

    :root {
        --ad-banner-height: 3.125rem;
    }
}


/* Holds the slot in the end screen's flex column without shrinking. */
.end-screen-ad {
    flex-shrink: 0;
    width: 100%;
    display: flex;
    justify-content: center;
}
