/* ==========================================================================
   SquirtzCraft Manager — "control room" theme
   --------------------------------------------------------------------------
   All styling for the dashboard (dashboard.html) and the login page
   (login.html). Served as /static/style.css?v=<asset_version>. There is no
   build step: the browser gets exactly this file.

   Contents (search for the number, e.g. "12. Console"):
      1. Design tokens ........ colours, radii, shadows, fonts, timings
      2. Reset & base ......... box model, typography, focus, icons, skip link
      3. Background ........... fixed glow + grid behind every page
      4. Buttons
      5. Badges & pills ....... badges, status pill, "live" indicator
      6. Top bar
      7. Layout ............... page shell, views, overview/server/players grids
      8. Cards
      9. Hero ................. overview status card, facts, player gauge
     10. Players .............. online player tiles, empty state
     11. Controls & job dock .. server control buttons, background job panel
     12. Console .............. live log terminal
     13. Server setup ......... version/loader/world switch form, form fields
     14. Whitelist
     15. World settings ....... server-list preview, icon, settings, seed
     16. Backups .............. schedule strip, backup list
     17. Storage .............. disk bar, worlds list, confirm-dialog extras,
                                loading skeleton and error state
     18. Guest call-to-action
     19. Footer
     20. Dialog ............... confirm dialog (<dialog>)
     21. Toasts
     22. Login page
     23. Animations ........... @keyframes
     24. Navigation ........... top tabs and the (hidden by default) tab bar
     25. Responsive ........... media queries, touch and reduced-motion tweaks

   Breakpoints (base styles are for phones; wider screens build on them):
     >= 760px         overview in two columns, facts in one row of four
     >= 1100px        desktop: fixed side column + full-height console;
                      storage view in two columns
     >= 1500px        wide: players get their own column
                      (>= 2200px: the page may grow wider)
     < 900px          tabs move to a fixed bottom tab bar
                      (except landscape phones, where height is scarce)
     900px-1100px     tab labels hidden, icons only
     < 760px          backup schedule stacks, world picker goes full width
     <= 640px         phones: tighter spacing, full-width actions
     short landscape  nothing sticky, console not fitted to the screen height
   The storage cards also use @container queries (section 17) so their rows
   wrap when the card itself is narrow, whatever the screen width.

   Stacking order (z-index), lowest to highest:
     -1 .backdrop < 20 .topbar < 30 .tabbar < 40 .job-dock < 50 .toasts
     < 100 .skip-link. The confirm dialog is opened with showModal(), which
     puts it in the browser's top layer, above all of these.

   Conventions:
     - Change colours through the tokens in :root, not with new hex values.
     - State classes are named "is-*" (is-active, is-loading, is-busy...) and
       are toggled by app.js. body[data-online], body[data-admin] and
       body[data-view] are set by the template and kept up to date by app.js.
     - ".js" is added to <html> by an inline script in the template, so
       ".js ..." rules only apply when JavaScript runs (the dashboard still
       works without it).
     - A property written twice in a row (e.g. 100vh then 100dvh) is a
       fallback: browsers that don't know the second unit keep the first.
   ========================================================================== */

/* ==========================================================================
   1. Design tokens
   --------------------------------------------------------------------------
   Every colour, radius, shadow, font and timing used below. Layout sizes
   (--gap, --page-max, --page-pad, --tabbar-h) live in section 7, and
   --topbar-h / --tabbar-h are changed by the media queries in section 25.
   ========================================================================== */

:root {
    /* Surfaces */
    --bg: #06080b;
    --panel: #0c1016;
    --panel-2: #10151c;
    --panel-3: #151b24;
    --panel-hover: #18202a;
    --terminal: #05070a;

    /* Lines */
    --line: rgba(255, 255, 255, 0.07);
    --line-strong: rgba(255, 255, 255, 0.12);

    /* Text */
    --text: #eef2f6;
    --text-2: #a4aebb;
    --text-3: #6b7684;

    /* Accent (grass) */
    --accent: #34d399;
    --accent-strong: #10b981;
    --accent-ink: #03170f;
    --accent-soft: rgba(52, 211, 153, 0.12);
    --accent-line: rgba(52, 211, 153, 0.32);
    --accent-glow: rgba(52, 211, 153, 0.28);

    /* Tones */
    --danger: #f87171;
    --danger-soft: rgba(248, 113, 113, 0.12);
    --danger-line: rgba(248, 113, 113, 0.32);
    --warning: #fbbf24;
    --warning-soft: rgba(251, 191, 36, 0.12);
    --warning-line: rgba(251, 191, 36, 0.3);
    --info: #60a5fa;
    --info-soft: rgba(96, 165, 250, 0.12);
    --info-line: rgba(96, 165, 250, 0.3);
    --muted-soft: rgba(148, 163, 184, 0.12);
    /* Components with a colour variant (.status-pill, .ctl, .toast) read a
       local --tone / --tone-soft / --tone-line set from these. */

    /* Shape */
    --r-xs: 6px;
    --r-sm: 10px;
    --r-md: 14px;
    --r-lg: 20px;
    --r-pill: 999px;

    --shadow-card: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 20px 50px -24px rgba(0, 0, 0, 0.7);
    --shadow-pop: 0 24px 60px -12px rgba(0, 0, 0, 0.75);

    --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;

    /* Motion: a quick ease-out shared by most transitions. */
    --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
    --fast: 140ms var(--ease);
    --med: 240ms var(--ease);

    /* Height of the top bar's content (without the safe-area inset).
       Also used to offset toasts, sticky cards and scroll targets. */
    --topbar-h: 64px;
}

/* ==========================================================================
   2. Reset & base
   ========================================================================== */

*,
*::before,
*::after {
    box-sizing: border-box;
}

/* The hidden attribute must win over any component "display" rule
   (app.js shows and hides elements by toggling it). */
[hidden] {
    display: none !important;
}

html {
    color-scheme: dark;
    background: var(--bg);
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    /* No grey flash when tapping on mobile; focus/active styles are custom. */
    -webkit-tap-highlight-color: transparent;
    scroll-behavior: smooth;
}

body {
    margin: 0;
    min-height: 100vh;
    min-height: 100dvh;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: 15px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
p {
    margin: 0;
}

button,
input,
select {
    font: inherit;
    color: inherit;
}

/* Disables double-tap-to-zoom on controls, which removes the tap delay. */
button,
a,
label,
select {
    touch-action: manipulation;
}

a {
    color: inherit;
    text-decoration: none;
}

img {
    max-width: 100%;
}

::selection {
    background: rgba(52, 211, 153, 0.3);
}

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--r-xs);
}

/* Inline SVG sprite (templates/_icons.html): present in the DOM so
   <use href="#..."> works, but takes no space. */
.icon-sprite {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

/* Outline (stroke) icons from the sprite: colour follows the text colour. */
.icon {
    width: 18px;
    height: 18px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* "Skip to content" link for keyboard users: parked above the viewport and
   slides into view when focused. */
.skip-link {
    position: absolute;
    left: 16px;
    top: -60px;
    z-index: 100;
    padding: 8px 14px;
    border-radius: var(--r-sm);
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 600;
    transition: top var(--fast);
}

.skip-link:focus {
    top: 12px;
}

/* ==========================================================================
   3. Background
   --------------------------------------------------------------------------
   .backdrop is a fixed, click-through layer behind everything (z-index -1):
   two soft colour glows plus a faint 32px grid (::after) that fades out
   towards the bottom through a radial mask.
   ========================================================================== */

.backdrop {
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(900px 480px at 15% -10%, rgba(16, 185, 129, 0.14), transparent 60%),
        radial-gradient(700px 420px at 95% 0%, rgba(59, 130, 246, 0.08), transparent 60%),
        var(--bg);
}

.backdrop::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
    background-size: 32px 32px;
    -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 75%);
    mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 75%);
}

/* ==========================================================================
   4. Buttons
   --------------------------------------------------------------------------
   .btn + one variant (.btn-primary, .btn-ghost, .btn-danger, .btn-danger-ghost)
   + optional size (.btn-sm, .btn-icon, .btn-block). .is-loading shows the
   button's spinner and blocks clicks while a request is running.
   ========================================================================== */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 40px;
    padding: 0 16px;
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    background: none;
    font-size: 14px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition: background var(--fast), border-color var(--fast), color var(--fast),
        transform var(--fast), box-shadow var(--fast), opacity var(--fast);
}

.btn:active:not(:disabled) {
    transform: scale(0.97);
}

.btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.btn .icon {
    width: 16px;
    height: 16px;
}

.btn-sm {
    height: 34px;
    padding: 0 12px;
    font-size: 13px;
}

.btn-block {
    width: 100%;
}

