        :root {
            /* Fluid scale tokens: clamp(min, preferred, max).
               preferred is today's vw/vh value, unchanged - min/max are that
               same formula evaluated at the supported viewport floor/ceiling
               (1280x800 .. 2560x1440). Inside that range this renders pixel-for-
               pixel identical to before; outside it, the value clamps instead of
               shrinking/growing without limit.
               --u-N = width axis, anchored 1280px/2560px (N = vw value x100)
               --v-N = height axis, anchored 800px/1440px (N = vh value x100) */
            --u-5: clamp(1px, 0.05vw, 1px);
            --u-10: clamp(1px, 0.1vw, 3px);
            --u-15: clamp(2px, 0.15vw, 4px);
            --u-20: clamp(3px, 0.2vw, 5px);
            --u-25: clamp(3px, 0.25vw, 6px);
            --u-30: clamp(4px, 0.3vw, 8px);
            --u-35: clamp(4px, 0.35vw, 9px);
            --u-40: clamp(5px, 0.4vw, 10px);
            --u-50: clamp(6px, 0.5vw, 13px);
            --u-52: clamp(7px, 0.52vw, 13px);
            --u-60: clamp(8px, 0.6vw, 15px);
            --u-65: clamp(8px, 0.65vw, 17px);
            --u-68: clamp(9px, 0.68vw, 17px);
            --u-70: clamp(9px, 0.7vw, 18px);
            --u-75: clamp(10px, 0.75vw, 19px);
            --u-80: clamp(10px, 0.8vw, 20px);
            --u-85: clamp(11px, 0.85vw, 22px);
            --u-90: clamp(12px, 0.9vw, 23px);
            --u-95: clamp(12px, 0.95vw, 24px);
            --u-100: clamp(13px, 1vw, 26px);
            --u-105: clamp(13px, 1.05vw, 27px);
            --u-110: clamp(14px, 1.1vw, 28px);
            --u-120: clamp(15px, 1.2vw, 31px);
            --u-126: clamp(16px, 1.26vw, 32px);
            --u-130: clamp(17px, 1.3vw, 33px);
            --u-140: clamp(18px, 1.4vw, 36px);
            --u-150: clamp(19px, 1.5vw, 38px);
            --u-160: clamp(20px, 1.6vw, 41px);
            --u-170: clamp(22px, 1.7vw, 44px);
            --u-180: clamp(23px, 1.8vw, 46px);
            --u-190: clamp(24px, 1.9vw, 49px);
            --u-200: clamp(26px, 2vw, 51px);
            --u-210: clamp(27px, 2.1vw, 54px);
            --u-220: clamp(28px, 2.2vw, 56px);
            --u-240: clamp(31px, 2.4vw, 61px);
            --u-250: clamp(32px, 2.5vw, 64px);
            --u-260: clamp(33px, 2.6vw, 67px);
            --u-280: clamp(36px, 2.8vw, 72px);
            --u-300: clamp(38px, 3vw, 77px);
            --u-320: clamp(41px, 3.2vw, 82px);
            --u-340: clamp(44px, 3.4vw, 87px);
            --u-350: clamp(45px, 3.5vw, 90px);
            --u-380: clamp(49px, 3.8vw, 97px);
            --u-450: clamp(58px, 4.5vw, 115px);
            --u-460: clamp(59px, 4.6vw, 118px);
            --u-500: clamp(64px, 5vw, 128px);
            --u-900: clamp(115px, 9vw, 230px);
            --u-1200: clamp(154px, 12vw, 307px);
            --u-1600: clamp(205px, 16vw, 410px);
            --u-2000: clamp(256px, 20vw, 512px);
            --u-2500: clamp(320px, 25vw, 640px);
            --u-3000: clamp(384px, 30vw, 768px);

            --v-20: clamp(2px, 0.2vh, 3px);
            --v-30: clamp(2px, 0.3vh, 4px);
            --v-40: clamp(3px, 0.4vh, 6px);
            --v-50: clamp(4px, 0.5vh, 7px);
            --v-60: clamp(5px, 0.6vh, 9px);
            --v-100: clamp(8px, 1vh, 14px);
            --v-150: clamp(12px, 1.5vh, 22px);
            --v-200: clamp(16px, 2vh, 29px);
            --v-250: clamp(20px, 2.5vh, 36px);
            --v-450: clamp(36px, 4.5vh, 65px);
            --v-500: clamp(40px, 5vh, 72px);
            --v-550: clamp(44px, 5.5vh, 79px);
            --v-600: clamp(48px, 6vh, 86px);
            --v-750: clamp(60px, 7.5vh, 108px);
            --v-800: clamp(64px, 8vh, 115px);
            --v-1600: clamp(128px, 16vh, 230px);
            --v-2000: clamp(160px, 20vh, 288px);
        }

        .server-item {
            transform: translateZ(0);
            will-change: transform;
        }
        html, body {
            margin: 0;
            padding: 0;
        }

        body {
            font-family: 'Outfit', sans-serif;
            background-color: #121212;
            color: #ffffff;
            background-image: url("/images/background_pripyat_2752x1536.png");
            background-size: cover;
            background-position: center;
            background-repeat: no-repeat;
            background-attachment: fixed;
        }

        .glass-panel {
            /* No backdrop-filter (see prior perf notes on this) - panels stay
               readable via a near-opaque background instead of blur. */
            background-color: rgba(20, 20, 20, 0.95);
            border: 1px solid rgba(255, 255, 255, 0.05);
            box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.5);
        }

        .active-duration {
            border-color: #cd412b !important;
            color: #cd412b !important;
            background-color: rgba(205, 65, 43, 0.05);
            box-shadow: 0 0 12px rgba(205, 65, 43, 0.35);
        }

        /* The selected server, filled the way the mobile picker fills it -
           a faint tint was not readable at a glance, which is the only job
           this has. Written as plain CSS rather than Tailwind arbitrary
           classes: `border-[#cd412b]/40` and `border-l-[var(--u-40)]` don't
           compile in the CDN build, and a `border` class with no usable
           colour falls back to currentColor, which painted a white outline. */
        .server-item.active {
            border-color: #cd412b !important;
            background: linear-gradient(155deg, #cd412b, #a8331f) !important;
        }
        /* On a saturated card the muted greys and the green/red status dot
           text all lose contrast, so everything inside goes to white with
           the secondary bits merely dimmed. */
        .server-item.active .text-\[\#cccccc\],
        .server-item.active .text-\[\#888888\],
        .server-item.active .text-green-500,
        .server-item.active .text-red-500 {
            color: rgba(255, 255, 255, 0.85) !important;
        }
        .server-item.active svg { color: rgba(255, 255, 255, 0.85) !important; }
        /* The VIP crown keeps its gold, which still reads on red. */
        .server-item.active .text-\[\#FBBF24\] { color: #ffd970 !important; }
        .server-item.active:hover { filter: brightness(1.06); }

        /* ==================================================================
           PRELOADER - the boot overlay (src/html/preloader.html)

           Built from what the rest of the site already uses: #121212, the
           accent, the header's wordmark, and the same uppercase grey label
           the panels put above their content. No frame, no glow, no texture -
           an earlier version had all three and read as a different product
           than the page behind it.

           Two rules, both from a real bug the first version had:

           1. NOTHING FADES IN. Everything is at its final opacity on the
              first painted frame. The overlay lives about a second, so an
              entry fade would still be running when the exit fade starts, and
              two opposing opacity animations over the same pixels is exactly
              what read as flickering.

           2. ONE THING MOVES, AND IT MEANS SOMETHING. The rail is the only
              animation here; it tracks the real session/server-list steps
              (js/shared/preloader.js). Decoration that merely moves was what
              made this screen feel louder than the site.

           The type is the system stack, not the page's Outfit, and the mark
           is inline SVG, not the logo PNG: both of those arrive over the
           network, and either landing mid-boot would reflow or pop the
           layout. That does mean the wordmark here is not quite the header's
           face - the alternative is watching it re-set itself when the
           webfont lands, which is worse.
           ================================================================== */
        #preloader {
            position: fixed;
            inset: 0;
            z-index: 100;
            background: #121212;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            transition: opacity 0.4s ease-out;
        }
        #preloader.preloader-hidden {
            opacity: 0;
            pointer-events: none;
        }

        /* Flat accent, like the logo file itself - fill is inherited, so the
           blades and hub need no rule of their own. */
        .pl-mark {
            width: var(--u-450);
            height: var(--u-450);
            fill: #cd412b;
        }

        .pl-wordmark {
            margin-top: var(--u-150);
            font-size: var(--u-240);
            font-weight: 800;
            line-height: 1;
            letter-spacing: -0.02em;
            color: #ffffff;
            white-space: nowrap;
        }
        .pl-wordmark span { color: #cd412b; }

        .pl-rail {
            width: var(--u-1600);
            height: var(--u-20);
            margin-top: var(--u-240);
            border-radius: 999px;
            background: rgba(255, 255, 255, 0.08);
            overflow: hidden;
        }
        /* Width, not a scaleX transform: preloader.js repaints this every
           frame, and a scaled fill would squash any content it ever gains.
           One hairline box is not a layout cost worth designing around. */
        .pl-fill {
            width: 4%;
            height: 100%;
            border-radius: inherit;
            background: #cd412b;
        }

        .pl-status {
            margin: var(--u-130) 0 0;
            min-height: var(--u-120);
            font-size: var(--u-80);
            font-weight: 600;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            color: #888888;
        }

        /* Nothing to switch off for reduced motion - the rail is information,
           not atmosphere, and it is all there is. Only the exit is shortened. */
        @media (prefers-reduced-motion: reduce) {
            #preloader { transition-duration: 0.15s; }
        }

        .command-well {
            box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.55), inset 0 0 0 1px rgba(255, 255, 255, 0.04);
            transition: box-shadow 0.3s ease;
        }
        .command-well.copy-success {
            box-shadow: inset 0 2px 8px rgba(34, 197, 94, 0.35), inset 0 0 0 1px rgba(34, 197, 94, 0.5);
        }

        /* Server-stats charts panel (Connect/Server tab, right column) */
        .stat-section {
            flex: 1;
            display: flex;
            flex-direction: column;
            padding: var(--u-100) var(--u-120);
            background: #171717;
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: var(--u-120);
            /* Was a flat 220px - immune to viewport height, so three stacked
               panels (660px+ before gaps) would blow through anything shorter
               than that regardless of window height. Height-axis token
               instead, so all three shrink together on short viewports. */
            min-height: var(--v-1600);
        }
        /* Grid, not flex+absolute: the old version centered the geo-location
           caption with position:absolute + left:50% so it wouldn't shift
           when the ms/GB/% value's text width changed - but that gave it
           zero collision guard against the title or value, so a longer
           location name (or just a narrower panel) visually overlapped
           both. A grid's middle track can shrink and ellipsize on its own,
           but by construction can never overlap the outer two tracks.

           Side columns are minmax(0, 1fr) - i.e. forced EQUAL to each
           other - rather than each auto-sized to its own content. They
           used to be `auto auto`, which sized column 1 to the title's
           width and column 3 to the value's width independently - so the
           middle (caption) track was centered in whatever space was left
           BETWEEN those two, not centered on the card. Every time the
           value's digit count changed (27ms vs 142ms), column 3's width
           changed, and the caption visibly shifted left/right along with
           it despite never itself changing. Equal 1fr side columns keep
           the caption's center fixed at the card's actual center
           regardless of how wide either side's content is. */
        .stat-header {
            display: grid;
            grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
            align-items: center;
            column-gap: var(--u-40);
            margin-bottom: var(--u-80);
        }
        /* Explicit columns, not auto-placement: the RAM/CPU headers have only
           two children (title + value, no center caption) - left to
           auto-placement, grid fills cells in DOM order, which would put
           the value into the empty middle track instead of the right one. */
        .stat-title { grid-column: 1; justify-self: start; }
        .stat-header-center {
            /* justify-self must stay stretch (the default - not "center"):
               a non-stretch justify-self sizes the item to its own content's
               preferred width instead of clamping it to the track, which
               silently let a long location name render at full width again,
               spilling over the title/value with no ellipsis ever kicking
               in. Stretch to the track, then center the *content* inside it
               with the element's own justify-content (it's a flex row via
               the "flex justify-center" Tailwind classes). min-width:0 is
               what actually lets that stretched box shrink below its
               content's natural size in the first place - a grid item's
               default min-width is auto ("never shrink below content"). */
            grid-column: 2;
            min-width: 0;
            overflow: hidden;
        }
        .stat-current-value { grid-column: 3; justify-self: end; }
        .stat-title {
            display: flex;
            align-items: center;
            gap: var(--u-40);
            font-size: var(--u-80);
            font-weight: bold;
            color: #888888;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }
        .stat-title i { width: 1em; height: 1em; }
        .stat-current-value {
            font-size: var(--u-110);
            font-weight: 800;
            color: #cd412b;
        }
        .time-filters {
            display: flex;
            gap: var(--u-40);
            margin-bottom: var(--u-80);
            flex-wrap: wrap;
        }
        .time-btn {
            background: transparent;
            border: 1px solid rgba(255, 255, 255, 0.1);
            color: #888888;
            padding: var(--u-30) var(--u-40);
            border-radius: var(--u-40);
            font-size: var(--u-70);
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
            flex-grow: 1;
            text-align: center;
        }
        .time-btn:hover {
            background: rgba(255, 255, 255, 0.05);
            color: white;
        }
        .time-btn.active {
            background: #cd412b;
            color: white;
            border-color: #cd412b;
        }
        .chart-container {
            flex-grow: 1;
            position: relative;
            width: 100%;
            min-height: 0;
            display: flex;
            flex-direction: column;
        }

        /* Hand-rolled sparkline (Ping/RAM/CPU) - a plain SVG line plotted at
           exact pixel coordinates we compute ourselves, plus tick labels laid
           out with flexbox space-between. No charting library scale/tick
           logic involved, so there's no category-vs-linear axis confusion,
           no tick-collision, no index-based positioning that drifts between
           refreshes - the line always spans the SVG edge-to-edge by
           construction, and the labels are just N flex children. */
        .sparkline-row {
            flex: 1;
            min-height: 0;
            display: flex;
            gap: var(--u-50);
        }
        /* SVG is a replaced element with an intrinsic aspect ratio (from its
           viewBox) - in a flex row that can make it size by aspect ratio
           instead of stretching to the row's full height like a plain <div>
           would, which is exactly why the grid lines and the (correctly
           stretched) Y-label column drifted out of alignment. Absolute-fill
           inside a relatively-positioned wrapper sidesteps that entirely:
           inset:0 always matches the wrapper's box exactly, no intrinsic
           sizing involved. */
        .sparkline-plot {
            flex: 1;
            min-width: 0;
            position: relative;
        }
        .sparkline-svg {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            display: block;
        }
        .sparkline-line {
            stroke-width: 2;
            vector-effect: non-scaling-stroke;
        }
        .sparkline-fill {
            opacity: 0.12;
        }
        .sparkline-grid {
            stroke: #2c2c2c;
            stroke-width: 1;
            vector-effect: non-scaling-stroke;
        }
        .sparkline-y-labels {
            width: var(--u-240);
            flex-shrink: 0;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            text-align: right;
        }
        .sparkline-y-labels span {
            color: #888888;
            font-size: var(--u-65);
            line-height: 1;
        }
        .sparkline-x-labels {
            display: flex;
            justify-content: space-between;
            margin-top: var(--u-40);
            margin-right: calc(var(--u-240) + var(--u-50)); /* stay under the SVG, not the y-labels column */
        }
        .sparkline-x-labels span {
            color: #888888;
            font-size: var(--u-65);
            white-space: nowrap;
        }
        .sparkline-cursor-line {
            stroke: rgba(255, 255, 255, 0.25);
            stroke-width: 1;
            vector-effect: non-scaling-stroke;
            pointer-events: none;
        }
        .sparkline-cursor-dot {
            stroke: #121212;
            stroke-width: 4;
            vector-effect: non-scaling-stroke;
            pointer-events: none;
        }
        .sparkline-tooltip {
            position: absolute;
            top: var(--u-40);
            transform: translateX(-50%);
            background: rgba(26, 26, 26, 0.95);
            border: 1px solid #2c2c2c;
            border-radius: var(--u-40);
            padding: var(--u-30) var(--u-60);
            color: white;
            font-size: var(--u-70);
            font-weight: 600;
            white-space: nowrap;
            pointer-events: none;
            z-index: 1;
        }
        .sparkline-tooltip .sparkline-tooltip-time {
            color: #888888;
            font-weight: 500;
            margin-right: var(--u-40);
        }

        .custom-scroll::-webkit-scrollbar { width: var(--u-30); }
        .custom-scroll::-webkit-scrollbar-track { background: transparent; }
        .custom-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: var(--u-30); }
        .custom-scroll::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }

        /* Every clickable element here already shows state via hover/active scale +
           color, so the browser's default blue focus ring is just visual noise on
           top of that — applies to server-item cards and leaderboard rows too, not
           just <a>/<button>. */
        *:focus {
            outline: none;
        }