.btn-primary {
    background: linear-gradient(180deg, #4ade80, var(--accent-strong));
    color: var(--accent-ink);
    box-shadow: 0 0 0 1px rgba(52, 211, 153, 0.4) inset, 0 8px 24px -10px var(--accent-glow);
}

.btn-primary:hover:not(:disabled) {
    box-shadow: 0 0 0 1px rgba(110, 231, 183, 0.7) inset, 0 10px 30px -8px rgba(52, 211, 153, 0.5);
    filter: brightness(1.06);
}

.btn-ghost {
    border-color: var(--line);
    background: rgba(255, 255, 255, 0.03);
    color: var(--text-2);
}

.btn-ghost:hover:not(:disabled) {
    border-color: var(--line-strong);
    background: rgba(255, 255, 255, 0.07);
    color: var(--text);
}

/* Toggle buttons (e.g. console pause) show the "on" state in amber. */
.btn-ghost[aria-pressed="true"] {
    border-color: var(--warning-line);
    background: var(--warning-soft);
    color: var(--warning);
}

.btn-danger {
    background: linear-gradient(180deg, #f87171, #dc2626);
    color: #1f0505;
    box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.45) inset, 0 8px 24px -10px rgba(248, 113, 113, 0.45);
}

.btn-danger:hover:not(:disabled) {
    filter: brightness(1.08);
}

/* Confirm button for actions that are risky but not destructive. */
.btn-danger.is-warning {
    background: linear-gradient(180deg, #fcd34d, #f59e0b);
    color: #1f1402;
    box-shadow: 0 0 0 1px rgba(251, 191, 36, 0.45) inset, 0 8px 24px -10px rgba(251, 191, 36, 0.45);
}

/* Quiet until hovered, then turns red ("Remove" / "Delete" in lists). */
.btn-danger-ghost {
    border-color: var(--line);
    background: rgba(255, 255, 255, 0.03);
    color: var(--text-2);
}

.btn-danger-ghost:hover:not(:disabled) {
    border-color: var(--danger-line);
    background: var(--danger-soft);
    color: var(--danger);
}

.btn-icon {
    width: 34px;
    height: 34px;
    padding: 0;
}

.btn .spinner {
    display: none;
}

.btn.is-loading .spinner {
    display: inline-block;
}

.btn.is-loading {
    pointer-events: none;
}

.spinner {
    width: 16px;
    height: 16px;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}

/* ==========================================================================
   5. Badges & pills
   ========================================================================== */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 24px;
    padding: 0 10px;
    border: 1px solid var(--accent-line);
    border-radius: var(--r-pill);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
}

.badge-muted {
    border-color: var(--line);
    background: var(--muted-soft);
    color: var(--text-2);
}

.badge-danger {
    border-color: var(--danger-line);
    background: var(--danger-soft);
    color: var(--danger);
}

/* Online/offline pill in the top bar. It defaults to the "offline" colours;
   body[data-online="true"] swaps the local --tone variables. */
.status-pill {
    --tone: var(--danger);
    --tone-soft: var(--danger-soft);
    --tone-line: var(--danger-line);
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 34px;
    padding: 0 14px;
    border: 1px solid var(--tone-line);
    border-radius: var(--r-pill);
    background: var(--tone-soft);
    color: var(--tone);
    font-size: 13px;
    font-weight: 600;
    transition: color var(--med), background var(--med), border-color var(--med);
}

body[data-online="true"] .status-pill {
    --tone: var(--accent);
    --tone-soft: var(--accent-soft);
    --tone-line: var(--accent-line);
}

.status-dot {
    position: relative;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--tone);
}

/* Expanding "ping" ring while the server is online. */
body[data-online="true"] .status-dot::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--tone);
    animation: ping 1.8s var(--ease) infinite;
}

/* "Updated Xs ago" indicator: breathing green dot, amber when the data is
   stale (or the console is paused), red when polling fails. */
.live-indicator {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--text-3);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

.live-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
    animation: breathe 2.4s ease-in-out infinite;
}

.live-indicator.is-stale .live-dot,
.terminal-state.is-paused .live-dot {
    background: var(--warning);
    box-shadow: 0 0 0 3px var(--warning-soft);
    animation: none;
}

.live-indicator.is-error .live-dot {
    background: var(--danger);
    box-shadow: 0 0 0 3px var(--danger-soft);
    animation: none;
}

/* ==========================================================================
   6. Top bar
   --------------------------------------------------------------------------
   Sticky, translucent bar (z-index 20). padding-top adds the safe-area inset
   so the bar clears the notch / status bar when the dashboard is opened as a
   home-screen web app; --topbar-h is the height of the content below it.
   ========================================================================== */

.topbar {
    position: sticky;
    top: 0;
    z-index: 20;
    padding-top: env(safe-area-inset-top, 0px);
    border-bottom: 1px solid var(--line);
    background: rgba(6, 8, 11, 0.72);
    -webkit-backdrop-filter: saturate(160%) blur(14px);
    backdrop-filter: saturate(160%) blur(14px);
}

.topbar-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    max-width: 1280px;
    height: var(--topbar-h);
    margin: 0 auto;
    padding: 0 clamp(16px, 3vw, 32px);
}

.brand {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
}

.brand-mark {
    width: 34px;
    height: 34px;
    flex: none;
    filter: drop-shadow(0 6px 14px rgba(52, 211, 153, 0.35));
    transition: transform var(--med);
}

.brand:hover .brand-mark {
    transform: rotate(-8deg) scale(1.06);
}

.brand-text {
    display: flex;
    flex-direction: column;
    line-height: 1.15;
    min-width: 0;
}

.brand-text strong {
    font-size: 16px;
    font-weight: 700;
    letter-spacing: -0.01em;
}

.brand-text small {
    color: var(--text-3);
    font-size: 12px;
}

.topbar-actions {
    display: flex;
    align-items: center;
    gap: 12px;
}

/* ==========================================================================
   7. Layout
   --------------------------------------------------------------------------
   The page shell (.app), the views (one per tab) and the grid of each view.
   Grids use named areas so the media queries in section 25 can rearrange the
   cards without touching the HTML. minmax(0, 1fr) instead of 1fr lets a
   column shrink below its content's width (long names, log lines).
   ========================================================================== */

:root {
    --gap: 20px;            /* space between cards */
    --page-max: 1440px;
    --page-pad: clamp(16px, 3vw, 32px);
    --tabbar-h: 0px;        /* set to 64px when the bottom tab bar shows */
}

.app {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap);
    max-width: var(--page-max);
    margin: 0 auto;
    /* Bottom padding keeps the last card clear of the bottom tab bar and
       the home indicator (safe-area inset) on phones. */
    padding: 24px var(--page-pad) calc(32px + var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
}

/* Only the active tab's view is shown. Without JavaScript (no .js class)
   every view is shown, one after another. */
.js body[data-admin="true"] .view:not(.is-active) {
    display: none;
}

.view {
    min-width: 0;
    /* Anchor jumps (#overview, #storage...) stop below the sticky top bar. */
    scroll-margin-top: calc(var(--topbar-h) + 16px);
}

.view > * + * {
    margin-top: var(--gap);
}

/* Overview grid. On phones everything is one column in this order; from
   1100px status, controls and players sit on the left and the console fills
   the rest of the screen on the right (see section 25). */
.ov-grid {
    display: grid;
    gap: var(--gap);
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "hero"
        "controls"
        "console"
        "players";
}

.ov-grid > .hero { grid-area: hero; }
.ov-grid > .controls-card { grid-area: controls; }
.ov-grid > .ov-players { grid-area: players; }
.ov-grid > .console-card { grid-area: console; }
.ov-grid > .cta-card { grid-area: console; } /* guests: sign-in card takes the console's place */

/* Guests (not signed in): status + players + sign-in card. */
body[data-admin="false"] .ov-grid {
    grid-template-areas:
        "hero"
        "players"
        "console";
}

.server-grid,
.players-grid {
    display: grid;
    gap: var(--gap);
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
}

/* ==========================================================================
   8. Cards
   ========================================================================== */

.card {
    position: relative;
    min-width: 0;
    padding: 22px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0) 40%), var(--panel);
    box-shadow: var(--shadow-card);
    animation: rise 420ms var(--ease) both;
}

/* Stagger the entrance animation of the cards in each grid. */
.ov-grid > :nth-child(2),
.server-grid > :nth-child(2),
.players-grid > :nth-child(2) { animation-delay: 40ms; }
.ov-grid > :nth-child(3) { animation-delay: 80ms; }
.ov-grid > :nth-child(4) { animation-delay: 120ms; }

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

.card-title {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
}

.card-title h2 {
    font-size: 16px;
    font-weight: 650;
    letter-spacing: -0.01em;
}

.card-sub {
    color: var(--text-3);
    font-size: 13px;
}

.card-icon {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    flex: none;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--panel-3);
    color: var(--accent);
}

.toolbar {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* ==========================================================================
   9. Hero
   --------------------------------------------------------------------------
   The status card at the top of the overview: title, "online/offline" eyebrow,
   a strip of facts (version, world, ...) and the player gauge. Colours follow
   body[data-online].
   ========================================================================== */

.hero {
    display: flex;
    flex-direction: column;
    gap: 18px;
    overflow: hidden;
}

.hero-top {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
}

/* Facts strip: the 1px gap over a --line background draws the dividers
   between the cells. */
.facts {
    position: relative;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1px;
    margin: 0;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--line);
}

.fact {
    min-width: 0;
    padding: 10px 12px;
    background: var(--panel-2);
}

.fact dt {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--text-3);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.fact dt .icon {
    width: 13px;
    height: 13px;
}

.fact dd {
    margin: 2px 0 0;
    overflow: hidden;
    font-size: 15px;
    font-weight: 650;
    text-overflow: ellipsis;
    white-space: nowrap;
}

body[data-online="true"] .fact [data-field="status"] {
    color: var(--accent);
}

body[data-online="false"] .fact [data-field="status"] {
    color: var(--danger);
}

/* Decorative glow in the top-right corner (clipped by .hero's overflow). */
.hero-glow {
    position: absolute;
    inset: -40% -10% auto auto;
    width: 520px;
    height: 520px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(52, 211, 153, 0.16), transparent 65%);
    pointer-events: none;
    transition: opacity 600ms var(--ease);
}

body[data-online="false"] .hero-glow {
    background: radial-gradient(circle, rgba(248, 113, 113, 0.1), transparent 65%);
}

.hero-main {
    position: relative;
    min-width: 0;
}

.eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 14px;
    padding: 5px 12px 5px 10px;
    border: 1px solid var(--danger-line);
    border-radius: var(--r-pill);
    background: var(--danger-soft);
    color: var(--danger);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.02em;
    transition: all var(--med);
}

body[data-online="true"] .eyebrow {
    border-color: var(--accent-line);
    background: var(--accent-soft);
    color: var(--accent);
}

.eyebrow .icon {
    width: 14px;
    height: 14px;
}

.hero-title {
    font-size: clamp(28px, 4vw, 40px);
    font-weight: 750;
    line-height: 1.02;
    letter-spacing: -0.035em;
    overflow-wrap: anywhere;
}

/* Faded gradient text (the word "server" in the title). */
.hero-title-muted {
    background: linear-gradient(90deg, var(--text-3), rgba(107, 118, 132, 0.4));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.hero-sub {
    margin-top: 12px;
    color: var(--text-2);
    font-size: 15px;
}

.hero-sub strong {
    color: var(--text);
    font-weight: 600;
}

/* --- Player gauge --- */
/* Circular "players online" meter. The SVG is rotated so the arc starts at
   12 o'clock; stroke-dasharray on .gauge-fill (set by the template, then
   by app.js) shows the fraction. The gradient is defined inside the SVG
   (#gauge-gradient). */

.gauge {
    position: relative;
    width: 108px;
    height: 108px;
    flex: none;
}

.gauge svg {
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);
}

.gauge-track {
    fill: none;
    stroke: rgba(255, 255, 255, 0.06);
    stroke-width: 9;
}

.gauge-fill {
    fill: none;
    stroke: url(#gauge-gradient);
    stroke-width: 9;
    stroke-linecap: round;
    filter: drop-shadow(0 0 6px rgba(52, 211, 153, 0.45));
    transition: stroke-dasharray 700ms var(--ease);
}

.gauge-label {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    line-height: 1.1;
}

.gauge-label strong {
    font-size: 30px;
    font-weight: 750;
    letter-spacing: -0.04em;
    font-variant-numeric: tabular-nums;
}

.gauge-label span {
    color: var(--text-2);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}

.gauge-label small {
    margin-top: 2px;
    color: var(--text-3);
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* Added by app.js for a moment when the player count changes. */
.bump {
    animation: bump 420ms var(--ease);
}

/* ==========================================================================
   10. Players
   --------------------------------------------------------------------------
   Online player tiles (overview) and the shared avatar and empty-state styles.
   ========================================================================== */

/* As many ~170px tiles per row as fit. */
.player-list {
    display: grid;
    align-content: start;
    grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.player {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    padding: 8px 12px 8px 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--panel-2);
    transition: border-color var(--fast), background var(--fast), transform var(--fast);
    animation: pop 320ms var(--ease) both;
}

.player:hover {
    border-color: var(--accent-line);
    background: var(--panel-3);
    transform: translateY(-1px);
}

.player.is-leaving {
    animation: fade-out 260ms var(--ease) forwards;
}

.avatar {
    position: relative;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    flex: none;
    overflow: hidden;
    border-radius: var(--r-xs);
    background: linear-gradient(135deg, #1f7a55, #0f3d2b);
    color: #d1fae5;
    font-weight: 700;
    image-rendering: pixelated;
}

/* The player's initial shows until the skin image loads, and stays when it
   fails (.is-fallback hides the broken image). */
.avatar::before {
    content: attr(data-initial);
}

.avatar-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    image-rendering: pixelated;
}

.avatar.is-fallback .avatar-img {
    display: none;
}

.player-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.player-online {
    width: 8px;
    height: 8px;
    flex: none;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 10px var(--accent-glow);
}

.empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 28px 16px;
    border: 1px dashed var(--line-strong);
    border-radius: var(--r-md);
    color: var(--text-3);
    text-align: center;
}

.empty .icon {
    width: 26px;
    height: 26px;
    opacity: 0.6;
}

/* ==========================================================================
   11. Controls & job dock
   --------------------------------------------------------------------------
   Server control buttons (.ctl: start, stop, restart, backup...) and the
   floating panel that follows a background job.
   ========================================================================== */

/* All controls in one row: as many ~88px buttons as fit, wrapping when
   needed (the button layout is in "Compact controls" below). */
.control-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
    gap: 10px;
}

/* Each button sits in its own <form>; "contents" makes the buttons the
   grid items instead of the forms. */
.control-grid form {
    display: contents;
}

/* A control button. data-tone picks its colour (green by default) through
   the local --tone variables; ::after is a glow that fades in on hover. */
.ctl {
    --tone: var(--accent);
    --tone-soft: var(--accent-soft);
    --tone-line: var(--accent-line);
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 72px;
    padding: 14px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--panel-2);
    text-align: left;
    cursor: pointer;
    transition: border-color var(--fast), background var(--fast), transform var(--fast), box-shadow var(--fast);
}

.ctl[data-tone="danger"] {
    --tone: var(--danger);
    --tone-soft: var(--danger-soft);
    --tone-line: var(--danger-line);
}

.ctl[data-tone="warning"] {
    --tone: var(--warning);
    --tone-soft: var(--warning-soft);
    --tone-line: var(--warning-line);
}

.ctl[data-tone="info"] {
    --tone: var(--info);
    --tone-soft: var(--info-soft);
    --tone-line: var(--info-line);
}

.ctl::after {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(260px 120px at 0% 0%, var(--tone-soft), transparent 70%);
    opacity: 0;
    transition: opacity var(--med);
    pointer-events: none;
}

.ctl:hover:not(:disabled) {
    border-color: var(--tone-line);
    background: var(--panel-3);
    transform: translateY(-2px);
    box-shadow: 0 12px 28px -16px var(--tone);
}

.ctl:hover:not(:disabled)::after {
    opacity: 1;
}

.ctl:active:not(:disabled) {
    transform: translateY(0) scale(0.98);
}

.ctl:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* The button whose job is running: stays fully visible while the others are
   disabled, and its icon spins. */
.ctl.is-busy {
    opacity: 1;
    border-color: var(--tone-line);
}

.ctl-icon {
    position: relative;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: var(--r-sm);
    background: var(--tone-soft);
    color: var(--tone);
}

.ctl.is-busy .ctl-icon .icon {
    animation: spin 1s linear infinite;
}

.ctl-text {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.25;
}

.ctl-text strong {
    font-size: 14px;
    font-weight: 650;
}

.ctl-text small {
    color: var(--text-3);
    font-size: 12px;
}

/* --- Compact controls --- */
/* Icon above label; the description line is hidden to keep them small. */
.control-grid .ctl {
    flex-direction: column;
    justify-content: center;
    min-height: 0;
    padding: 14px 6px 12px;
    gap: 8px;
    text-align: center;
}

.control-grid .ctl-text small {
    display: none;
}

/* --- Job dock --- */
/* Floating panel in the bottom-right corner that shows the running
   background job and its output (app.js fills #job). It sits above the
   bottom tab bar and clear of the home indicator / rounded corners
   (safe-area insets). The dock itself ignores clicks so the page under its
   empty area stays usable; only the job card inside takes them. */
.job-dock {
    position: fixed;
    right: max(16px, env(safe-area-inset-right, 0px));
    bottom: calc(16px + var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
    z-index: 40;
    width: min(440px, calc(100vw - 32px));
    pointer-events: none;
    border-radius: var(--r-md);
    background: var(--panel-2);
    box-shadow: var(--shadow-pop);
}

.job-dock .job {
    pointer-events: auto;
}

.job {
    position: relative;
    margin-top: 0;
    overflow: hidden;
    border: 1px solid var(--info-line);
    border-radius: var(--r-md);
    background: var(--info-soft);
    animation: pop 260ms var(--ease) both;
}

/* data-state is "running", "completed" or "failed" (set by app.js). */
.job[data-state="completed"] {
    border-color: var(--accent-line);
    background: var(--accent-soft);
}

.job[data-state="failed"] {
    border-color: var(--danger-line);
    background: var(--danger-soft);
}

.job-bar {
    height: 3px;
    overflow: hidden;
    background: rgba(96, 165, 250, 0.15);
}

/* Indeterminate progress bar: a 40% stripe sliding across. */
.job-bar span {
    display: block;
    width: 40%;
    height: 100%;
    border-radius: 3px;
    background: var(--info);
    animation: indeterminate 1.2s var(--ease) infinite;
}

.job:not([data-state="running"]) .job-bar {
    visibility: hidden;
}

.job-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 12px 12px 14px;
}

.job-icon {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    flex: none;
    color: var(--info);
}

/* Only the icon that matches the job state is shown. */
.job-icon > * {
    display: none;
}

.job[data-state="running"] .spinner,
.job[data-state="completed"] .icon-ok,
.job[data-state="failed"] .icon-fail {
    display: block;
}

.job[data-state="completed"] .job-icon {
    color: var(--accent);
}

.job[data-state="failed"] .job-icon {
    color: var(--danger);
}

.job-text {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}

.job-text small {
    color: var(--text-2);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

.job-output {
    max-height: min(260px, 40vh);
    margin: 0 12px 12px;
    padding: 10px 12px;
    overflow-x: hidden;
    overflow-y: auto;
    overflow-wrap: anywhere;
    border-radius: var(--r-sm);
    background: rgba(0, 0, 0, 0.35);
    color: var(--text-2);
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.55;
    white-space: pre-wrap;
    word-break: break-word;
}

/* ==========================================================================
   12. Console
   --------------------------------------------------------------------------
   Live server log in a terminal-style frame: title bar with filter box and
   state, the scrolling log (.console) and the "jump to latest" button. On
   desktop the console stretches to fill the overview (section 25).
   ========================================================================== */

.console-card {
    display: flex;
    flex-direction: column;
}

.terminal {
    position: relative;
    display: flex;
    flex: 1;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--terminal);
}

.terminal-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 10px 8px 14px;
    border-bottom: 1px solid var(--line);
    background: rgba(255, 255, 255, 0.02);
}

.terminal-dots {
    display: flex;
    gap: 6px;
    flex: none;
}

.terminal-dots i {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #2a313b;
}

/* Decorative "traffic light" dots. */
.terminal-dots i:nth-child(1) { background: #f87171; opacity: 0.7; }
.terminal-dots i:nth-child(2) { background: #fbbf24; opacity: 0.7; }
.terminal-dots i:nth-child(3) { background: #34d399; opacity: 0.7; }

.terminal-search {
    display: flex;
    flex: 1;
    align-items: center;
    gap: 8px;
    min-width: 0;
    height: 30px;
    padding: 0 10px;
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-3);
    transition: border-color var(--fast), background var(--fast);
}

.terminal-search:focus-within {
    border-color: var(--accent-line);
    background: rgba(255, 255, 255, 0.06);
}

.terminal-search .icon {
    width: 14px;
    height: 14px;
}

.terminal-search input {
    flex: 1;
    min-width: 0;
    border: 0;
    outline: 0;
    background: none;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 12px;
}

.terminal-search input::placeholder {
    color: var(--text-3);
}

.terminal-state {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    flex: none;
    color: var(--text-3);
    font-size: 12px;
}

.console {
    flex: 1;
    min-width: 0;
    height: 420px;
    min-height: 0;
    margin: 0;
    padding: 14px 10px 20px;
    /* Long lines wrap instead of scrolling sideways. */
    overflow-x: hidden;
    overflow-y: auto;
    color: #c9d3de;
    font-family: var(--font-mono);
    font-size: 12.5px;
    line-height: 1.65;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    word-break: break-word;
    tab-size: 4;
    /* Scrolling the log doesn't scroll the page once it reaches the end,
       and touch drags only scroll vertically. */
    overscroll-behavior: contain;
    touch-action: pan-y;
    scrollbar-width: thin;
    scrollbar-color: #2a313b transparent;
}

/* Draw the focus ring inside, so the frame's overflow doesn't clip it. */
.console:focus-visible {
    outline-offset: -2px;
}

.console .ln {
    display: block;
    max-width: 100%;
    /* Padding (not negative margins) so the hover highlight can't push
       content past the edge and create a horizontal scrollbar. */
    padding: 0 6px;
    border-radius: 4px;
}

.console .ln:hover {
    background: rgba(255, 255, 255, 0.035);
}

/* One .ln per log line; app.js adds a type class for colouring, .ts for the
   timestamp and <mark> for filter matches. */
.console .ln-warn { color: #fcd34d; }
.console .ln-error { color: #fca5a5; background: rgba(248, 113, 113, 0.06); }
.console .ln-join { color: #6ee7b7; }
.console .ln-chat { color: #93c5fd; }
.console .ln-ok { color: #34d399; font-weight: 600; }

.console .ts {
    color: #5b6573;
}

.console mark {
    border-radius: 3px;
    background: rgba(251, 191, 36, 0.28);
    color: #fff;
}

.console-empty {
    color: var(--text-3);
    font-style: italic;
}

.jump-latest {
    position: absolute;
    right: 16px;
    bottom: 16px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 32px;
    padding: 0 12px;
    border: 1px solid var(--accent-line);
    border-radius: var(--r-pill);
    background: rgba(6, 30, 21, 0.92);
    color: var(--accent);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    box-shadow: var(--shadow-pop);
    animation: pop 200ms var(--ease) both;
}

.jump-latest .icon {
    width: 14px;
    height: 14px;
}

/* Toggle buttons with two icons: show the one matching aria-pressed. */
.icon-when-on {
    display: none;
}

[aria-pressed="true"] .icon-when-on {
    display: block;
}

[aria-pressed="true"] .icon-when-off {
    display: none;
}

/* ==========================================================================
   13. Server setup
   --------------------------------------------------------------------------
   The version / loader / world switch form (#profile-form): segmented pickers,
   selects, the "what will happen" plan list and the update note. The shared
   form field styles (.select, .text-input, .field-help) live here too, plus a
   few global helpers (code, .visually-hidden).
   ========================================================================== */

.profile-form {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.profile-form > .btn {
    height: 48px;
}

.label-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.link-btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 28px;
    padding: 0 4px;
    border: 0;
    background: none;
    color: var(--text-3);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
}

.link-btn:hover {
    color: var(--accent);
}

.link-btn .icon {
    width: 13px;
    height: 13px;
}

/* A disabled link button is busy (e.g. while checking for new versions). */
.link-btn:disabled .icon {
    animation: spin 1s linear infinite;
}

.field-help.is-warn {
    color: var(--warning);
}

/* Options that still need a download first. */
.seg.is-download small {
    color: var(--info);
}

/* Switch plan: bullet list of what the switch will do, filled by app.js.
   Hidden while empty; bullet colour follows the item's severity. */
.plan {
    display: grid;
    gap: 6px;
    margin: 0;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--panel-2);
    list-style: none;
    font-size: 13px;
    line-height: 1.45;
}

.plan:empty {
    display: none;
}

.plan li {
    position: relative;
    padding-left: 16px;
    color: var(--text);
}

.plan li::before {
    content: "";
    position: absolute;
    top: 0.55em;
    left: 2px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
}

.plan li.is-muted {
    color: var(--text-3);
}

.plan li.is-muted::before {
    background: var(--text-3);
}

.plan li.is-warn {
    color: var(--warning);
}

.plan li.is-warn::before {
    background: var(--warning);
}

.plan li.is-danger {
    color: var(--danger);
}

.plan li.is-danger::before {
    background: var(--danger);
}

.profile-group {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

.profile-group > legend {
    margin-bottom: 8px;
    padding: 0;
}

/* Segmented control: radio buttons styled as a row of three tiles. The real
   <input> is stretched invisibly over its tile so clicks, keyboard and
   screen readers all work normally; the look comes from ".seg-body"
   right after it (input:checked + .seg-body). */
.segmented {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 4px;
    padding: 4px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--terminal);
}

.seg {
    position: relative;
    min-width: 0;
    cursor: pointer;
}

.seg input {
    position: absolute;
    inset: 0;
    margin: 0;
    opacity: 0;
    cursor: inherit;
}

.seg-body {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 44px;
    padding: 6px 10px;
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    line-height: 1.2;
    text-align: center;
    transition: background var(--fast), border-color var(--fast), color var(--fast);
}

.seg-body strong {
    font-size: 14px;
    font-weight: 650;
}

.seg-body small {
    overflow: hidden;
    color: var(--text-3);
    font-size: 11px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.seg:hover input:not(:disabled) + .seg-body {
    background: rgba(255, 255, 255, 0.04);
}

.seg input:checked + .seg-body {
    border-color: var(--accent-line);
    background: var(--accent-soft);
    color: var(--accent);
}

.seg input:checked + .seg-body small {
    color: var(--text-2);
}

.seg input:focus-visible + .seg-body {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.seg input:disabled + .seg-body {
    opacity: 0.45;
}

.seg:has(input:disabled) {
    cursor: not-allowed;
}

.select-wrap {
    position: relative;
}

/* Custom dropdown arrow (the native one is removed with appearance: none). */
.select-wrap::after {
    content: "";
    position: absolute;
    top: 50%;
    right: 16px;
    width: 8px;
    height: 8px;
    border-right: 2px solid var(--text-3);
    border-bottom: 2px solid var(--text-3);
    transform: translateY(-70%) rotate(45deg);
    pointer-events: none;
}

.select,
.text-input {
    width: 100%;
    height: 46px;
    padding: 0 14px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    outline: none;
    background: var(--terminal);
    font-size: 15px;
    transition: border-color var(--fast), box-shadow var(--fast);
}

.select {
    padding-right: 40px;
    -webkit-appearance: none;
    appearance: none;
    text-overflow: ellipsis;
    cursor: pointer;
}

.select:focus,
.text-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 4px var(--accent-soft);
}

/* :user-invalid only matches after the user has interacted with the field. */
.text-input:user-invalid {
    border-color: var(--danger-line);
}

.field-help {
    color: var(--text-3);
    font-size: 12px;
}

.profile-note {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: 0;
    color: var(--text-3);
    font-size: 12.5px;
    line-height: 1.45;
}

.profile-note .icon {
    width: 15px;
    height: 15px;
    margin-top: 1px;
    color: var(--warning);
}

/* app.js adds .is-unchanged when there is nothing to apply (the form
   matches the current setup, or has a problem). */
.profile-form.is-unchanged > .btn {
    filter: saturate(0.4);
    opacity: 0.7;
}

/* "Update available" note above the switch form. */
.update-note {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: -4px 0 16px;
    padding: 12px 14px;
    border: 1px solid var(--info-line);
    border-radius: var(--r-md);
    background: var(--info-soft);
    color: var(--text-2);
    font-size: 13px;
}

.update-note > .icon {
    margin-top: 2px;
    color: var(--info);
}

.update-note strong {
    margin-right: 6px;
    color: var(--text);
}

.update-body {
    flex: 1;
    min-width: 0;
}

.update-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 8px;
}

.update-note small {
    display: block;
    margin-top: 2px;
    color: var(--text-3);
}

/* Inline code (global). */
code {
    padding: 1px 5px;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.06);
    font-family: var(--font-mono);
    font-size: 0.92em;
}

/* Small blue dot next to a segment label (e.g. "update available"). */
.seg-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-left: 6px;
    border-radius: 50%;
    background: var(--info);
    vertical-align: middle;
    box-shadow: 0 0 0 3px var(--info-soft);
}

/* Hidden on screen but still read by screen readers. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* ==========================================================================
   14. Whitelist
   --------------------------------------------------------------------------
   The whitelist on/off switch (a <button role="switch">), the add-player form
   and the list of allowed players.
   ========================================================================== */

.switch {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-height: 34px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}

.switch-track {
    position: relative;
    width: 44px;
    height: 26px;
    flex: none;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-pill);
    background: var(--panel-3);
    transition: background var(--fast), border-color var(--fast);
}

.switch-thumb {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--text-2);
    transition: transform var(--med), background var(--fast);
}

.switch[aria-checked="true"] {
    color: var(--accent);
}

.switch[aria-checked="true"] .switch-track {
    border-color: var(--accent-line);
    background: var(--accent-soft);
}

.switch[aria-checked="true"] .switch-thumb {
    background: var(--accent);
    transform: translateX(18px);
}

/* Disabled while the change is being saved. */
.switch:disabled {
    opacity: 0.5;
    cursor: wait;
}

.access-state {
    margin-bottom: 14px;
    padding: 10px 12px;
    border-radius: var(--r-sm);
    background: var(--warning-soft);
    color: var(--warning);
    font-size: 13px;
}

.access-state[data-enabled="true"] {
    background: var(--accent-soft);
    color: var(--accent);
}

.add-player {
    display: flex;
    gap: 8px;
    margin-bottom: 14px;
}

.add-player .text-input {
    flex: 1;
    min-width: 0;
}

.add-player .btn {
    height: 46px;
}

.whitelist {
    display: grid;
    gap: 6px;
    max-height: 360px;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    list-style: none;
    overscroll-behavior: contain;
}

.wl-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 6px 6px 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--panel-2);
}

.wl-item .avatar {
    width: 32px;
    height: 32px;
}

.wl-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wl-name .badge {
    height: 20px;
    margin-left: 4px;
    padding: 0 7px;
    font-size: 11px;
    vertical-align: middle;
}

/* ==========================================================================
   15. World settings
   --------------------------------------------------------------------------
   World picker, the Minecraft server-list preview (icon + MOTD), the settings
   form (grid of options and toggles), resource pack and world generation /
   seed sections.
   ========================================================================== */

.select-sm .select {
    height: 38px;
    min-width: 170px;
    font-size: 14px;
}

.world-settings {
    display: grid;
    gap: 16px;
    transition: opacity var(--fast);
}

/* Dimmed and not clickable while the selected world's settings load. */
.world-settings.is-loading {
    opacity: 0.55;
    pointer-events: none;
}

/* Imitates the in-game server list: dirt-block checkerboard (a repeating
   conic gradient) darkened by a translucent overlay. */
.mc-preview {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: var(--r-xs);
    background:
        linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55)),
        repeating-conic-gradient(#3b2a1c 0 25%, #4a3524 0 50%) 0 0 / 16px 16px;
    font-family: var(--font-mono);
}

.mc-icon-wrap {
    flex: none;
    width: 64px;
    height: 64px;
    background: rgba(0, 0, 0, 0.35);
}

.mc-icon {
    display: block;
    width: 64px;
    height: 64px;
    image-rendering: pixelated;
}

.mc-icon.is-default {
    opacity: 0.8;
}

.mc-text {
    flex: 1;
    min-width: 0;
    font-size: 13px;
    line-height: 1.45;
}

.mc-row {
    display: flex;
    justify-content: space-between;
    gap: 8px;
}

.mc-name {
    overflow: hidden;
    color: #fff;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mc-players {
    flex: none;
    color: #aaaaaa;
}

.mc-motd {
    min-height: 38px;
    overflow: hidden;
}

/* white-space: pre keeps the MOTD's spacing exactly as the game shows it. */
.mc-line {
    min-height: 19px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: pre;
}

/* Stand-in for Minecraft's "obfuscated" (§k) text. */
.mc-obf {
    filter: blur(2px);
}

.icon-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 10px;
}

/* Button that opens a file picker: the real <input type="file"> covers the
   button invisibly, and the focus ring is drawn on the button. */
.file-btn {
    position: relative;
    overflow: hidden;
}

.file-btn input {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
}

.file-btn:focus-within {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.settings-form {
    display: grid;
    gap: 16px;
}

.motd-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
}

.textarea {
    height: auto;
    min-height: 70px;
    padding: 10px 14px;
    font-family: var(--font-mono);
    line-height: 1.5;
    resize: vertical;
}

.color-codes {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

/* Buttons that insert a Minecraft colour/format code into the MOTD. */
.code-chip {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--panel-2);
    color: var(--text-2);
    font-size: 12px;
    cursor: pointer;
}

.code-chip:hover {
    border-color: var(--line-strong);
    color: var(--text);
}

.code-swatch {
    width: 14px;
    height: 14px;
    border-radius: 3px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}

.settings-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 16px;
}

.settings-grid .profile-group.is-unsupported {
    opacity: 0.45;
}

/* Checkbox rows (and full-width notes) span both columns of the settings
   grid. */
.toggle-row {
    grid-column: 1 / -1;
}

.check {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    cursor: pointer;
}

.check input {
    width: 20px;
    height: 20px;
    margin: 0;
    accent-color: var(--accent-strong);
}

.check small {
    color: var(--text-3);
    font-weight: 400;
}

/* "Live" tag: the setting applies right away while the world is running. */
.live-tag {
    display: inline-block;
    margin-left: 4px;
    padding: 1px 6px;
    border-radius: var(--r-pill);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    vertical-align: 1px;
}

.settings-result {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 14px;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--panel-2);
    font-size: 13px;
}

.settings-result p {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    color: var(--text-2);
}

.settings-result .icon {
    width: 15px;
    height: 15px;
    color: var(--accent);
}

/* Pushes an action to the right end of a flex row. */
.inline-action {
    margin-left: auto;
}

.settings-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px 16px;
}

.settings-actions .field-help {
    flex: 1;
    min-width: 200px;
}

/* --- Settings sections (resource pack, world generation) --- */
/* These are <fieldset>s: the reset below removes the default fieldset
   frame and a top border separates them. */
.settings-section {
    display: grid;
    gap: 14px;
    min-width: 0;
    margin: 4px 0 0;
    padding: 16px 0 0;
    border: 0;
    border-top: 1px solid var(--line);
}

/* <legend> hack: a floated legend stops being drawn on the fieldset
   border and behaves like a normal heading inside the box; the next
   element clears the float. */
.section-title {
    float: left;
    width: 100%;
    margin: 0 0 2px;
    padding: 0;
    color: var(--text);
    font-size: 14px;
    font-weight: 650;
}

.section-title + * {
    clear: both;
}

.input-row {
    display: flex;
    gap: 8px;
    min-width: 0;
}

.input-row .text-input {
    flex: 1;
    min-width: 0;
}

.input-row .btn {
    flex: none;
}

.mono {
    font-family: var(--font-mono);
    font-size: 13px;
}

.pack-status {
    margin: 2px 0 0;
    color: var(--text-3);
    font-size: 12.5px;
    overflow-wrap: anywhere;
}

.pack-status.is-ok { color: var(--accent); }
.pack-status.is-warn { color: var(--warning); }
.pack-status.is-error { color: var(--danger); }

/* Spinning icon while the resource pack URL is being checked. */
#pack-check.is-busy .icon {
    animation: spin 900ms linear infinite;
}

.gen-locked {
    display: grid;
    gap: 6px;
}

.seed-display {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    padding: 8px 8px 8px 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--panel-2);
}

.seed-display .field-label {
    margin: 0;
}

.seed-display code {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 14px;
    text-overflow: ellipsis;
    white-space: nowrap;
    user-select: all;       /* one click selects the whole seed */
}

/* --- New-world generation options (in the switch form) --- */
/* A <details> box. The native disclosure triangle is removed
   (list-style / ::-webkit-details-marker) and replaced by a chevron drawn
   with two borders that flips when the box is open. */
.gen-options {
    margin-top: 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--panel-2);
}

.gen-options summary {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 14px;
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.gen-options summary::-webkit-details-marker {
    display: none;
}

.gen-options summary::after {
    content: "";
    flex: none;
    width: 7px;
    height: 7px;
    margin-left: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--fast);
}

.gen-options[open] summary::after {
    transform: translateY(1px) rotate(-135deg);
}

.gen-options summary small {
    min-width: 0;
    overflow: hidden;
    color: var(--text-3);
    font-weight: 400;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.gen-options-body {
    display: grid;
    gap: 12px;
    padding: 4px 14px 12px;
}

/* ==========================================================================
   16. Backups
   --------------------------------------------------------------------------
   Backup schedule strip (automatic on/off, next backup, last trigger) and
   the list of backup files. Row wrapping on narrow cards is handled by the
   @container query in section 17.
   ========================================================================== */

/* Same 1px-gap divider trick as .facts. */
.schedule {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1px;
    margin-bottom: 18px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--line);
}

.schedule-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    padding: 14px 16px;
    background: var(--panel-2);
}

.schedule-label {
    color: var(--text-3);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.schedule-value {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.schedule-rel {
    color: var(--text-3);
    font-size: 12px;
}

.backup-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
}

.backup-item {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 12px 12px 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--panel-2);
    transition: border-color var(--fast), background var(--fast), opacity var(--med), transform var(--med);
    animation: rise 300ms var(--ease) both;
}

.backup-item:hover {
    border-color: var(--line-strong);
    background: var(--panel-3);
}

/* Slides out before app.js removes a deleted backup. */
.backup-item.is-removing {
    opacity: 0;
    transform: translateX(16px);
}

.backup-file-icon {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    flex: none;
    border-radius: var(--r-sm);
    background: var(--info-soft);
    color: var(--info);
}

.backup-info {
    flex: 1;
    min-width: 0;
}

.backup-name {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

.backup-name span {
    overflow: hidden;
    font-family: var(--font-mono);
    font-size: 13px;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.backup-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    margin-top: 3px;
    color: var(--text-3);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

.backup-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: none;
}

.backup-actions form {
    margin: 0;
}

/* ==========================================================================
   17. Storage
   --------------------------------------------------------------------------
   Storage view: disk usage card, worlds card and backups card (section 16),
   plus the low-disk banner shown on every view. The worlds and backups cards
   are size containers, so their rows can react to the card's own width.
   ========================================================================== */

.storage-grid {
    display: grid;
    gap: var(--gap);
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "disk"
        "worlds"
        "backups";
    align-items: start;
}

.storage-grid > .disk-card { grid-area: disk; }
.storage-grid > .worlds-card { grid-area: worlds; animation-delay: 40ms; }
.storage-grid > .backups-card { grid-area: backups; animation-delay: 80ms; }

.card-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.card-actions form {
    margin: 0;
}

/* Spinning icon while the disk usage is being refreshed. */
#disk-refresh.is-busy .icon {
    animation: spin 900ms linear infinite;
}

/* --- Low disk banner (all views) --- */
.disk-alert {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px 10px 16px;
    border: 1px solid var(--warning-line);
    border-radius: var(--r-md);
    background: var(--warning-soft);
    color: var(--warning);
    animation: rise 300ms var(--ease) both;
}

.disk-alert[data-status="critical"] {
    border-color: var(--danger-line);
    background: var(--danger-soft);
    color: var(--danger);
}

.disk-alert p {
    flex: 1;
    min-width: 0;
    color: var(--text);
    font-size: 14px;
}

.disk-alert > .icon {
    flex: none;
}

/* Amber dot on the Storage tab while disk space is low. */
.tab-dot.tab-dot-warn,
.tabbar-item .tab-dot.tab-dot-warn {
    background: var(--warning);
    box-shadow: 0 0 0 3px var(--warning-soft);
}

/* --- Usage bar --- */
/* Stacked bar of what uses the disk. Each segment's colour comes from a
   .seg-* class that sets --seg; the legend swatches use the same classes.
   .disk-threshold is the marker for the low-space warning level. */
.disk-meter {
    --seg-worlds: #34d399;
    --seg-backups: #60a5fa;
    --seg-software: #a78bfa;
    --seg-mods: #f472b6;
    --seg-logs: #fbbf24;
    --seg-other: #94a3b8;
    --seg-system: #475569;
    --seg-reserved: #334155;
    --seg-free: rgba(255, 255, 255, 0.06);
    --seg-used: #475569;
    display: grid;
    gap: 16px;
}

.disk-bar {
    position: relative;
    display: flex;
    height: 16px;
    overflow: visible;
    border-radius: var(--r-pill);
    background: var(--seg-free);
    box-shadow: 0 0 0 1px var(--line) inset;
}

/* Segment widths are set inline as percentages (template / app.js). */
.disk-seg {
    height: 100%;
    min-width: 2px;
    background: var(--seg);
    transition: width var(--med);
}

.disk-seg:first-child {
    border-radius: var(--r-pill) 0 0 var(--r-pill);
}

.disk-seg + .disk-seg {
    box-shadow: 1px 0 0 rgba(0, 0, 0, 0.35) inset;
}

.disk-threshold {
    position: absolute;
    top: -4px;
    bottom: -4px;
    width: 2px;
    margin-left: -1px;
    border-radius: 2px;
    background: var(--warning);
    box-shadow: 0 0 0 2px var(--panel);
}

.disk-meter[data-status="critical"] .disk-threshold {
    background: var(--danger);
}

.seg-worlds { --seg: var(--seg-worlds); }
.seg-backups { --seg: var(--seg-backups); }
.seg-software { --seg: var(--seg-software); }
.seg-mods { --seg: var(--seg-mods); }
.seg-logs { --seg: var(--seg-logs); }
.seg-other { --seg: var(--seg-other); }
.seg-system { --seg: var(--seg-system); }
.seg-reserved { --seg: var(--seg-reserved); }
.seg-free { --seg: var(--seg-free); }
.seg-used { --seg: var(--seg-used); }

.disk-legend {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 6px 24px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.disk-legend .skeleton-row {
    grid-column: 1 / -1;
    height: 48px;
}

.legend-item {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    font-size: 13px;
}

.legend-swatch {
    width: 10px;
    height: 10px;
    flex: none;
    border-radius: 3px;
    background: var(--seg);
    box-shadow: 0 0 0 1px var(--line-strong) inset;
}

.legend-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    color: var(--text-2);
    text-overflow: ellipsis;
    white-space: nowrap;
}

.legend-value {
    color: var(--text);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.disk-note,
.retention-note {
    margin-top: 14px;
    color: var(--text-3);
    font-size: 12.5px;
    line-height: 1.5;
}

/* Same look as .disk-note, but sits above the backup list. */
.retention-note {
    margin: 0 0 12px;
}

/* --- Worlds --- */
/* Make these cards size containers for the @container rules below. */
.worlds-card,
.backups-card {
    container-type: inline-size;
}

.world-list {
    display: grid;
    gap: 8px;
    margin: 0 0 12px;
    padding: 0;
    list-style: none;
}

.world-list .skeleton-row {
    height: 64px;
}

.world-item {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 12px 12px 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--panel-2);
    animation: rise 300ms var(--ease) both;
}

.world-icon {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    flex: none;
    border-radius: var(--r-sm);
    background: var(--accent-soft);
    color: var(--accent);
}

.world-info {
    flex: 1;
    min-width: 0;
}

.world-name {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

.world-name > span:first-child {
    overflow: hidden;
    font-weight: 650;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.world-name .badge,
.backup-name .badge {
    height: 20px;
    padding: 0 8px;
    flex: none;
    font-size: 11px;
}

.world-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 14px;
    margin-top: 3px;
    color: var(--text-3);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

.world-size {
    color: var(--text-2);
    font-weight: 600;
}

.text-warn {
    color: var(--warning);
}

.world-actions {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: none;
}

.world-actions form {
    margin: 0;
}

.backup-world {
    color: var(--text-2);
    font-weight: 600;
}

/* Narrow cards (side-by-side columns, tablets): actions go under the text. */
@container (max-width: 470px) {
    .world-item {
        flex-wrap: wrap;
    }

    .world-actions {
        width: 100%;
        justify-content: flex-end;
    }
}

/* Backup names are long; give them the full row sooner. */
@container (max-width: 600px) {
    .backup-item {
        flex-wrap: wrap;
    }

    .backup-actions {
        width: 100%;
        justify-content: flex-end;
    }
}

/* --- Confirm dialog extras --- */
/* List of affected items, typed-name field and checkbox that
   confirmAction() in app.js can add to the dialog (section 20). */
.dialog-list {
    display: grid;
    gap: 4px;
    max-height: 220px;
    margin: 2px 0 0;
    padding: 10px 12px 10px 28px;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--panel);
    color: var(--text-2);
    font-size: 13px;
    overflow-wrap: anywhere;
}

.dialog-field {
    display: grid;
    gap: 6px;
    margin-top: 4px;
}

.dialog-check {
    min-height: 36px;
    color: var(--text-2);
    font-size: 14px;
}

/* --- Loading and error states (shared) --- */
/* Shimmering placeholder row while a list loads. */
.skeleton-row {
    height: 64px;
    border-radius: var(--r-md);
    background: linear-gradient(90deg, var(--panel-2) 0%, var(--panel-3) 50%, var(--panel-2) 100%);
    background-size: 200% 100%;
    animation: shimmer 1.4s linear infinite;
}

/* Shown in place of a list that failed to load. */
.error-state {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 16px;
    border: 1px solid var(--danger-line);
    border-radius: var(--r-md);
    background: var(--danger-soft);
    color: var(--danger);
}

/* ==========================================================================
   18. Guest call-to-action
   --------------------------------------------------------------------------
   Sign-in card shown to visitors who aren't signed in, in the console's place
   on the overview.
   ========================================================================== */

.cta-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    overflow: hidden;
    background:
        radial-gradient(400px 200px at 100% 0%, rgba(52, 211, 153, 0.12), transparent 70%),
        var(--panel);
}

.cta-card h2 {
    font-size: 18px;
    font-weight: 650;
}

.cta-card p {
    margin-bottom: 6px;
    color: var(--text-2);
}

.cta-icon {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border: 1px solid var(--accent-line);
    border-radius: var(--r-md);
    background: var(--accent-soft);
    color: var(--accent);
}

/* ==========================================================================
   19. Footer
   ========================================================================== */

.footer {
    display: flex;
    justify-content: center;
    gap: 10px;
    padding-top: 12px;
    color: var(--text-3);
    font-size: 12px;
}

/* ==========================================================================
   20. Dialog
   --------------------------------------------------------------------------
   Native <dialog> used by confirmAction() in app.js (opened with showModal(),
   so it is in the top layer; ::backdrop dims and blurs the page).
   ========================================================================== */

.dialog {
    width: min(420px, calc(100vw - 32px));
    padding: 0;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-lg);
    background: var(--panel-2);
    color: var(--text);
    box-shadow: var(--shadow-pop);
}

.dialog[open] {
    animation: dialog-in 220ms var(--ease);
}

.dialog::backdrop {
    background: rgba(2, 4, 6, 0.6);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
}

.dialog-body {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 24px;
}

.dialog-body h2 {
    font-size: 18px;
    font-weight: 650;
}

/* pre-line keeps the line breaks in the confirm message. */
.dialog-body p {
    white-space: pre-line;
    color: var(--text-2);
}

.dialog-icon {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    margin-bottom: 4px;
    border-radius: var(--r-md);
    background: var(--danger-soft);
    color: var(--danger);
}

.dialog-icon.is-warning {
    background: var(--warning-soft);
    color: var(--warning);
}

.dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 14px;
}

/* ==========================================================================
   21. Toasts
   --------------------------------------------------------------------------
   Short notifications (data-tone: success / error / warning, info by default).
   ========================================================================== */

/* The stack sits under the top bar (plus the safe-area inset) so it never
   covers the job panel at the bottom. The stack ignores clicks; the toasts
   themselves take them. */
.toasts {
    position: fixed;
    top: calc(var(--topbar-h) + env(safe-area-inset-top, 0px) + 12px);
    right: max(16px, env(safe-area-inset-right, 0px));
    z-index: 50;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    width: min(380px, calc(100vw - 32px));
    pointer-events: none;
}

.toast {
    --tone: var(--info);
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: 100%;
    padding: 12px 12px 12px 14px;
    border: 1px solid var(--line-strong);
    border-left: 3px solid var(--tone);
    border-radius: var(--r-md);
    background: rgba(16, 21, 28, 0.96);
    box-shadow: var(--shadow-pop);
    pointer-events: auto;
    animation: toast-in 260ms var(--ease) both;
}

.toast.is-leaving {
    animation: toast-out 200ms var(--ease) forwards;
}

.toast[data-tone="success"] { --tone: var(--accent); }
.toast[data-tone="error"] { --tone: var(--danger); }
.toast[data-tone="warning"] { --tone: var(--warning); }

.toast > .icon {
    margin-top: 1px;
    color: var(--tone);
}

.toast-text {
    flex: 1;
    min-width: 0;
    font-size: 14px;
    overflow-wrap: anywhere;
}

.toast-close {
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    border-radius: var(--r-xs);
    background: none;
    color: var(--text-3);
    cursor: pointer;
}

.toast-close:hover {
    background: rgba(255, 255, 255, 0.08);
    color: var(--text);
}

.toast-close .icon {
    width: 14px;
    height: 14px;
}

/* ==========================================================================
   22. Login page
   --------------------------------------------------------------------------
   login.html: centred card with the logo, password field (with show/hide
   toggle) and the error alert.
   ========================================================================== */

.login-shell {
    display: grid;
    place-items: center;
    min-height: 100vh;
    min-height: 100dvh;
    padding: 24px 16px;
}

.login-card {
    width: min(400px, 100%);
    padding: 36px 32px 28px;
    overflow: hidden;
    text-align: center;
}

.login-glow {
    position: absolute;
    top: -160px;
    left: 50%;
    width: 360px;
    height: 300px;
    transform: translateX(-50%);
    background: radial-gradient(closest-side, rgba(52, 211, 153, 0.22), transparent);
    pointer-events: none;
}

.login-mark {
    position: relative;
    width: 60px;
    height: 60px;
    margin-bottom: 16px;
    filter: drop-shadow(0 10px 24px rgba(52, 211, 153, 0.4));
    animation: float 5s ease-in-out infinite;
}

.login-card h1 {
    position: relative;
    font-size: 26px;
    font-weight: 750;
    letter-spacing: -0.03em;
}

.login-subtitle {
    margin-top: 4px;
    color: var(--text-2);
}

.login-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 26px;
    text-align: left;
}

.field-label {
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
}

.field {
    position: relative;
    display: flex;
    align-items: center;
    margin-bottom: 10px;
}

/* Icon inside the input on the left; the input's padding leaves room for it
   and for the show/hide toggle on the right. */
.field-icon {
    position: absolute;
    left: 14px;
    width: 16px;
    height: 16px;
    color: var(--text-3);
    pointer-events: none;
    transition: color var(--fast);
}

.field input {
    width: 100%;
    height: 46px;
    padding: 0 46px 0 42px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    outline: none;
    background: var(--terminal);
    font-size: 15px;
    transition: border-color var(--fast), box-shadow var(--fast);
}

.field input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 4px var(--accent-soft);
}

.field:focus-within .field-icon {
    color: var(--accent);
}

.field input[aria-invalid="true"] {
    border-color: var(--danger-line);
}

.field-toggle {
    position: absolute;
    right: 6px;
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: var(--r-xs);
    background: none;
    color: var(--text-3);
    cursor: pointer;
}

.field-toggle:hover,
.field-toggle[aria-pressed="true"] {
    color: var(--text);
}

.field-toggle .icon {
    width: 16px;
    height: 16px;
}

.login-form .btn {
    height: 46px;
    font-size: 15px;
}

.alert {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 20px;
    padding: 11px 14px;
    border-radius: var(--r-sm);
    font-size: 14px;
    text-align: left;
}

/* Wrong password: red alert that shakes once. */
.alert-danger {
    border: 1px solid var(--danger-line);
    background: var(--danger-soft);
    color: #fecaca;
    animation: shake 380ms var(--ease);
}

.alert-danger .icon {
    color: var(--danger);
}

.back-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 22px;
    color: var(--text-3);
    font-size: 13px;
    transition: color var(--fast);
}

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

.back-link .icon {
    width: 14px;
    height: 14px;
    transition: transform var(--fast);
}

.back-link:hover .icon {
    transform: translateX(-3px);
}

/* ==========================================================================
   23. Animations
   --------------------------------------------------------------------------
   All motion is switched off for people who ask for reduced motion (end of
   section 25).
   ========================================================================== */

@keyframes spin {
    to { transform: rotate(360deg); }
}

@keyframes ping {
    0% { transform: scale(1); opacity: 0.7; }
    80%, 100% { transform: scale(2.6); opacity: 0; }
}

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

@keyframes rise {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
}

@keyframes pop {
    from { opacity: 0; transform: scale(0.96); }
    to { opacity: 1; transform: none; }
}

@keyframes fade-out {
    to { opacity: 0; transform: scale(0.94); }
}

@keyframes bump {
    40% { transform: scale(1.14); }
}

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

@keyframes indeterminate {
    from { transform: translateX(-100%); }
    to { transform: translateX(260%); }
}

@keyframes toast-in {
    from { opacity: 0; transform: translateY(12px) scale(0.98); }
    to { opacity: 1; transform: none; }
}

@keyframes toast-out {
    to { opacity: 0; transform: translateX(24px); }
}

@keyframes dialog-in {
    from { opacity: 0; transform: translateY(8px) scale(0.97); }
    to { opacity: 1; transform: none; }
}

@keyframes shake {
    20%, 60% { transform: translateX(-5px); }
    40%, 80% { transform: translateX(5px); }
}

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

/* ==========================================================================
   24. Navigation
   --------------------------------------------------------------------------
   Top tabs (inside the top bar) and the bottom tab bar. Both are hidden by
   default and only shown when JavaScript runs (.js), because tab switching
   needs the script. Below 900px the tabs are replaced by the bottom tab bar
   (section 25).
   ========================================================================== */

.tabs {
    display: none;
    align-items: center;
    gap: 2px;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: rgba(255, 255, 255, 0.02);
}

.js .tabs {
    display: flex;
}

.tab {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    height: 34px;
    padding: 0 12px;
    border-radius: var(--r-sm);
    color: var(--text-2);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    transition: background var(--fast), color var(--fast);
}

.tab .icon {
    width: 15px;
    height: 15px;
}

.tab:hover {
    background: rgba(255, 255, 255, 0.05);
    color: var(--text);
}

.tab.is-active {
    background: var(--accent-soft);
    color: var(--accent);
}

/* Small status dot on a tab (blue = update available). */
.tab-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--info);
    box-shadow: 0 0 0 3px var(--info-soft);
}

.tab-count {
    min-width: 18px;
    padding: 0 5px;
    border-radius: var(--r-pill);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 11px;
    line-height: 18px;
    text-align: center;
}

.tab-count:empty,
.tab-count[hidden] {
    display: none;
}

/* Bottom tab bar: only shown below 900px (section 25). */
.tabbar {
    display: none;
}

/* ==========================================================================
   25. Responsive
   --------------------------------------------------------------------------
   Kept at the end so these rules win over the base styles above (same
   specificity, later in the file). The order of the blocks matters too, e.g.
   the phone rules (<= 640px) come after the desktop ones.
   ========================================================================== */

/* --- >= 760px: tablets and small laptops --- */
/* Two columns on the overview. */
@media (min-width: 760px) {
    .ov-grid {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        grid-template-areas:
            "hero hero"
            "controls players"
            "console console";
    }

    body[data-admin="false"] .ov-grid {
        grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
        grid-template-areas:
            "hero hero"
            "players console";
        align-items: start;
    }

    .facts {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }

    .players-grid {
        grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    }

    /* dvh: follows the mobile browser's collapsing address bar. */
    .console {
        height: 55vh;
        height: 55dvh;
        max-height: none;
    }
}

/* --- >= 1100px: desktop --- */
/* A fixed-width side column and a console that fills the screen. */
@media (min-width: 1100px) {
    /* --ov-height = viewport height minus the top bar and room for the
       page's padding. The third row (players) takes whatever height is
       left. */
    .ov-grid {
        --ov-height: calc(100vh - var(--topbar-h) - 64px);
        grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
        grid-template-rows: max-content max-content minmax(140px, 1fr);
        grid-template-areas:
            "hero console"
            "controls console"
            "players console";
        min-height: var(--ov-height);
    }

    /* Use the dynamic viewport height where supported (mobile browsers). */
    @supports (height: 100dvh) {
        .ov-grid {
            --ov-height: calc(100dvh - var(--topbar-h) - 64px);
        }
    }

    /* Tall enough: the overview fits the screen exactly and the console and
       player list scroll inside their cards. */
    @media (min-height: 720px) {
        body[data-admin="true"] .ov-grid {
            height: var(--ov-height);
        }

        body[data-admin="true"] .ov-grid > .ov-players {
            display: flex;
            flex-direction: column;
            min-height: 0;
        }

        body[data-admin="true"] .ov-grid > .ov-players .player-list {
            flex: 1 1 0;
            min-height: 0;
            overflow-y: auto;
            overscroll-behavior: contain;
            scrollbar-width: thin;
        }
    }

    /* A more compact hero and card headers in the narrow side column. */
    .ov-grid .hero {
        padding: 20px;
    }

    .ov-grid .hero-title {
        font-size: 28px;
    }

    .ov-grid .gauge {
        width: 92px;
        height: 92px;
    }

    .ov-grid .gauge-label strong {
        font-size: 26px;
    }

    .ov-grid .controls-card .card-head,
    .ov-grid .ov-players .card-head {
        flex-wrap: nowrap;
        margin-bottom: 14px;
    }

    .ov-grid .controls-card .card-sub,
    .ov-grid .ov-players .card-sub {
        display: none;
    }

    .ov-grid .controls-card .card-icon,
    .ov-grid .ov-players .card-icon {
        width: 30px;
        height: 30px;
    }

    /* Guests: no side column, just status on top and two halves below. */
    body[data-admin="false"] .ov-grid {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        grid-template-rows: auto;
        grid-template-areas:
            "hero hero"
            "players console";
        min-height: 0;
    }

    body[data-admin="true"] .ov-grid .facts {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    /* The console card, its terminal and the log flex to fill the grid row
       (min-height: 0 lets them shrink so the log scrolls inside). The footer
       is hidden so the overview fits the screen. */
    .ov-grid > .console-card {
        min-height: 0;
    }

    body[data-admin="true"][data-view="overview"] .footer {
        display: none;
    }

    .console-card .terminal {
        flex: 1 1 0;
    }

    .console {
        flex: 1 1 0;
        height: auto;
        min-height: 0;
    }

    .server-grid {
        grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
    }

    /* The switch form stays in view while scrolling the world settings. */
    .server-grid > .profile-card {
        position: sticky;
        top: calc(var(--topbar-h) + 20px);
    }
}

/* Storage (>= 1100px): disk on top, worlds and backups side by side. */
@media (min-width: 1100px) {
    .storage-grid {
        grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
        grid-template-areas:
            "disk disk"
            "worlds backups";
    }
}

/* --- >= 1500px: wide screens --- */
/* Players get their own column next to the console. */
@media (min-width: 1500px) {
    :root {
        --page-max: 1680px;
    }

    .ov-grid {
        grid-template-columns: minmax(340px, 400px) minmax(0, 1fr) minmax(260px, 320px);
        grid-template-rows: max-content minmax(0, 1fr);
        grid-template-areas:
            "hero console players"
            "controls console players";
    }
}

/* Very wide screens: let the page grow a little more. */
@media (min-width: 2200px) {
    :root {
        --page-max: 2000px;
    }
}

/* --- Short landscape screens --- */
/* Phones on their side, small laptops: don't try to fit the console into
   the viewport height, and don't keep the top bar or switch form sticky.
   svh = the smallest viewport height (address bar shown), so the console
   never ends up taller than what's visible. */
@media (max-height: 620px) and (orientation: landscape) {
    .topbar {
        position: static;
    }

    .ov-grid {
        min-height: 0;
    }

    .console-card {
        min-height: 0;
    }

    .console {
        flex: none;
        height: 80vh;
        height: 80svh;
    }

    .server-grid > .profile-card {
        position: static;
    }
}

/* --- < 900px: bottom tab bar --- */
/* The top tabs are replaced by a bar fixed to the bottom of the screen
   (z-index 30). --tabbar-h makes .app and .job-dock leave room for it. */
@media (max-width: 899px) {
    :root {
        --tabbar-h: 64px;
    }

    .js .tabs {
        display: none;
    }

    .js body[data-admin="true"] .tabbar {
        position: fixed;
        right: 0;
        bottom: 0;
        left: 0;
        z-index: 30;
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        /* Grow by the bottom safe-area inset (home indicator) and keep the
           items clear of rounded corners / notches in landscape. */
        height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
        padding: 0 max(4px, env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px) max(4px, env(safe-area-inset-left, 0px));
        border-top: 1px solid var(--line);
        background: rgba(6, 8, 11, 0.9);
        -webkit-backdrop-filter: saturate(160%) blur(14px);
        backdrop-filter: saturate(160%) blur(14px);
    }

    .tabbar-item {
        position: relative;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 3px;
        color: var(--text-3);
        font-size: 11px;
        font-weight: 600;
    }

    .tabbar-item .icon {
        width: 22px;
        height: 22px;
    }

    .tabbar-item.is-active {
        color: var(--accent);
    }

    /* Accent line along the top edge of the active item. */
    .tabbar-item.is-active::before {
        content: "";
        position: absolute;
        top: 0;
        width: 32px;
        height: 2px;
        border-radius: 0 0 2px 2px;
        background: var(--accent);
    }

    /* Badges sit on the icon's top-right corner. */
    .tabbar-item .tab-dot,
    .tabbar-item .tab-count {
        position: absolute;
        top: 8px;
        left: calc(50% + 8px);
    }

    .tabbar-item .tab-count {
        line-height: 16px;
    }
}

/* Landscape phones: no bottom bar (it eats scarce height); the top tabs come
   back, icons only. Must stay after the "< 900px" block to override it. */
@media (max-width: 899px) and (max-height: 520px) and (orientation: landscape) {
    :root {
        --tabbar-h: 0px;
    }

    .js body[data-admin="true"] .tabbar {
        display: none;
    }

    .js .tabs {
        display: flex;
    }

    .tab span {
        display: none;
    }
}

/* --- 900px-1100px: icon-only tabs --- */
/* Not enough room in the top bar for the tab labels. */
@media (max-width: 1100px) and (min-width: 900px) {
    .tab span {
        display: none;
    }

    .tab {
        padding: 0 10px;
    }
}

/* --- < 760px --- */
/* Backup schedule stacks; the world picker takes the full card width. */
@media (max-width: 759px) {
    .schedule {
        grid-template-columns: minmax(0, 1fr);
    }

    .world-card .card-head {
        align-items: stretch;
    }

    .world-card .select-wrap.select-sm,
    .world-card .select-sm .select {
        width: 100%;
    }
}

/* --- <= 640px: phones --- */
/* Tighter spacing, smaller console text, full-width buttons in lists and
   dialogs, and less decoration. The job dock and toasts span the width. */
@media (max-width: 640px) {
    :root {
        --topbar-h: 58px;
        --gap: 14px;
    }

    .app {
        padding-top: 16px;
    }

    .card {
        padding: 18px;
        border-radius: var(--r-md);
    }

    .gauge {
        width: 92px;
        height: 92px;
    }

    .gauge-label strong {
        font-size: 26px;
    }

    .hide-sm,
    .live-indicator,
    .brand-text small {
        display: none;
    }

    .topbar-actions {
        gap: 8px;
    }

    .console {
        height: 60vh;
        height: 60svh;
        min-height: 280px;
        max-height: none;
        padding: 12px 6px 16px;
        font-size: 11.5px;
    }

    .job-dock {
        right: 12px;
        left: 12px;
        width: auto;
    }

    .console-card .card-head {
        flex-wrap: nowrap;
    }

    .console-card .card-title {
        flex: 1;
    }

    .console-card .card-title > div {
        min-width: 0;
    }

    .console-card .card-sub {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .console-card .toolbar {
        flex: none;
        gap: 4px;
    }

    .terminal-bar {
        gap: 8px;
        padding: 6px 8px 6px 12px;
    }

    .jump-latest {
        right: 10px;
        bottom: 10px;
    }

    .settings-grid {
        grid-template-columns: minmax(0, 1fr);
    }

    .settings-actions .btn {
        width: 100%;
    }

    .inline-action,
    .inline-action .btn {
        width: 100%;
        margin-left: 0;
    }

    .toasts {
        right: 12px;
        left: 12px;
        width: auto;
        align-items: stretch;
    }

    .dialog-body {
        padding: 20px;
    }

    .backup-name span {
        font-size: 12px;
    }

    .terminal-dots {
        display: none;
    }

    .backup-item {
        flex-wrap: wrap;
    }

    .backup-file-icon {
        display: none;
    }

    .backup-actions {
        width: 100%;
    }

    .backup-actions > * {
        flex: 1;
    }

    .backup-actions .btn {
        width: 100%;
    }

    .world-item {
        flex-wrap: wrap;
    }

    .world-icon {
        display: none;
    }

    .world-actions {
        width: 100%;
    }

    .world-actions > form {
        flex: 1;
    }

    .world-actions .btn {
        width: 100%;
    }

    /* Button labels stay visible in the full-width world actions. */
    .world-actions .hide-sm {
        display: inline;
    }

    .backups-card .card-actions {
        width: 100%;
    }

    .backups-card .card-actions > * {
        flex: 1;
    }

    .backups-card .card-actions .btn,
    .backups-card .card-actions .select {
        width: 100%;
    }

    .disk-card .card-actions {
        width: 100%;
        flex-wrap: nowrap;
    }

    .disk-card .card-actions form {
        flex: 1;
    }

    .disk-card .card-actions form .btn {
        width: 100%;
    }

    .disk-alert {
        flex-wrap: wrap;
    }

    .disk-alert .btn {
        margin-left: auto;
    }

    .dialog-actions .btn {
        flex: 1;
    }

    .login-card {
        padding: 30px 22px 24px;
    }
}

/* --- Touch, hover and motion preferences --- */
/* Touch screens: 44px tap targets (Apple HIG / WCAG 2.5.5), and inputs at
   16px so iOS Safari doesn't zoom the page when they're focused. */
@media (pointer: coarse) {
    .btn,
    .btn-sm {
        min-height: 44px;
    }

    .btn-icon {
        width: 44px;
        height: 44px;
    }

    .terminal-search {
        height: 38px;
    }

    .code-chip {
        width: 40px;
        height: 40px;
    }

    .terminal-search input,
    .field input,
    .select,
    .text-input {
        font-size: 16px;
    }

    .field-toggle {
        width: 44px;
        height: 44px;
        right: 1px;
    }
}

/* No hover lift on touch devices (hover "sticks" after a tap there). */
@media (hover: none) {
    .ctl:hover:not(:disabled),
    .player:hover {
        transform: none;
    }
}

/* Reduced motion: animations and transitions finish instantly (0.01ms
   rather than 0 so "animationend"/"transitionend" events still fire). */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}
