        /* ---- Tokens --------------------------------------------------
           The palette used to be spelled out in raw hex at every use site
           - #8b7040 alone appeared 24 times - so changing a colour meant
           find-and-replace, and near-misses (#6b5030 the gilt shadow vs
           #6b5533 the muted ink) were indistinguishable at a glance while
           reading. Same values throughout, named once. The three --raise*
           shadows in particular were being retyped by hand on every
           button, which is how they had already drifted apart.
           -------------------------------------------------------------- */
        :root {
            /* --- Typefaces. Three roles, not one font stretched to do
               three jobs: Cinzel for display (its capitals are cut like
               inscriptions), Ubuntu for running UI text, and a mono for
               the numbers so a column of prices actually lines up. ------ */
            --font-display: 'Cinzel', Georgia, 'Times New Roman', serif;
            --font-body:    'Ubuntu', system-ui, -apple-system, sans-serif;
            --font-mono:    'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

            /* Ink on parchment, darkest to faintest. --ink doubles as the
               deepest leather shade (button bottom edges): same colour,
               and they should stay the same colour. */
            --ink:          #2c1a08;
            --ink-soft:     #3d2810;
            /* Muted/faint were a shade too light to read comfortably on the
               parchment surfaces they actually sit on: #8a7458 on #d6c9ae is
               only ~2.7:1, well under the 4.5:1 body-text minimum, and these
               carry real content (bot owners, "no offers", hints) rather than
               decoration. Darkened so faint still reads as secondary but is
               legible. --ink-faint's own fix undershot slightly (4.31:1,
               still short of 4.5) - nudged the rest of the way. */
            --ink-muted:    #5c4830;
            --ink-faint:    #654f2d;

            /* Parchment surfaces, lit to sunken */
            --parch-lit:    #faf4e8;
            --parch:        #f2e8d5;
            --parch-page:   #d8cbb0;
            --parch-sunk:   #d6c9ae;
            --parch-deep:   #cbbfa2;
            --parch-panel:  #c8b898;

            /* Gilt and leather - the frame around everything. --gilt-hi2 is
               the new top-of-gradient highlight that gives the buttons a
               metal read rather than a flat two-stop fade. */
            --gilt:         #8b7040;
            --gilt-lo:      #6b5030;
            --gilt-inset:   #5c4828;
            --gilt-hi:      #9a8050;
            --gilt-hi2:     #d9bd77;
            --gilt-hi-lo:   #7a5c38;
            --gilt-lit:     #c0a850;
            --gilt-lit-lo:  #a08838;
            --gilt-bright:  #e8c874;
            --leather:      #4a3520;
            --umber:        #2a1e12;

            /* The gilt button face in its three states, as 3-stop gradients
               so a light catches the top edge - the difference between
               painted gold and a metal button. Named because .tab,
               .side-btn and .bot-btn are the same button at three sizes and
               all three had these gradients typed out separately. */
            /* Gilt was a *mid* tone, which no lettering clears 4.5:1 on:
               cream measured 2.0-3.5 across the gradient and dark ink
               2.6-3.6. So the metal is split by meaning instead - bronze for
               anything that carries lettering at rest (cream on it is
               5.5:1), and the bright leaf reserved for the *lit* state,
               where dark engraved ink clears 6.5:1. The night theme's
               reading, mirrored. */
            --face:         linear-gradient(180deg, #6d5836 0%, #5a472a 46%, #3e301c 100%);
            --face-hi:      linear-gradient(180deg, #74603c 0%, #5f4b2e 46%, #453722 100%);
            --face-on:      linear-gradient(180deg, #ebc97a 0%, #d3ad58 46%, #a8893f 100%);

            /* Lettering over gilt/leather rather than over parchment */
            --on-dark:      #f0e0c0;
            --on-dark-soft: #e8d8b0;
            /* Lettering engraved into the *lit* gold, rather than bleached
               onto any gold: see the faces below. */
            --on-gilt:      #33240f;

            /* Depth: one raised control at rest, lifted, and pushed in */
            --raise:        0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.15);
            --raise-hi:     0 3px 6px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.15);
            --pressed:      inset 0 2px 5px rgba(0,0,0,0.4);

            /* Motion. Three speeds, and they mean different things:
               `press` is the physical click - fast and slightly springy,
               because a button must feel like it answers instantly;
               `tint` is the colour settling behind it, deliberately
               slower, so the thing reads as having moved first and
               recoloured after; `enter` is content arriving, on a curve
               that covers most of the distance immediately and then eases
               out long, which is what makes an arrival read as smooth
               rather than as something sliding into place. */
            --press-t:      0.1s;
            --press-ease:   cubic-bezier(.2,.7,.3,1);
            --tint-t:       0.15s;
            --enter-t:      0.26s;
            --enter-ease:   cubic-bezier(.16,1,.3,1);
            --spring:       cubic-bezier(.34,1.56,.64,1);

            /* Semantic accents, so "the red" or "the green" is one edit
               everywhere instead of a hex retyped per use. The *-deep pair
               exist for text on parchment, where the mid tone alone is too
               light to pass contrast. */
            --danger:       #a03422;
            --danger-deep:  #7d2a18;
            --success:      #2a6b2e;
            --success-deep: #1f5423;
            --info:         #3a5f9e;
            --link-hover:   #6b4a20;
            /* Trend figures. Darker than --success/--danger on purpose: they
               are 1d/7d readings in a dense table, and the mid tones read as
               washed out at 12px. The script reads these two rather than
               hardcoding the pair in six places (see TREND_COLOURS). */
            --up:           #0f5132;
            --down:         #8a1c1c;
            /* The gold of a favourite's star, and the red the "no gc" and
               clear buttons are filled with - both were spelled out at every
               use site. */
            --star:         #f5c518;
            --danger-fill:  #c0392b;
            /* The side tools' own colour - the ledger's teal, so the two
               links that leave this page read as one family and are not
               mistaken for market links (brown) or for the up/down pair
               (green/red). Darkened from the chart accent (#2f7d8c, 3.9:1
               on parchment) to clear 4.5:1 as text. */
            --tool:         #25646f;
            --tool-lit:     #1b4d56;

            /* The one shadow the panels and chart share, so they sit in the
               same light rather than each inventing its own depth. */
            --panel-shadow: 0 2px 7px rgba(74,53,32,0.18);

            /* Corner softness. Small on purpose - the bevels do the
               "raised" talking; the radius only takes the razor edge off
               the otherwise pure rectangles. */
            --radius:       6px;
            --radius-sm:    4px;

            /* A fine paper grain, tiled. Kept as a token so it can sit over
               any parchment surface without another data-URI pasted in. */
            --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.07'/%3E%3C/svg%3E");

            /* The chart's own palette. These were literals inside app.js,
               which meant the canvas could not follow the theme: a night
               page drew a daylight plot. Now the draw code reads them
               back off the document (see CH in app.js), and the dark
               block below re-lights them with everything else. The two
               series hues are validated for colour-vision deficiency
               against the day plot surface (deutan ΔE 10.4); the dark
               pair keeps the same hues and separates by lightness. */
            --chart-surface: #f2e8d5;
            --chart-surface-2: #e6d9bd;
            --chart-grid:    #ded0b4;
            --chart-axis:    #6b5533;
            --chart-ink:     #3d2810;
            --chart-band:    rgba(107,85,51,0.10);
            --chart-sub:     rgba(138,116,88,0.75);
            --chart-sell:    #059669;
            --chart-buy:     #b91c1c;
            --chart-rarity:  #7d3fa8;
            /* The hover bubble is a dark card in both themes - it floats
               over the plot and has to read against either. */
            --chart-tip-bg:    rgba(44,26,8,0.96);
            --chart-tip-edge:  #8b7040;
            --chart-tip-head:  #c9b795;
            --chart-tip-value: #f7ecd6;
        }

        /* Anchor jumps and the "scroll the chart into view" helper both
           teleported the page; over a document this long that costs you
           your place every time. */
        html { scroll-behavior: smooth; }
        @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

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

        /* Prose (the explainer lines under panels, the empty-state text)
           gets more air than the default; controls and table cells keep
           their own tight leading, so this can't inflate a button or a row. */
        p { line-height: 1.55; }

        /* Keyboard focus ring, consistent everywhere - buttons had none at
           all, and inputs removed the native outline without a real
           replacement (bot-filter especially: silently unfocusable-looking
           for keyboard users). Only shows for keyboard nav, not mouse clicks. */
        :focus-visible {
            outline: 2px solid var(--gilt-lit);
            outline-offset: 2px;
        }

        /* Themed scrollbars for the suggest dropdown and scrollable tables,
           instead of the jarring default OS scrollbar. */
        .suggest, .table-scroll {
            scrollbar-width: thin;
            scrollbar-color: var(--gilt) var(--parch-sunk);
        }
        .suggest::-webkit-scrollbar, .table-scroll::-webkit-scrollbar { height: 10px; width: 10px; }
        .suggest::-webkit-scrollbar-track, .table-scroll::-webkit-scrollbar-track { background: var(--parch-sunk); }
        .suggest::-webkit-scrollbar-thumb, .table-scroll::-webkit-scrollbar-thumb {
            background: var(--gilt); border-radius: 5px; border: 2px solid var(--parch-sunk);
        }
        .suggest::-webkit-scrollbar-thumb:hover, .table-scroll::-webkit-scrollbar-thumb:hover { background: var(--gilt-lo); }

        body {
            font-family: var(--font-body);
            font-size: 13.5px;
            /* The parchment is a light surface with dark ink - subpixel AA
               on the default hinting made the small Cinzel caps look fuzzy
               against it. Grayscale smoothing reads cleaner here. */
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            text-rendering: optimizeLegibility;
            /* Lit from above, darkening to the edges, over a coarse weave —
               the flat brown read like a colour swatch rather than a surface. */
            background-color: #4a3a26;
            background-image:
                radial-gradient(ellipse 80% 55% at 50% -5%, rgba(190,158,100,0.26), transparent 70%),
                radial-gradient(ellipse 120% 85% at 50% 45%, transparent 45%, rgba(0,0,0,0.45) 100%),
                repeating-linear-gradient(0deg,  transparent 0 2px, rgba(0,0,0,0.045) 2px 4px),
                repeating-linear-gradient(90deg, transparent 0 2px, rgba(0,0,0,0.045) 2px 4px);
            background-attachment: fixed;
            min-height: 100vh;
            padding: 30px 20px;
        }

        ::selection { background: var(--gilt); color: #fff8e7; }
        html { scrollbar-width: thin; scrollbar-color: var(--gilt) #3a2d1d; }
        html::-webkit-scrollbar { width: 13px; }
        html::-webkit-scrollbar-track { background: #3a2d1d; }
        html::-webkit-scrollbar-thumb {
            background: var(--gilt); border: 3px solid #3a2d1d; border-radius: 7px;
        }
        html::-webkit-scrollbar-thumb:hover { background: #a68a52; }

        /* A hairline of progress pinned to the very top of the viewport,
           driven by in-flight fetches (see the window.fetch wrapper below).
           The page is one document that re-renders in place, so without
           this a slow search just looks like nothing is happening; this is
           the one piece of chrome that can say "working" without moving the
           page or locking it. Reduced-motion users get a still bar rather
           than the sweep. */
        #loadbar {
            position: fixed;
            top: 0; left: 0; right: 0;
            height: 3px;
            z-index: 300;
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.2s ease;
        }
        #loadbar.active { opacity: 1; }
        #loadbar::before {
            content: '';
            position: absolute;
            inset: 0;
            background: linear-gradient(90deg,
                transparent, var(--gilt-lit) 20%, var(--gilt) 50%,
                var(--gilt-lit) 80%, transparent);
            transform: translateX(-100%);
            animation: loadslide 1.1s ease-in-out infinite;
        }
        @keyframes loadslide {
            0%   { transform: translateX(-100%) scaleX(0.4); }
            50%  { transform: translateX(15%)   scaleX(0.5); }
            100% { transform: translateX(100%)  scaleX(0.4); }
        }
        @media (prefers-reduced-motion: reduce) {
            #loadbar::before { animation: none; transform: none; }
        }

        .container {
            max-width: 1100px;
            margin: 0 auto;
            /* mottled parchment instead of one flat fill */
            background-color: var(--parch-page);
            background-image:
                radial-gradient(ellipse 55% 40% at 16% 10%, rgba(255,250,232,0.55), transparent 60%),
                radial-gradient(ellipse 50% 45% at 84% 26%, rgba(150,124,84,0.16), transparent 60%),
                radial-gradient(ellipse 60% 35% at 28% 72%, rgba(150,124,84,0.13), transparent 60%),
                radial-gradient(ellipse 45% 40% at 76% 94%, rgba(255,250,232,0.34), transparent 60%);
            border: 3px solid var(--leather);
            box-shadow:
                0 0 0 1px var(--gilt),
                inset 0 0 0 1px rgba(255,246,224,0.35),
                inset 0 0 40px rgba(120,95,58,0.16),
                0 25px 50px rgba(0,0,0,0.55);
            position: relative;
            overflow: hidden;
            /* The whole sheet settles into place once, on load. This is
               the first thing anyone sees and it used to simply be there
               already, which is the difference between a page that opens
               and a page that blinks on. Fill-mode is deliberately left
               at `none`: the transform must be gone once this finishes,
               or it would remain the containing block for #itemTooltip
               and quietly break its position: fixed for the whole
               session. */
            animation: pageIn 0.45s var(--enter-ease);
        }
        @keyframes pageIn {
            from { opacity: 0; transform: translateY(10px); }
            to   { opacity: 1; transform: none; }
        }

        /* Studded leather bands, top and bottom. The old repeating hard-edged
           bars read as a barcode rather than anything medieval. */
        .container::before, .container::after {
            content: '';
            display: block;
            height: 20px;
            background:
                radial-gradient(circle at 11px 44%,
                    rgba(255,236,196,0.85) 0 2.1px,
                    rgba(198,166,104,0.95) 2.1px 3.4px,
                    rgba(58,42,20,0.95)    3.4px 4.6px,
                    transparent            5.0px) 0 0 / 22px 100% repeat-x,
                linear-gradient(to bottom, #7a6039 0%, #57411f 45%, #3a2a13 100%);
            box-shadow: inset 0 1px 0 rgba(255,228,178,0.28), inset 0 -1px 0 rgba(0,0,0,0.45);
        }
        .container::before { border-bottom: 2px solid var(--leather); }
        .container::after  { border-top: 2px solid var(--leather); }

        .inner { padding: 30px 40px; position: relative; }
        /* A fine grain over the parchment - without it the mottled
           gradients read as a digital wash. It sits on .inner (which has no
           fill of its own) so it lands on the parchment gaps and never on
           the panels, which paint their own opaque surfaces above it. */
        .inner { background-image: var(--grain); }

        h1 {
            font-family: var(--font-display);
            color: var(--ink);
            margin-bottom: 6px;
            text-align: center;
            font-size: 26px;
            font-weight: 600;
            letter-spacing: 4px;
            text-transform: uppercase;
            text-wrap: balance;
            /* carved into the parchment rather than a flat drop shadow */
            text-shadow: 0 1px 0 rgba(255,250,235,0.6), 0 -1px 0 rgba(74,53,32,0.25);
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 16px;
        }
        /* letter-spacing puts a trailing 4px after the final letter that the
           flex centring still counts as part of the text, so the words sit
           ~2px left of true centre between the two ornaments. Pull that
           trailing space back off - the sort of thing you only notice as
           "slightly off" rather than consciously. */
        h1 > span { margin-right: -4px; }
        /* U+2694 renders in text presentation here, which draws as thin
           crossed blades that read as scissors — an inline mark instead. */
        h1 .orn { width: 30px; height: 30px; flex: none; color: #5c4020; opacity: 0.9; }
        @media (max-width: 700px) { h1 .orn { width: 19px; height: 19px; } }

        /* Just the ornament now: "search the trade bots" said what the
           subtitle and the search box directly below both already say, and
           a masthead is not the place to repeat itself. */
        .title-divider {
            display: flex;
            align-items: center;
            gap: 15px;
            margin: 14px 0 18px;
        }
        .title-divider::before, .title-divider::after {
            content: '';
            flex: 1;
            height: 1px;
            background: linear-gradient(to right, transparent, var(--gilt), var(--leather), var(--gilt), transparent);
        }
        .title-divider span { color: var(--gilt); font-size: 16px; letter-spacing: 2px; }

        .subtitle { text-align: center; color: var(--ink-muted); font-size: 13.5px; margin-bottom: 14px; font-style: italic; }

        /* Everything above the tabs used to be four centred lines at
           roughly one size - the strapline, the sync status and the game
           clock all reading as equally important - so you arrived at the
           search box, the actual point of the page, in sixth position
           with nothing having drawn your eye to it. These are telemetry:
           same colour, a step smaller, gathered into one block rather than
           each floating free.

           Framed, too: a hairline rule above and below and a breath of
           light behind, the way a printed page sets its imprint block
           apart from the title. Three centred lines on their own read as
           loose sentences - the same three inside rules read as the
           masthead, which is what they are. */
        .meta-bar {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 5px;
            width: fit-content;
            max-width: 100%;
            margin: 0 auto 18px;
            padding: 9px 24px 10px;
            border-top: 1px solid color-mix(in srgb, var(--gilt) 55%, transparent);
            border-bottom: 1px solid color-mix(in srgb, var(--gilt) 55%, transparent);
            background: linear-gradient(180deg, rgba(255,251,240,0.5), rgba(255,251,240,0.1));
            font-size: 12px;
            color: var(--ink-muted);
        }

        /* Data-source status light, shown beside "Last update" */
        .status-line { display: flex; align-items: center; justify-content: center; gap: 7px; }
        .status-dot {
            display: inline-block;
            flex: none;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: var(--ink-faint);
        }
        .status-dot.up   { background: #059669; box-shadow: 0 0 5px rgba(5,150,105,0.65); }
        .status-dot.warn { background: #b45309; box-shadow: 0 0 5px rgba(180,83,9,0.55); }
        .status-dot.down { background: #b91c1c; box-shadow: 0 0 5px rgba(185,28,28,0.55); }
        .status-dot.neutral { background: var(--ink-faint); box-shadow: 0 0 5px rgba(138,116,88,0.55); }
        .status-dot.pulse { animation: statusPulse 1.6s ease-in-out infinite; }
        /* Pinned to .inner's own top-right corner (right of the title),
           not the viewport - it scrolls away with the header like any
           other part of it, rather than floating over the page the whole
           time. Small and out of the way: this is a diagnostic for
           whoever's checking why something looks stale, not something
           every visitor needs to notice. */
        .sources-status {
            position: absolute;
            top: 4px;
            right: 4px;
            z-index: 5;
            display: flex;
            flex-direction: column;
            gap: 3px;
            padding: 6px 9px;
            background: rgba(44,26,8,0.82);
            border: 1px solid var(--gilt);
            border-radius: 6px;
            font-size: 10.5px;
            color: var(--on-dark-soft);
        }
        .sources-status .src-row { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
        /* Each label links to the source it reports on - dotted underline
           rather than a full one, the same "informational link" treatment
           #sourceLabel uses in the header below. */
        .sources-status a {
            color: inherit;
            text-decoration: underline dotted;
            text-underline-offset: 2px;
            transition: color 0.12s ease;
        }
        .sources-status a:hover { color: #fff3d6; }
        /* The two pages that live one click away, on their own quiet line
           under the clock: not views (that is the nav), not status (that is
           the corner), so they get neither slot. Centred with the rest of
           the masthead by .meta-bar, whose column they are a row of.
           One colour for both - colour here says "a page away", a
           category, and giving each its own hue would make it decoration
           instead - so a small glyph each carries the identity: a clock
           and a ledger. */
        .tools-line {
            display: flex;
            align-items: baseline;
            gap: 10px;
        }
        .tool-link {
            color: var(--tool);
            text-decoration: underline;
            text-decoration-color: var(--tool);
            text-decoration-thickness: 1px;
            text-underline-offset: 2px;
        }
        .tool-link:hover { color: var(--tool-lit); }
        .tool-sep { color: var(--ink-faint); }
        /* Current-colour strokes, so the glyphs are the same teal as the
           labels they belong to and hover keeps working without a rule. */
        .tool-glyph {
            width: 12px;
            height: 12px;
            margin-right: 5px;
            vertical-align: -1px;
            fill: none;
            stroke: currentColor;
            stroke-width: 1.5;
            stroke-linecap: round;
            stroke-linejoin: round;
        }
        /* The page's descriptive line and the night switch share one row:
           the text stays centred on the sheet (the 56px outer columns match
           the switch, so it is centred on the page and not on the space left
           over by the control), and the switch sits at the right edge where
           a setting belongs. Narrow screens stack it, still centred. */
        .subtitle-row {
            display: grid;
            grid-template-columns: 56px 1fr 56px;
            align-items: center;
            gap: 12px;
            margin: 2px 0 12px;
        }
        .subtitle-row .subtitle,
        .subtitle-row .gamedate {
            grid-column: 2;
            text-align: center;
            margin: 0;
            min-width: 0;
        }
        .subtitle-row .theme-switch { grid-column: 3; justify-self: end; }
        @media (max-width: 640px) {
            .subtitle-row { grid-template-columns: 1fr; gap: 8px; }
            .subtitle-row .subtitle,
            .subtitle-row .gamedate,
            .subtitle-row .theme-switch { grid-column: 1; }
            .subtitle-row .theme-switch { justify-self: center; }
        }
        /* The switch itself: a two-state setting, not a destination - in the
           tools line it read as a third place to go. The knob sits under
           whichever icon is active, so the state is visible without reading
           anything; aria-pressed carries the same fact to a screen reader. */
        /* The switch as a small sky, not a generic slider: by day the knob is a
           warm sun disc, by night a pale moon, so which theme you are in reads
           off the knob itself before any icon does. The track is a sunken slot
           (inset shade) so it reads as something to press; the old flat pill with
           a faint off-icon read as decoration. Icons sit exactly on the knob's two
           resting centres (13px and 41px across the 54px padding box). */
        .theme-switch {
            position: relative;
            flex: none;
            width: 56px;
            height: 28px;
            padding: 0;
            background: var(--parch-sunk);
            border: 1px solid var(--gilt);
            border-radius: 14px;
            box-shadow: inset 0 1px 3px rgba(0,0,0,0.30);
            cursor: pointer;
            transition: border-color 0.2s ease;
        }
        .theme-switch:hover { border-color: var(--gilt-hi); }
        .theme-switch:focus-visible { outline: 2px solid var(--gilt-lit); outline-offset: 2px; }
        .theme-switch .ts-knob {
            position: absolute;
            top: 2px;
            left: 2px;
            width: 22px;
            height: 22px;
            border-radius: 50%;
            background: radial-gradient(circle at 35% 30%, #fbe9b0, #e2b857 55%, #b98a2e);
            box-shadow: 0 1px 2px rgba(40,25,10,0.45), 0 0 8px rgba(233,183,80,0.55);
            transition: transform 0.32s cubic-bezier(0.34, 1.4, 0.64, 1),
                        background 0.3s ease, box-shadow 0.3s ease;
        }
        .theme-switch:hover .ts-knob {
            box-shadow: 0 1px 2px rgba(40,25,10,0.45), 0 0 12px rgba(233,183,80,0.8);
        }
        .theme-switch .ts-sun,
        .theme-switch .ts-moon {
            position: absolute;
            top: 6px;
            width: 14px;
            height: 14px;
            fill: none;
            stroke: currentColor;
            stroke-width: 1.6;
            stroke-linecap: round;
            stroke-linejoin: round;
            pointer-events: none;
            transition: color 0.25s ease, opacity 0.25s ease;
        }
        .theme-switch .ts-sun { left: 6px; color: #5a3d12; }
        .theme-switch .ts-moon { left: 34px; color: var(--ink-muted); opacity: 0.8; }
        .theme-switch[aria-pressed="true"] .ts-knob {
            transform: translateX(28px);
            background: radial-gradient(circle at 35% 30%, #fbf4de, #e3d6b0 55%, #b3a174);
            box-shadow: 0 1px 2px rgba(0,0,0,0.6), 0 0 8px rgba(236,226,196,0.35);
        }
        .theme-switch[aria-pressed="true"]:hover .ts-knob {
            box-shadow: 0 1px 2px rgba(0,0,0,0.6), 0 0 12px rgba(236,226,196,0.6);
        }
        .theme-switch[aria-pressed="true"] .ts-sun { color: var(--ink-muted); opacity: 0.8; }
        .theme-switch[aria-pressed="true"] .ts-moon { color: #2a2418; opacity: 1; }
        @media (prefers-reduced-motion: reduce) {
            .theme-switch .ts-knob, .theme-switch .ts-sun, .theme-switch .ts-moon { transition: none; }
        }
        @media (max-width: 700px) {
            /* No room for a status badge in the corner alongside the
               title once it wraps/shrinks at this width - the tooltip
               detail is still one tap away from the header's own status
               dot below, so nothing is lost, just this redundant second
               copy. */
            .sources-status { display: none; }
        }
        /* The scrape runs every 8 hours, so an age well past that is not
           "between runs", it is a failing query - and the numbers on the page
           look exactly as ordinary either way. */
        .stale-warn { color: var(--danger-deep); font-weight: 600; cursor: help; }
        /* One line, in the masthead where the status already lives, saying
           which part of the page could not be refreshed. Every loader keeps
           showing the last good data when its fetch fails - which is right -
           but stale prices look exactly like fresh ones, so it has to say. */
        .fetch-error { color: var(--danger-deep); }
        .fetch-error::before { content: '⚠ '; font-weight: 600; }
        .fetch-error .link-btn { font-size: inherit; padding: 0 2px; }
        /* The colophon. Same shape as the drops ledger's own note, so the two
           pages end the same way they otherwise look the same. */
        .site-foot {
            margin: 26px auto 4px;
            padding-top: 16px;
            border-top: 1px solid #c8b888;
            max-width: 74ch;
            text-align: center;
            font-size: 12px;
            line-height: 1.6;
            color: var(--ink-faint);
        }
        .site-foot p { margin: 0 0 8px; }
        .site-foot p:last-child { margin-bottom: 0; }
        .foot-links a {
            color: var(--ink-soft);
            text-decoration: underline;
            text-decoration-color: var(--gilt);
            text-decoration-thickness: 1px;
            text-underline-offset: 2px;
        }
        .foot-links a:hover { color: var(--link-hover); text-decoration-color: currentColor; }
        .foot-sources { font-size: 11.5px; }
        #sourceLabel a { color: inherit; text-decoration: underline dotted; }
        #sourceLabel a:hover { color: #4a3a1f; }
        /* "EL time 2:09:09 · Ordinary day" ticks, and that is all it does:
           it is a readout, not a destination. It used to double as the way
           into the clock page, which nobody guessed at and which made the
           one live, self-updating line on the page behave unlike every
           other line of text. The clock page has its own entry in the
           top-right utility cluster, next to Instance Drops. */
        .el-time { color: inherit; cursor: default; }

        /* The special-day part shows a hover tooltip, not just "click to
           go to /eltime" like the rest of the link - cursor: help (same
           signal as .warn-badge.npc / .bot-score) is the only thing that
           told those two apart, otherwise it reads as one plain link. */
        #elClockDayWrap, .removed-day-tag { cursor: help; }
        /* Carries the sync-detail tooltip that used to live on the status
           dot next to it (see setStatusDot) - same "hover for more" signal
           as the special-day text above. */
        #lastUpdateText { cursor: help; }
        #elClockMini { font-variant-numeric: tabular-nums; }
        /* A pulled bad day is a separate fact from "what today is" - out of
           the link's own underline (it isn't one, it's just informational)
           and set off as its own small tag rather than run into the same
           dotted-underlined sentence as the clock and special-day text. */
        .removed-day-tag {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            margin-left: 2px;
            font-style: normal;
            font-size: 11.5px;
            color: #7a5a44;
            vertical-align: 1px;
        }
        /* The tag's own `display: inline-flex` above is author CSS, which
           beats the browser's UA-stylesheet `[hidden] { display: none }`
           regardless of specificity or order - so setting .hidden in JS
           silently did nothing and yesterday's removed-day note just sat
           there next to today's real special day. Spell the override out. */
        .removed-day-tag[hidden] { display: none; }
        #elClockMini .el-sec.holding { color: #b91c1c; font-weight: 700; }
        @keyframes statusPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
        /* The pulsing status light was the only thing honouring this - the
           tab fade, the "querying" pulse and every button's hover lift all
           still moved. Cover the lot: nothing here conveys meaning through
           motion, so there's nothing lost by stilling it. */
        @media (prefers-reduced-motion: reduce) {
            .status-dot.pulse, .pulse { animation: none; }
            #tab-search, #tab-history, .results-section.show, .suggest.show,
            .container, .fav-star-btn.active { animation: none; }
            /* The tooltip still fades (opacity alone is not motion and
               losing it would put the hard blink back), it just stops
               travelling. */
            #itemTooltip { transform: none; transition: opacity 0.13s ease, visibility 0.13s; }
            #itemTooltip.show { transform: none; }
            .tab, .side-btn, .bot-btn, .reset-btn, .fav-remove,
            .npc-toggle-btn, .fav-star-btn, .popular-item, .fav-chip,
            tbody tr td { transition: none; }
            .tab-ink { transition: none; }
            #resultsBody tr, .bot-grid.animate .bot-btn,
            .empty::before { animation: none; }
            .tab:hover, .side-btn:hover, .bot-btn:hover, .reset-btn:hover,
            .npc-toggle-btn:hover { transform: none; }
        }

        /* Where the OS asks for more contrast, firm the hairlines up and
           darken the secondary text - better than making someone hunt for a
           setting to make the page legible. */
        @media (prefers-contrast: more) {
            .panel, .chart-wrap, table, .suggest { border-color: var(--ink-soft); }
            .muted, .search-hint, .bot-owner, .bot-sub, .npc-note, .chart-latest,
            .fav-empty, .suggest-empty { color: var(--ink-soft); }
            th { border-right-color: var(--leather); }
            td { border-bottom-color: var(--gilt); border-right-color: var(--gilt-lo); }
            .panel-header, th { text-shadow: none; }
        }

        /* ---- Raised controls ----------------------------------------
           .tab, .side-btn and .bot-btn are one gilt button at three
           sizes, and were being maintained as three near-copies: the
           same gradient, border, text-shadow, hover gradient, lift and
           press state written out in full in each. Only the typography
           and padding actually differ, so only that is left in each of
           them below. .reset-btn / .fav-remove / .fav-star-btn /
           .npc-toggle-btn carry their own colours but depress the same
           way, so they share that half and nothing else.
           -------------------------------------------------------------- */
        .tab, .side-btn, .bot-btn {
            background: var(--face);
            color: var(--on-dark);
            border: 1px solid var(--leather);
            border-bottom-color: var(--ink);
            border-radius: var(--radius-sm);
            font-weight: 600;
            cursor: pointer;
            text-shadow: 0 1px 1px rgba(0,0,0,0.4);
        }
        .tab:hover, .side-btn:hover, .bot-btn:hover { background: var(--face-hi); }
        .tab.active, .bot-btn.active { background: var(--face-on); color: var(--on-gilt); }
        /* The chosen tab is lit rather than merely a different colour -
           a warm bloom off the gilt, which is what separates "this one is
           selected" from "this one happens to be yellow" when four gold
           buttons sit in a row. Only the top-level tabs and the
           mutually-exclusive side filter get it; putting it on every
           active bot button would light up half the page. */
        .tab.active { box-shadow: var(--raise), 0 0 16px rgba(216,185,119,0.55); }

        /* The press itself, shared by everything that physically moves. */
        .tab, .side-btn, .bot-btn, .reset-btn,
        .fav-remove, .fav-star-btn, .npc-toggle-btn {
            transition: transform var(--press-t) var(--press-ease),
                        box-shadow var(--press-t) ease,
                        background var(--tint-t) ease,
                        color var(--tint-t) ease;
        }
        .tab, .side-btn, .bot-btn, .reset-btn, .fav-remove { box-shadow: var(--raise); }
        .tab:hover, .side-btn:hover, .bot-btn:hover, .reset-btn:hover {
            transform: translateY(-1px);
            box-shadow: var(--raise-hi);
        }
        .tab:active, .side-btn:active, .bot-btn:active, .reset-btn:active,
        .fav-remove:active, .fav-star-btn:active {
            transform: translateY(1px);
            box-shadow: var(--pressed);
        }

        /* Search */
        .search-wrap { position: relative; max-width: 720px; margin: 0 auto 8px; }
        /* The one thing the page exists for, so it gets to be the biggest
           thing on it after the title - it was previously the same weight
           as the bot filter box inside the panel below, which is a
           secondary control. */
        /* The leading slot carries one thing at a time: the magnifier while
           the box is empty, the item's own icon once an item is picked (see
           .search-input.icon-shown + .search-glass). */
        .search-glass {
            position: absolute;
            left: 17px;
            top: 50%;
            transform: translateY(-50%);
            width: 18px; height: 18px;
            fill: none;
            stroke: var(--ink-faint);
            stroke-width: 1.9;
            stroke-linecap: round;
            pointer-events: none;
            z-index: 2;
        }
        .search-input.icon-shown + .search-glass { display: none; }
        .search-input {
            width: 100%;
            padding: 17px 20px 17px 46px;
            border: 2px solid var(--gilt);
            border-top-color: var(--gilt-inset);
            border-left-color: var(--gilt-inset);
            border-radius: var(--radius);
            font-size: 17px;
            background: var(--parch);
            color: #1a0c04;
            font-family: var(--font-body);
            box-shadow: inset 2px 2px 5px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.2);
            /* The focus state changes background, border and ring all at
               once; jumping between the two states made the main search box
               - the first thing most people touch - feel abrupt next to
               every button on the page, which all ease. */
            transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
        }
        .search-input:focus {
            outline: none;
            border-color: var(--leather);
            background-color: var(--parch-lit);
            box-shadow: inset 2px 2px 5px rgba(0,0,0,0.1),
                        0 0 0 3px rgba(192,168,80,0.30),
                        0 0 20px rgba(192,168,80,0.20);
        }
        .search-input::placeholder { color: #9a8568; font-style: italic; }

        .search-icon {
            position: absolute;
            left: 12px;
            top: 50%;
            transform: translateY(-50%);
            width: 26px; height: 26px;
            display: none;
            image-rendering: pixelated;
            border: 1px solid var(--gilt);
            background: var(--ink);
            z-index: 2;
        }
        .search-input.icon-shown { padding-left: 48px; }
        /* A clear (x) inside the search field. Clearing a query meant
           either selecting the text and deleting it, or reaching for the
           results panel's own "Clear" in the far corner below - a lot of
           work for the most common correction, and on a phone there's no
           Backspace-everything habit either. It only exists while there is
           text to clear. */
        .search-clear {
            position: absolute;
            right: 9px;
            top: 50%;
            transform: translateY(-50%);
            width: 30px; height: 30px;
            display: flex; align-items: center; justify-content: center;
            background: none;
            border: none;
            border-radius: 50%;
            color: var(--ink-muted);
            font-size: 14px;
            line-height: 1;
            cursor: pointer;
            z-index: 3;
        }
        .search-clear:hover { background: rgba(139,112,64,0.16); color: var(--ink); }
        .search-clear:focus-visible { outline: 2px solid var(--gilt-lit); outline-offset: 1px; }
        /* The author `display: flex` above beats the browser's own
           [hidden] { display: none }, so the hidden state has to be spelled
           out (the same trap .removed-day-tag hit). */
        .search-clear[hidden] { display: none; }
        .search-wrap.has-text .search-input { padding-right: 46px; }

        .fav-star-btn {
            flex: none;
            width: 52px;
            font-size: 22px;
            line-height: 1;
            border-radius: var(--radius-sm);
            background: var(--parch);
            color: var(--gilt-lit-lo);
            border: 2px solid var(--gilt);
            border-top-color: var(--gilt-inset);
            border-left-color: var(--gilt-inset);
            /* Its own sunken face rather than the gilt one - it sits in
               the search row and should read as part of that field, not as
               a button parked next to it. Only the press is shared. */
            box-shadow: inset 2px 2px 5px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.2);
            cursor: pointer;
        }
        .fav-star-btn:hover { color: var(--star); background: #fbf0d0; }
        /* Favouriting something is one of the few purely elective, purely
           pleasurable things you can do on this page, and it used to just
           swap ☆ for ★. classList.toggle is a no-op when the value is
           unchanged, so this fires on the actual act and not on every
           re-render that happens to find it already starred. */
        .fav-star-btn.active { color: var(--star); animation: starPop 0.3s var(--press-ease); }
        @keyframes starPop {
            0%   { transform: scale(1); }
            45%  { transform: scale(1.32); }
            100% { transform: scale(1); }
        }
        .fav-star-btn.active:hover { color: #ffdd55; }

        .suggest {
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            z-index: 20;
            background: var(--parch-lit);
            border: 1px solid var(--leather);
            border-top: none;
            border-radius: 0 0 var(--radius) var(--radius);
            box-shadow: 0 12px 24px rgba(0,0,0,0.35);
            display: none;
            max-height: 320px;
            overflow-y: auto;
        }
        /* Unfolds from its own top edge, where it's attached to the input,
           rather than being switched on. Short - this one appears on
           almost every keystroke, so anything longer would be in the way
           rather than pleasant. */
        .suggest.show { display: block; animation: dropIn 0.15s var(--enter-ease); transform-origin: top center; }
        @keyframes dropIn {
            from { opacity: 0; transform: translateY(-5px) scaleY(0.97); }
            to   { opacity: 1; transform: none; }
        }
        .suggest-item {
            padding: 9px 16px;
            font-size: 13.5px;
            color: var(--ink);
            cursor: pointer;
            border-bottom: 1px solid #e0d4b8;
        }
        /* Otherwise the final row's own separator lands directly on the
           dropdown's border and reads as one thick, slightly darker line. */
        .suggest-item:last-child { border-bottom: none; }
        .suggest-item:hover, .suggest-item.active { background: #e6d9bd; }
        .suggest.kbd .suggest-item:hover { background: transparent; }
        .suggest.kbd .suggest-item.active { background: #e6d9bd; }
        .suggest-item img {
            width: 24px; height: 24px;
            vertical-align: middle;
            margin-right: 8px;
            image-rendering: pixelated;
            border: 1px solid var(--gilt);
            background: var(--ink);
        }
        .suggest-empty { padding: 12px 16px; color: var(--ink-faint); font-style: italic; }

        .search-hint { text-align: center; color: var(--ink-faint); font-size: 12px; margin-bottom: 24px; }

        /* Panel */
        .panel {
            background-color: var(--parch-panel);
            background-image: var(--grain);
            border: 1px solid var(--gilt);
            border-radius: var(--radius);
            /* Clips the header's gradient to the rounded top corners, so the
               radius doesn't reveal a square sliver behind the header. */
            overflow: hidden;
            /* A soft cast shadow as well as the inset bevel - every other
               raised thing here (buttons, tabs, the container itself) sits
               above its background rather than flush against it, and a
               panel of all things shouldn't be the one that reads as
               printed onto the parchment. */
            box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),
                        inset 0 -1px 0 rgba(0,0,0,0.1),
                        var(--panel-shadow);
            margin-top: 6px;
        }
        .panel-header {
            /* Fallback first, then a colour-mixed version that catches a
               little more light at the top for a metal read. */
            background: linear-gradient(180deg, #7a6038 0%, #63492a 52%, #4f3a21 100%);
            background: linear-gradient(180deg,
                color-mix(in srgb, var(--gilt-hi) 32%, var(--gilt-lo)) 0%,
                color-mix(in srgb, var(--gilt-lo) 78%, var(--gilt-inset)) 52%,
                var(--gilt-inset) 100%);
            box-shadow: inset 0 1px 0 rgba(255,238,196,0.45), inset 0 -1px 0 rgba(0,0,0,0.28);
            border-bottom: 1px solid #6d5530;
            padding: 10px 16px;
            font-family: var(--font-display);
            font-size: 12px;
            font-weight: 600;
            letter-spacing: 2px;
            text-transform: uppercase;
            color: var(--on-dark);
            text-shadow: 0 1px 1px rgba(0,0,0,0.3);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .panel-header .count {
            font-size: 11px;
            letter-spacing: 1px;
            color: var(--on-dark-soft);
            background: rgba(0,0,0,0.20);
            padding: 2px 9px;
            border-radius: 999px;
            box-shadow: inset 0 1px 0 rgba(255,255,255,0.12);
        }
        #resultsTitle { text-transform: none; }

        #itemTooltip {
            position: fixed;
            max-width: 340px;
            padding: 10px 14px;
            background: var(--ink);
            color: var(--on-dark);
            border: 1px solid var(--gilt);
            box-shadow: 0 6px 16px rgba(0,0,0,0.5);
            font-size: 12px;
            font-family: var(--font-body);
            line-height: 1.4;
            white-space: pre-line;
            pointer-events: none;
            z-index: 100;
            /* Was display:none/block, so it blinked in and out. This is by
               far the most-repeated interaction on the page - you hover
               your way down a column of items reading descriptions - and
               a hard cut on every one of them is what made the whole page
               feel cheap. visibility (not display) so it still has
               measurable dimensions for positionTooltip while hidden. */
            opacity: 0;
            visibility: hidden;
            transform: translateY(3px);
            transition: opacity 0.13s ease, transform 0.13s var(--enter-ease),
                        visibility 0.13s;
        }
        #itemTooltip.show { opacity: 1; visibility: visible; transform: none; }
        /* The "no owner on record" line inside a bot's hover tooltip. Amber,
           not error-red: it's a gap in the data rather than a fault, but it
           should read as a flag rather than as ordinary text. */
        #itemTooltip .tip-warn { color: #e8bf63; }
        .panel-body { padding: 18px; }

        /* Side filter */
        .filter-bar { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
        /* Sits inside the chart's own bordered box, not the filter bar
           above it - "how much history" reads as a property of the
           chart itself, the same way TradingView/Google Finance put
           their range picker on the chart rather than beside it. */
        .chart-range-bar {
            display: flex;
            justify-content: flex-end;
            gap: 4px;
            padding: 6px 8px 0;
        }
        .range-btn {
            padding: 3px 10px;
            background: transparent;
            color: var(--ink-muted);
            border: 1px solid transparent;
            border-radius: var(--radius-sm);
            font-size: 11px;
            font-weight: 600;
            font-family: var(--font-body);
            cursor: pointer;
        }
        .range-btn:hover { background: rgba(139,112,64,0.12); }
        .range-btn.active {
            background: var(--gilt-lo);
            color: var(--on-dark);
            border-color: var(--leather);
        }
        .chart-latest {
            margin-left: auto;
            align-self: center;
            font-size: 11px;
            color: var(--ink-faint);
        }
        /* Matches the canvas's own `compact` cutoff (its plot width, not
           the viewport) roughly enough - narrow was always an
           approximation, not a number worth keeping in exact lockstep. */
        @media (max-width: 500px) { .chart-latest { display: none; } }
        /* Face, border, shadow, hover and press all come from the
           raised-control block above - only size and lettering here. */
        .side-btn {
            padding: 8px 20px;
            font-size: 12px;
            font-family: var(--font-display);
            text-transform: uppercase;
            letter-spacing: 2px;
        }
        /* Colour-coded before you click them, not only after: all three
           used to be identical gilt buttons whose green/red meaning only
           appeared once selected, so the two sides had to be read rather
           than recognised. Each now carries its side's colour at rest - the
           same green and rust the result rows themselves are tinted with
           (see .row-sell/.row-buy), so the button and the rows it filters
           agree. The label still says Selling/Buying: colour here is a
           second cue, never the only one. */
        /* All gets the same treatment in neutral: with the two sides tinted,
           a plain gilt "All" was the loudest button in the row - heavier
           than the selected one beside it. Three pale chips at rest, one
           saturated button when chosen. */
        .side-btn.btn-all:not(.active) {
            background: color-mix(in srgb, var(--gilt-hi) 22%, var(--parch-lit));
            color: var(--ink-soft);
            border-color: color-mix(in srgb, var(--gilt) 45%, var(--parch-lit));
        }
        .side-btn.btn-all:not(.active):hover {
            background: color-mix(in srgb, var(--gilt-hi) 32%, var(--parch-lit));
        }
        .side-btn.btn-sell:not(.active) {
            background: color-mix(in srgb, var(--success) 14%, var(--parch-lit));
            color: var(--success-deep);
            border-color: color-mix(in srgb, var(--success) 42%, var(--gilt));
        }
        .side-btn.btn-buy:not(.active) {
            background: color-mix(in srgb, var(--danger) 14%, var(--parch-lit));
            color: var(--danger-deep);
            border-color: color-mix(in srgb, var(--danger) 42%, var(--gilt));
        }
        .side-btn.btn-sell:not(.active):hover {
            background: color-mix(in srgb, var(--success) 24%, var(--parch-lit));
        }
        .side-btn.btn-buy:not(.active):hover {
            background: color-mix(in srgb, var(--danger) 24%, var(--parch-lit));
        }
        /* Each of the three carries its own --glow, so the bloom below
           picks up whichever colour that button actually is - a gold halo
           around the green "Selling" button would read as a rendering
           mistake. The box-shadow is written once and re-resolves per
           variant off the inherited custom property. */
        /* Derived from --success/--success-deep rather than a third and
           fourth hand-picked green: the Selling side, a sell row's edge and
           the SELL chip are one idea and should not be three hues. Literal
           first as the fallback, mixed second - the house pattern. */
        .side-btn.btn-sell.active {
            background: linear-gradient(to bottom, #427a3c, #2f5f2a);
            background: linear-gradient(to bottom,
                color-mix(in srgb, var(--success) 74%, var(--gilt-lo)),
                color-mix(in srgb, var(--success-deep) 90%, var(--gilt-lo)));
            --glow: rgba(90,148,80,0.5);
        }
        .side-btn.btn-buy.active  { background: linear-gradient(to bottom, #8a3f31, #6f2a1e); --glow: rgba(168,85,64,0.5); }
        .side-btn.btn-all.active  { background: var(--face-on); --glow: rgba(192,168,80,0.45); }
        /* A brighter inset highlight than --raise carries, plus the bloom:
           whichever of the three is chosen should read as lit, not merely
           tinted, since they sit in a row and differ only by colour. */
        .side-btn.active {
            color: var(--on-gilt);   /* the gold All is a lit face */
            box-shadow: 0 2px 4px rgba(0,0,0,0.3),
                        inset 0 1px 0 rgba(255,255,255,0.25),
                        0 0 12px var(--glow, transparent);
        }

        /* Own class, not .side-btn - that one's generic click handler
           toggles .active across every .side-btn in the document by
           element identity, which would fight this toggle's independent
           on/off state. Deliberately NOT styled like the All/Selling/
           Buying group (a mutually-exclusive choice) - this is a plain
           on/off switch, so it gets a checkbox look and sits apart on the
           far side of the filter bar instead of blending into that row. */
        .npc-toggle-btn {
            margin-left: auto;
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 8px 16px;
            background: var(--parch-sunk);
            color: var(--ink);
            border: 1px solid var(--gilt);
            border-radius: var(--radius-sm);
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
            font-family: var(--font-body);
            /* Deliberately a flatter shadow than --raise: this is a
               checkbox, and it should sit closer to the page than the
               gilt buttons it shares a row with. */
            box-shadow: 0 2px 4px rgba(0,0,0,0.15);
        }
        .npc-toggle-box {
            width: 15px; height: 15px; flex: none;
            border: 2px solid var(--gilt-lo); border-radius: 3px;
            background: #f5efe0;
            position: relative;
        }
        .npc-toggle-btn.active .npc-toggle-box { background: #8a3525; border-color: #5a2015; }
        .npc-toggle-btn.active .npc-toggle-box::after {
            content: '✓'; position: absolute; inset: 0; color: #fff;
            font-size: 12px; line-height: 13px; text-align: center; font-weight: 700;
        }
        .npc-toggle-btn:hover { background: #e2d8c0; transform: translateY(-1px); }
        .npc-toggle-btn:active { transform: translateY(1px); }
        .npc-toggle-btn.active { background: #f0dad0; border-color: #8a3525; }

        /* Grouped results */
        tr.group-header td {
            background: linear-gradient(180deg, #a98f5c 0%, #987d4c 100%);
            color: var(--on-dark);
            font-family: var(--font-display);
            font-size: 12.5px;
            font-weight: 600;
            letter-spacing: 1.5px;
            text-transform: uppercase;
            text-shadow: 0 1px 1px rgba(0,0,0,0.3);
            border-bottom: 1px solid #7a6035;
            border-top: 2px solid var(--leather);
        }
        tr.group-header:first-child td { border-top: none; }
        tr.sell-head td { color: var(--success-deep); background: #dfeeda; text-shadow: none; }
        tr.sell-head td { background: color-mix(in srgb, var(--success) 14%, var(--parch-lit)); }
        tr.buy-head td  { color: var(--danger-deep); background: #f3e2dd; text-shadow: none; }
        tr.buy-head td  { background: color-mix(in srgb, var(--danger) 12%, var(--parch-lit)); }
        .group-count {
            display: inline-block;
            margin-left: 8px;
            background: var(--gilt-lo);
            color: var(--on-dark-soft);
            padding: 1px 8px;
            font-size: 11px;
            letter-spacing: 1px;
            border: 1px solid var(--leather);
        }

        /* Bot list */
        .bot-grid { display: flex; flex-wrap: wrap; gap: 8px; }
        /* The 118-chip grid comes in as a quick wave on first paint. Uses
           `backwards` fill, never `both` - a forwards fill would hold the
           final `transform: none` and kill the hover lift on every chip. */
        @keyframes chipIn {
            from { opacity: 0; transform: translateY(3px) scale(.97); }
            to   { opacity: 1; transform: none; }
        }
        .bot-grid.animate .bot-btn {
            animation: chipIn 0.3s var(--enter-ease) backwards;
            animation-delay: calc(var(--i, 0) * 14ms);
        }
        /* The one of the three that stays lower case and set in Ubuntu -
           these are player names, not labels, and shouting them in Cinzel
           caps made a bot list read as a row of section headings.

           It is also the one there are 118 of. As a full gilt button -
           gradient, drop shadow, lit text - the grid became 118 separate
           raised objects shouting at once, which swamped the search box
           that is the actual point of the page and left the eye nowhere
           to rest. So: a flat parchment chip at rest, and it becomes the
           gilt button on hover or when chosen. Same control, same colours,
           but the field reads as a calm list of names until you reach for
           one. */
        .bot-btn {
            padding: 8px 16px;
            font-size: 12.5px;
            font-family: var(--font-body);
            letter-spacing: 0.5px;
            text-transform: none;
            background: var(--parch-sunk);
            color: var(--ink);
            /* A hairline half-way between the gilt and the chip, and no
               near-black bottom edge (inherited from the raised-button base):
               at full gilt with an ink underline, 119 of these read as a wall
               of boxes rather than a list of names. */
            border-color: #c4ad84;
            border-color: color-mix(in srgb, var(--gilt) 50%, var(--parch-sunk));
            border-radius: 6px;
            text-shadow: none;
            box-shadow: inset 0 1px 0 rgba(255,250,235,0.5);
        }
        /* Hover lifts the surface rather than recolouring the text: the old
           rule turned the label cream while the chip stayed parchment in
           day mode (about 1.4:1, briefly unreadable) and did nothing at all
           at night, where the text was already cream. */
        .bot-btn:hover {
            background: var(--parch);
            color: var(--ink);
            text-shadow: none;
            box-shadow: var(--raise-hi);
        }
        .bot-btn.active { text-shadow: 0 1px 1px rgba(0,0,0,0.4); box-shadow: var(--raise); }

        .popular-row { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:8px; }
        .popular-row:last-child { margin-bottom:0; }
        /* Keeps its own bubble/chip look (background, .bot-btn's button
           chrome) - unlike .trend-link, which has none - but borrows
           .trend-link's underline-on-hover on top of that, so hovering a
           favourite's name reads as "this is a link" the same way
           Risers/Falls does, without giving up the chip styling
           (feedback: liked the underline, wanted the bubble kept). */
        .popular-item {
            display:inline-flex; align-items:center; gap:8px;
            background:var(--parch-sunk); color:var(--ink-soft);
            transition: background 0.12s ease, text-decoration-color 0.12s ease;
        }
        /* `color: var(--ink-soft)` overrides .bot-btn:hover's own
           `color: var(--on-dark)` (a light cream, meant for its darker
           .tab/.side-btn hover background) - this chip's hover background
           stays light, so its text needs to stay dark too, not flip
           lighter (feedback: keep it black on hover). */
        .popular-item:hover { background:var(--parch); color:var(--ink-soft); text-decoration:underline; text-decoration-color:var(--gilt); }
        .popular-item img { margin-right:0; }
        .pop-pct { font-weight:700; margin-left:4px; }

        /* Favourites (kept in the browser via localStorage) */
        .fav-chip { position: relative; display: inline-flex; cursor: grab; transition: opacity 0.12s ease; }
        .fav-chip:active { cursor: grabbing; }
        .fav-chip.dragging { opacity: 0.4; }
        /* A thin bar right on the boundary being targeted, rather than
           shifting/highlighting the whole chip - so which gap you're about
           to drop into (before vs. after it) is unambiguous. */
        .fav-chip.drag-over-before::before, .fav-chip.drag-over-after::after {
            content: ''; position: absolute; top: 2px; bottom: 2px; width: 3px;
            background: var(--gilt-lit-lo); border-radius: 2px; box-shadow: 0 0 4px rgba(160,136,56,0.7);
        }
        .fav-chip.drag-over-before::before { left: -6px; }
        .fav-chip.drag-over-after::after { right: -6px; }
        .fav-chip .fav-remove { border-right: none; }
        .fav-remove {
            padding: 0 10px;
            background: linear-gradient(to bottom, var(--parch-deep), #b8a97f);
            color: var(--star);
            border: 1px solid var(--leather);
            border-bottom-color: var(--ink);
            border-radius: var(--radius-sm);
            font-size: 16px;
            line-height: 1;
            cursor: pointer;
        }
        .fav-remove:hover { background: linear-gradient(to bottom, #f5dfa0, #e0c470); color: #d4a800; }
        /* Inside a favourite, the star is the chip's own left segment, not a
           second raised box glued to it: a leather-edged gradient tile
           (ending on a hard-coded light tan, which at night lit up as a
           beige block) beside a hairline chip read as two controls fused by
           accident. One surface, one hairline, a divider between. */
        .fav-chip .fav-remove {
            background: var(--parch-sunk);
            border: 1px solid #c4ad84;
            border-color: color-mix(in srgb, var(--gilt) 50%, var(--parch-sunk));
            border-right: none;
            border-radius: 6px 0 0 6px;
            box-shadow: none;
        }
        .fav-chip .fav-remove:hover { background: var(--parch-lit); color: var(--star); }
        .fav-chip .popular-item {
            border-radius: 0 6px 6px 0;
            border-left-style: dotted;
        }
        .fav-empty { color: var(--ink-muted); font-style: italic; font-size: 13px; padding: 4px 2px; }

        .bot-filter {
            width: 100%;
            padding: 9px 12px;
            margin-bottom: 12px;
            border: 1px solid var(--gilt);
            border-top-color: var(--gilt-inset);
            border-left-color: var(--gilt-inset);
            border-radius: var(--radius-sm);
            font-size: 12.5px;
            background: var(--parch);
            color: #1a0c04;
            font-family: var(--font-body);
            box-shadow: inset 1px 1px 3px rgba(0,0,0,0.15);
            transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
        }
        .bot-filter:focus {
            outline: none;
            border-color: var(--leather);
            background-color: var(--parch-lit);
            box-shadow: 0 0 0 3px rgba(138,112,64,0.3);
        }

        /* "Back to the main menu" - navigation, not a destructive act, so a
           quiet secondary button rather than the alarm-red face it had,
           which was the loudest control on the results panel and read as
           "delete something". Shadow, lift and press still come from the
           raised-control block. */
        .reset-btn {
            margin-left: auto;
            padding: 8px 20px;
            background: var(--parch);
            color: var(--ink-soft);
            border: 1px solid var(--gilt);
            border-radius: var(--radius-sm);
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
            font-family: var(--font-display);
            text-transform: uppercase;
            letter-spacing: 2px;
            text-shadow: none;
        }
        .reset-btn:hover { background: var(--parch-lit); color: var(--ink); }

        .bot-heading {
            font-family: var(--font-body);
            font-size: 19px;
            font-weight: 400;
            color: var(--ink);
            letter-spacing: 1px;
            text-transform: none;
            margin: 4px 0 2px;
            text-shadow: 1px 1px 0px rgba(255,255,255,0.3);
        }
        .bot-owner {
            font-size: 12px;
            letter-spacing: 1px;
            text-transform: none;
            color: var(--ink-muted);
            font-family: var(--font-body);
        }
        /* A bot with no usable owner gets a flag rather than the plain muted
           "(owned by Unknown)" text - the same warning the hover tooltip
           shows, so the bot's own page says it as loudly as the listings do.
           Amber-on-parchment is kept dark enough to stay above 4.5:1. */
        .bot-owner-unknown {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 1px 9px;
            border-radius: 9px;
            background: rgba(176,118,24,0.13);
            border: 1px solid rgba(150,100,20,0.38);
            color: #7a4a0a;
            font-size: 12px;
            font-family: var(--font-body);
            font-style: normal;
            letter-spacing: 0.5px;
            cursor: help;
            vertical-align: 1px;
        }
        .bot-sub {
            font-size: 12px;
            color: var(--ink-muted);
            font-style: italic;
            margin-bottom: 14px;
        }

        /* ---- Navigation. ------------------------------------------------
           Search and Trends are two views of this page. Instance Drops is
           a different page entirely - and squeezing all three into one
           segmented control made the bar claim they were peers: the third
           segment kept needing its own shape, its own label length and an
           arrow just to say it wasn't one of the others. The control now
           holds the two real tabs - equal halves, the selected one a
           raised gilt pill that slides (positionTabInk) - and Drops sits
           beside it as what it actually is: a link away, dressed like
           every other page-changing link (gilt underline, →, warmer ink).
           ----------------------------------------------------------------- */
        /* The nav holds the two views of this page and nothing else. The
           tools that live one page away - Instance Drops, the EL clock -
           sit in the top-right utility cluster with the data sources, which
           is where every site with a "main app plus side tools" shape puts
           them. That is why the old third element never stopped looking
           like a stranger: a two-option control and a lone link beside it
           are two different vocabularies in one row. */
        .tabbar {
            display: flex;
            gap: 3px;
            width: 320px;
            max-width: 100%;
            margin: 18px auto 22px;
            padding: 4px;
            position: relative;
            /* A leather band, not a pale track. On a parchment page a
               parchment-sunk control with a thin gilt edge read as
               decoration - the primary navigation should be the first
               thing the eye lands on after the title, and only a dark
               surface makes the gilt pill on it look lit. Dark band,
               light lettering, gold for the one you are on: the same
               grammar the panel headers use, inverted. */
            background: linear-gradient(180deg, #6b5030 0%, #4a3520 55%, #33240f 100%);
            border: 1px solid var(--umber);
            border-radius: calc(var(--radius) + 2px);
            box-shadow: 0 3px 9px rgba(44,26,8,0.34),
                        inset 0 1px 0 rgba(255,228,178,0.30);
        }
        /* The pill under the selected segment. Sits behind the labels
           (they carry z-index: 1) and is moved by positionTabInk(). */
        .tab-ink {
            position: absolute;
            left: 0;
            top: 3px;
            bottom: 3px;
            width: 0;
            border-radius: var(--radius);
            background: var(--face-on);
            /* --gilt-bright along the top edge: the same lit-metal catch the
               old sliding filament had, now on the pill itself. */
            box-shadow: var(--raise), inset 0 1px 0 var(--gilt-bright);
            pointer-events: none;
            transition: transform 0.32s var(--enter-ease), width 0.32s var(--enter-ease);
        }
        /* Segments: flat in the track at rest, the pill - not a second
           colour scheme - marking the chosen one. Anything more made the
           unselected half look like an unlit button. */
        .tabbar .tab {
            position: relative;
            z-index: 1;
            flex: 1 1 0;
            min-width: 0;
            padding: 11px 10px;
            background: none;
            border: none;
            box-shadow: none;
            color: var(--on-dark-soft);
            text-align: center;
            text-shadow: 0 1px 1px rgba(0,0,0,0.5);
            text-decoration: none;
            font-size: 12.5px;
            font-weight: 600;
            font-family: var(--font-display);
            text-transform: uppercase;
            letter-spacing: 2px;
        }
        /* The segment under the pointer brightens; it must not pick up the
           gilt face - that means "this is the one you are looking at". */
        .tabbar .tab:hover {
            color: #fff;
            background: rgba(255,236,196,0.14);
            transform: none;
            box-shadow: none;
        }
        .tabbar .tab.active { color: #fff; text-shadow: 0 1px 1px rgba(0,0,0,0.45); }


        /* Keyboard-focus ring for every clickable control above, since none
           of them are native <a> underlines that browsers already outline
           legibly against this dark-gold theme. */
        .tab:focus-visible, .side-btn:focus-visible, .bot-btn:focus-visible,
        .reset-btn:focus-visible, .fav-remove:focus-visible, .link-btn:focus-visible,
        .bot-link:focus-visible, .trend-link:focus-visible, .item-link:focus-visible,
        th.sortable:focus-visible, .popular-item:focus-visible, .fav-star-btn:focus-visible {
            outline: 2px solid var(--gilt-lit);
            outline-offset: 2px;
        }

        /* Content arriving rises a little as it fades, rather than just
           materialising at full opacity where it already was - the rise
           is what reads as "this is new" instead of "the screen
           repainted". Kept to 8px: enough to register, not so far that
           switching tabs looks like a slide transition. */
        #tab-search, #tab-history { animation: riseIn var(--enter-t) var(--enter-ease); }
        @keyframes riseIn {
            from { opacity: 0; transform: translateY(8px); }
            to   { opacity: 1; transform: none; }
        }

        .pulse { animation: pulse 1.4s ease-in-out infinite; }
        @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

        /* Shown once a chart point has been clicked (see showMarketDay in
           app.js): names which day Best Sell/Buy below now shows, since
           without it the tables would silently stop meaning "right now"
           with no visible sign why. */
        .market-day-bar {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 10px;
            margin-top: 14px;
            padding: 6px 12px;
            background: var(--parch-sunk);
            border: 1px solid var(--gilt);
            border-radius: var(--radius-sm);
            font-size: 12.5px;
            color: var(--ink-soft);
        }
        .market-cols {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin-top: 18px;
        }
        .market-col-head {
            font-family: var(--font-display);
            font-size: 12px;
            font-weight: 600;
            letter-spacing: 2px;
            text-transform: uppercase;
            padding: 8px 14px;
            border: 1px solid var(--leather);
            border-bottom: none;
            border-radius: var(--radius) var(--radius) 0 0;
            color: var(--success-deep);
            background: #dfeeda;
            background: color-mix(in srgb, var(--success) 14%, var(--parch-lit));
        }
        .market-col-head.buy-head {
            color: var(--danger-deep);
            background: #f3e2dd;
            background: color-mix(in srgb, var(--danger) 12%, var(--parch-lit));
        }
        @media (max-width: 700px) {
            .market-cols { grid-template-columns: 1fr; }
        }

        /* Chart */
        .chart-wrap {
            position: relative;
            margin-top: 6px;
            border: 1px solid var(--gilt);
            border-radius: var(--radius);
            overflow: hidden;
            background: var(--parch);
            box-shadow: inset 0 1px 3px rgba(0,0,0,0.08);
        }
        /* pan-y: a vertical drag still scrolls the page (the chart is most
           of a phone's width, so stealing that would trap the scroll), while
           a horizontal drag is delivered as pointer events so the readout can
           be scrubbed. Without this the browser claimed the whole gesture and
           a scrub arrived as a scroll instead. */
        /* pointer: a point is clickable (loads that day's Best Sell/Buy,
           see showMarketDay in app.js), not just hoverable for a tooltip. */
        #priceChart { display: block; width: 100%; height: 340px; touch-action: pan-y; cursor: pointer; }
        #priceChart:focus-visible { outline: 2px solid var(--gilt-lit); outline-offset: -2px; }
        @media (max-width: 700px) { #priceChart { height: 260px; } }

        .chart-foot { display: flex; justify-content: space-between; align-items: baseline; margin-top: 6px; }
        .chart-hint { font-size: 12px; color: var(--ink-muted); }
        .link-btn {
            background: none;
            border: none;
            color: var(--ink-muted);
            font-family: var(--font-body);
            font-size: 12px;
            cursor: pointer;
            text-decoration: underline;
            text-decoration-color: #a8916a;
            padding: 2px 4px;
        }
        .link-btn:hover { color: var(--ink-soft); text-decoration-color: var(--ink-soft); }
        #chartTableWrap { margin-top: 8px; }
        #chartTable td, #chartTable th { font-variant-numeric: tabular-nums; }

        /* Table */
        .table-scroll { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
        /* The results list is deliberately NOT capped in height. A sticky-
           header version gave it its own max-height and inner scrollbar, so
           a long search lived in a scrolling box inside the page - two
           scrollbars, and the list never looked complete. It flows in the
           document now: the page is the only scroller, and a search shows
           the whole list. */
        table { width: 100%; border-collapse: collapse; border: 1px solid var(--leather); }
        thead { border-bottom: 2px solid var(--leather); }
        th {
            background: linear-gradient(180deg, #75603a 0%, #614c2c 55%, #4f3d22 100%);
            background: linear-gradient(180deg,
                color-mix(in srgb, var(--gilt-hi) 30%, var(--gilt-lo)) 0%,
                color-mix(in srgb, var(--gilt-lo) 76%, var(--gilt-inset)) 55%,
                var(--gilt-inset) 100%);
            padding: 9px 14px;
            text-align: left;
            font-weight: 600;
            color: var(--on-dark);
            border-right: 1px solid #7a6035;
            font-size: 11px;
            text-transform: uppercase;
            letter-spacing: 1px;
            font-family: var(--font-display);
            text-shadow: 0 1px 1px rgba(0,0,0,0.3);
            box-shadow: inset 0 1px 0 rgba(255,240,200,0.35),
                        inset 0 -2px 0 var(--leather);
        }
        th:last-child { border-right: none; }
        /* No !important needed: th.sortable out-specifies the shared th rule
           (a class beats an element), which is why the flags were only ever
           defensive. */
        th.sortable { cursor: pointer; user-select: none; background: linear-gradient(180deg, #75603a 0%, #614c2c 100%); }
        th.hint-th { cursor: help; }
        th.sortable:hover { background: linear-gradient(180deg, #806a42 0%, #6b5531 100%); }
        th.sortable.active {
            background: linear-gradient(180deg, #75603a 0%, #614c2c 55%, #4f3d22 100%);
            color: var(--on-dark);
            box-shadow: inset 0 -3px 0 var(--gilt-lit);
        }

        /* Every row publishes its own background as a custom property, and
           one declaration reads it. That is the whole reason this table used
           to carry six !importants: the stripe, the side tint and the hover
           state were four selectors of near-identical specificity fighting
           over one `background`, so hover had to shout to be heard.
           Now the rest states set --row-bg, and the hover rules - same
           specificity, later in the sheet - simply win on order. */
        tbody tr td { background: var(--row-bg); transition: background-color 0.14s ease, box-shadow 0.14s ease; }
        tbody tr:nth-child(odd)  { --row-bg: var(--parch-sunk); }
        tbody tr:nth-child(even) { --row-bg: var(--parch-deep); }
        /* Sell/buy rows tint the parchment with the semantic green/red itself
           rather than a hand-picked near-miss, so a row, the "Selling" button
           and a Riser badge are always the same green. Literal first as the
           fallback, the mixed value second. */
        /* A light wash only - the SELL/BUY chip and the grouping carry the
           side, not a flooded row. At 15-18% the table read as two slabs
           (a sickly olive block over a salmon one) and the prices, the
           thing you came for, sat on the loudest colour on the page. */
        tbody tr.row-sell:nth-child(odd) {
            --row-bg: #dfe4cc;
            --row-bg: color-mix(in srgb, var(--success) 7%, var(--parch-sunk));
        }
        tbody tr.row-sell:nth-child(even) {
            --row-bg: #d4dbbf;
            --row-bg: color-mix(in srgb, var(--success) 10%, var(--parch-deep));
        }
        tbody tr.row-buy:nth-child(odd) {
            --row-bg: #eadacc;
            --row-bg: color-mix(in srgb, var(--danger) 6%, var(--parch-sunk));
        }
        tbody tr.row-buy:nth-child(even) {
            --row-bg: #e0cdbd;
            --row-bg: color-mix(in srgb, var(--danger) 9%, var(--parch-deep));
        }
        /* Hover last, so it wins by order at equal specificity - a lift of
           the surface, not a recolour, so it reads the same in both themes. */
        tbody tr:hover          { --row-bg: #e6dcc4; }
        tbody tr.row-sell:hover { --row-bg: #e4e9d2; }
        tbody tr.row-buy:hover  { --row-bg: #f0e0d3; }
        /* An edge down the first cell of the hovered row only: on a dense
           table the tint alone is easy to lose track of once your eye is
           three columns to the right. Not at rest - the SELL/BUY chip, the
           wash and the grouping already say the side, and a permanent
           stripe on every row just said it a fourth time. An inset shadow
           so it costs no layout. */
        tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--gilt); }
        tbody tr.row-sell:hover td:first-child { box-shadow: inset 3px 0 0 var(--success); }
        tbody tr.row-buy:hover td:first-child  { box-shadow: inset 3px 0 0 var(--danger); }

        /* NPC rows get a grey/green stripe over whatever buy/sell tint they
           already have, so a trade NPC reads as visually different from a
           player bot at a glance, not just via the small NPC badge. */
        /* background-image only: the row's colour still comes from --row-bg
           through the one base rule, and this paints the stripe over it. */
        tbody tr.npc-row td {
            background-image: repeating-linear-gradient(45deg,
                rgba(107,85,51,0.30) 0 9px, rgba(31,122,77,0.30) 9px 18px);
        }
        tbody tr.npc-row:hover td {
            background-image: repeating-linear-gradient(45deg,
                rgba(107,85,51,0.40) 0 9px, rgba(31,122,77,0.40) 9px 18px);
        }

        /* Real in-game NPCs get their own stripe, blue (a color used
           nowhere else in these tables) paired with the row's own
           sell/buy green or red, so a genuine game vendor is tellable
           from both a regular bot row and a player-run npc-row at a
           glance. */
        tbody tr.real-npc-row-sell td {
            background-image: repeating-linear-gradient(45deg,
                rgba(58,110,168,0.30) 0 9px, rgba(31,122,77,0.30) 9px 18px);
        }
        tbody tr.real-npc-row-sell:hover td {
            background-image: repeating-linear-gradient(45deg,
                rgba(58,110,168,0.40) 0 9px, rgba(31,122,77,0.40) 9px 18px);
        }
        tbody tr.real-npc-row-buy td {
            background-image: repeating-linear-gradient(45deg,
                rgba(58,110,168,0.30) 0 9px, rgba(168,85,64,0.30) 9px 18px);
        }
        tbody tr.real-npc-row-buy:hover td {
            background-image: repeating-linear-gradient(45deg,
                rgba(58,110,168,0.40) 0 9px, rgba(168,85,64,0.40) 9px 18px);
        }

        /* Was 10px vertical padding around a 32px icon - the icon, not
           the text, set the floor on row height, so trimming padding
           alone barely moves it. Both come down together: with 100+ rows
           a common search returns, the difference is most of a screen's
           worth of extra scrolling. */
        td {
            padding: 7px 14px;
            border-bottom: 1px solid #b8a878;
            border-right: 1px solid #c8b888;
            color: var(--ink);
            font-family: var(--font-body);
            font-size: 13.5px;
        }
        td:last-child { border-right: none; }

        /* Dotted leaders, as an index page in a book of this sort would
           have - see renderTopBots. The filler is a flex child so it eats
           exactly the slack between the name and the number, however wide
           the table happens to be. */
        .leaders { display: flex; align-items: baseline; gap: 8px; }
        .leaders .leader-fill {
            flex: 1;
            align-self: center;
            border-bottom: 1px dotted var(--gilt);
            opacity: 0.45;
        }
        /* Rank and figure columns of the Risers/Falls and Rarest tables.
           The widths are set here rather than in the row builders' inline
           styles so the phone layout can narrow them. */
        td.rank-col { width: 34px; }
        td.pct-col { font-weight: 700; text-align: right; white-space: nowrap; }
        /* Shrink-to-fit, so the name cell really is the greedy one and the
           dotted leaders run all the way to the score. Left to auto layout
           the table split the width, parking the pill a third of a table
           away behind an empty cell. */
        td.score-col { width: 1%; white-space: nowrap; text-align: right; }
        td.price-col { text-align: right; white-space: nowrap; }

        /* The numeric layer: prices, counts, trend figures, the chart's data
           table and the clock seconds. Set in the mono face so digits share
           one width and a column of prices reads as a column rather than a
           ragged edge. Nudged a hair smaller - mono glyphs run large against
           Ubuntu at the same nominal size. */
        .price, .count, td.trend-col, td.qty-col, #chartTable td, #chartTable th,
        #elClockMini, .group-count, .pop-pct, #chartLatest {
            font-family: var(--font-mono);
            font-size: 0.92em;
            font-variant-numeric: tabular-nums;
            font-feature-settings: "tnum" 1;
        }
        .price { font-weight: 700; white-space: nowrap; }
        .muted { color: var(--ink-faint); font-style: italic; }
        .bot-link {
            color: var(--ink);
            text-decoration: underline;
            text-decoration-color: var(--gilt);
            text-underline-offset: 2px;
            cursor: pointer;
            font-weight: 600;
        }
        .bot-link:hover { color: var(--link-hover); text-decoration-color: var(--link-hover); }
        /* Attributes - rarity, side, score. Soft pills: these are properties
           of a thing, not problems with it. */
        .bot-score, .rarity-badge, .side-chip {
            display: inline-block;
            margin-left: 6px;
            padding: 1px 7px;
            font-size: 9px;
            font-weight: 600;
            letter-spacing: 1px;
            border-radius: 8px;
            font-family: var(--font-body);
            vertical-align: middle;
            border: 1px solid var(--ink);
            box-shadow: inset 0 1px 0 rgba(255,255,255,0.20);
        }
        /* Warnings are a different animal and look like one: a squarer tag
           with a warning mark, a mite larger (they were the smallest text on
           the page at 9px in a lozenge, which read as decoration), a tinted
           border instead of the near-black one, and tighter tracking, since
           "NEARLY FULL" is a phrase where "VERY RARE" is a word.
           Shape now says problem-versus-property at a glance. */
        .warn-badge {
            display: inline-block;
            margin-left: 6px;
            padding: 2px 7px 2px 6px;
            font-size: 10px;
            font-weight: 600;
            letter-spacing: 0.5px;
            border-radius: 4px;
            font-family: var(--font-body);
            vertical-align: middle;
            border: 1px solid;
            box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
        }
        .warn-badge::before { content: '⚠'; margin-right: 4px; font-size: 0.9em; }
        /* Which side of the market the row is - see sideChip(): the word,
           not just the row's green/red tint, so it survives colour
           blindness. */
        .side-chip.sell { background: var(--success); color: #fff; }
        .side-chip.buy  { background: #9c3b3b; color: #fff; }
        /* Border is a darker shade of the fill, not the near-black every
           badge used to share - which is what made a warning look like a
           button. */
        /* Soft form in day: a pale tint of the hue with the hue itself as
           the lettering - the day mirror of the night rule (deep tint, light
           lettering). Solid saturated fills with white text were the three
           loudest things on the parchment, and a grid of 119 chips carries
           dozens of them. Still told apart by hue and by the warning mark. */
        /* Translucent, not a pale opaque fill: these sit on bot chips, row
           washes and panels of different tones, and an opaque near-white
           fill read as a sticker pasted on top. A tint of the hue takes on
           whatever surface is underneath instead. */
        .warn-badge.full      { background: rgba(176,52,34,0.14); border-color: rgba(142,42,28,0.55); color: #8a2718; }
        .warn-badge.low-slots { background: rgba(46,77,112,0.13); border-color: rgba(46,77,112,0.50); color: #2a4868; }
        .warn-badge.low-emu   { background: rgba(196,138,38,0.20); border-color: rgba(150,104,20,0.60); color: #6a480a; }
        /* NPC badges are attributes wearing the warning shape's slot (they
           sit where warnings sit): keep the pill and the neutral ink. */
        .warn-badge.npc {
            background: var(--ink-muted); border-color: var(--ink); color: #f0e6cf;
            cursor: help; border-radius: 8px; font-size: 9px; letter-spacing: 1px; padding: 1px 7px;
        }
        .warn-badge.game-npc {
            background: rgba(46,77,112,0.13); border-color: rgba(46,77,112,0.50); color: #2a4868;
            cursor: help; border-radius: 8px; font-size: 9px; letter-spacing: 1px; padding: 1px 7px;
        }
        .warn-badge.npc::before, .warn-badge.game-npc::before { content: none; }
        .npc-note {
            font-size: 12px; color: var(--ink-faint); font-style: italic;
            margin: 4px 0 10px; padding-top: 10px; border-top: 1px dashed var(--parch-sunk);
        }
        .bot-score { font-weight: 700; cursor: help; }
        .bot-score.good { background: var(--success); color: #fff; }
        .bot-score.mid  { background: #7d5c12; color: #fff; }
        .bot-score.poor { background: #9c3b3b; color: #fff; }
        .rarity-badge { cursor: help; }
        /* Same soft form as the warnings, for the same reason: a column of
           solid blue UNCOMMON pills under every row of a search out-shouted
           the prices. */
        .rarity-badge.uncommon  { background: rgba(46,77,112,0.13);  border-color: rgba(46,77,112,0.50);  color: #2a4868; }
        .rarity-badge.rare      { background: rgba(110,50,150,0.13); border-color: rgba(92,45,128,0.50);  color: #562a78; }
        .rarity-badge.very_rare { background: rgba(160,120,20,0.18); border-color: rgba(122,90,10,0.55);  color: #634805; }
        th.trend-col, td.trend-col {
            text-align: right;
            font-size: 12px;
            font-weight: 700;
            white-space: nowrap;
        }
        th.trend-col { cursor: help; }
        td.trend-col.trend-up   { color: var(--up); }
        td.trend-col.trend-down { color: var(--down); }
        td.trend-col.muted { font-weight: 400; }
        /* .trend-link's own color/weight (shared with Risers/Falls and
           Rarest items) would otherwise override the up/down color set
           on the cell - these three classes beat it on specificity alone
           without needing !important. */
        /* Underlined at all times, not just on hover - a colored number
           alone didn't read as clickable, which was the actual problem
           (a 3rd click-target elsewhere in the row wouldn't have fixed
           that, it just would have given people something else to miss). */
        td.trend-col a.trend-link {
            color: inherit; font-weight: inherit;
            text-decoration: underline; text-decoration-color: currentColor;
            text-decoration-thickness: 1px; text-underline-offset: 2px;
        }
        td.trend-col a.trend-link:hover { text-decoration-thickness: 2px; }
        /* Outlined, in the same red family as the NO GOLD tag it produces:
           a real action (it flags the bot for everyone), so it keeps the
           danger hue, but not the solid alarm-red slab it was - which was
           the loudest thing on a bot's page, above its own offers. */
        .no-gc-btn {
            margin-top: 10px;
            padding: 6px 16px;
            background: rgba(176,52,34,0.08);
            color: #8a2718;
            border: 1px solid rgba(142,42,28,0.55);
            border-radius: var(--radius-sm);
            font-size: 11px;
            font-weight: 600;
            cursor: pointer;
            font-family: var(--font-body);
            letter-spacing: 1px;
            text-transform: uppercase;
        }
        .no-gc-btn:hover { background: rgba(176,52,34,0.16); }
        /* Already flagged: the button now undoes something, so it goes
           neutral - the grey gradient it had was the one colour in the
           whole palette that belonged to nothing else. */
        .no-gc-btn.flagged { background: var(--parch); color: var(--ink-soft); border-color: var(--gilt); }
        .no-gc-btn.flagged:hover { background: var(--parch-lit); color: var(--ink); }
        tr.no-gc-buy td { color: var(--ink-faint); }
        tr.no-gc-buy .price { text-decoration: line-through; color: var(--danger-fill); }
        /* Every text link snaps between two browns on hover; easing the
           colour (not the underline, which should appear at once so the
           thing reads as clickable immediately) takes the flicker out of
           running the cursor down a long column of them. */
        .item-link, .trend-link, .bot-link, .link-btn,
        #sourceLabel a { transition: color 0.12s ease, text-decoration-color 0.12s ease; }
        /* font-weight matches .bot-link and .trend-link - both were
           already bold, which made a search result row inconsistent with
           itself (bold Bot cell, plain Item cell in the very same row)
           and item names read lighter on Search than the identical names
           on Trends (Risers/Falls, Rarest, favourites all use
           .trend-link). Not a deliberate distinction, just a gap. */
        /* Underlined at rest, not just on hover. The movers' percentages
           learned this first: a coloured number alone didn't read as
           clickable. Item and bot names had the same problem and one
           worse - on a phone there is no hover at all, so a link that only
           underlines under a pointer is invisible as a link to every touch
           visitor. The rule is: a name that goes somewhere is underlined
           with the same soft gilt, always, and the underline firms up
           under the pointer. */
        .item-link { color: var(--ink); cursor: pointer; font-weight: 600;
            text-decoration: underline; text-decoration-color: var(--gilt);
            text-decoration-thickness: 1px; text-underline-offset: 2px; }
        .item-link:hover { color: var(--link-hover); text-decoration-color: currentColor; }
        .trend-link { color: var(--ink); cursor: pointer; font-weight: 600;
            text-decoration: underline; text-decoration-color: var(--gilt);
            text-decoration-thickness: 1px; text-underline-offset: 2px; }
        .trend-link:hover { color: var(--link-hover); text-decoration-color: currentColor; }

        /* Links that change page - Instance Drops, the EL clock page, the
           outside data sources - carry an arrow, because "underlined" says
           "clickable" but not "and you will be somewhere else". → for
           another page of this site, ↗ for another site. */
        .go-arrow {
            display: inline-block;
            margin-left: 4px;
            font-size: 0.92em;
            line-height: 1;
            opacity: 0.75;
            transition: transform var(--tint-t) ease, opacity var(--tint-t) ease;
        }
        a:hover > .go-arrow, a:focus-visible > .go-arrow { opacity: 1; transform: translateX(2px); }
        a:hover > .go-arrow.ext, a:focus-visible > .go-arrow.ext { transform: translate(2px, -2px); }
        /* .no-loc means "we're browsing one bot's own listing" (see the
           classList.toggle site) - every row's location is necessarily
           the same one already named in the heading above the table, and
           now Bot too: 242 rows all saying "Keres" repeats the one fact
           the panel header ("Bot: Keres") already states, and on a phone
           that repeated name was exactly what left no room for its own
           column. */
        table.no-loc th.loc-col, table.no-loc td.loc-col,
        table.no-loc th.bot-col, table.no-loc td.bot-col { display: none; }
        .itemicon {
            width: 26px; height: 26px;
            vertical-align: middle;
            margin-right: 8px;
            image-rendering: pixelated;
            border: 1px solid var(--gilt);
            border-radius: 3px;
            background: var(--ink);
        }

        .empty { text-align: center; padding: 40px 0; color: var(--ink-muted); font-size: 16px; font-style: italic; }
        /* A bare italic line in the middle of an otherwise heavily
           ornamented page looked like the panel had failed to render
           rather than like a deliberate "nothing here" - the same ✧ the
           title divider uses, faint, reads as intentional. */
        .empty::before {
            content: '✧';
            display: block;
            margin-bottom: 10px;
            font-size: 18px;
            font-style: normal;
            color: #a8916a;
            animation: ghostFloat 3.6s ease-in-out infinite;
        }
        @keyframes ghostFloat {
            0%, 100% { transform: translateY(0);   opacity: 0.55; }
            50%      { transform: translateY(-4px); opacity: 0.95; }
        }

        .results-section { display: none; }
        .results-section.show { display: block; animation: riseIn var(--enter-t) var(--enter-ease); }
        /* Results arrive as a short wave down the table rather than all at
           once, so the eye lands on the top row instead of a block that
           simply blinks into place. Per-row index capped in rowHtml. */
        @keyframes rowIn {
            from { opacity: 0; transform: translateY(-4px); }
            to   { opacity: 1; transform: none; }
        }
        #resultsBody tr {
            animation: rowIn 0.3s var(--enter-ease) both;
            animation-delay: calc(var(--i, 0) * 18ms);
        }

        @media (max-width: 700px) {
            .inner { padding: 18px 12px; }
            h1 { font-size: 20px; letter-spacing: 2px; }

            /* ---- Results as cards. Even trimmed to Item/Price/Bot the
               three-column table still pushed the Bot column off the right
               edge of a 390px phone, and the only way to reach it was a
               sideways scroll on a list people scan vertically. Each row is
               a small tinted card instead - the header is gone, so each
               value names itself - and nothing can overflow sideways. ---- */
            #resultsTable { display: block; border: none; }
            #resultsTable thead { display: none; }
            #resultsTable tbody { display: block; }
            #resultsTable tr {
                display: block;
                border: 1px solid var(--gilt);
                border-radius: var(--radius);
                margin-bottom: 8px;
                overflow: hidden;
            }
            #resultsTable td {
                display: block;
                width: auto;
                border: none;
                border-bottom: 1px solid #c8b888;
                padding: 6px 12px;
            }
            #resultsTable tr td:last-child { border-bottom: none; }
            #resultsTable td.price::before,
            #resultsTable td.bot-col::before,
            #resultsTable td.qty-col::before {
                content: attr(data-label);
                display: inline-block;
                min-width: 46px;
                color: var(--ink-muted);
                font-family: var(--font-body);
                font-size: 10.5px;
                font-weight: 600;
                letter-spacing: 1px;
                text-transform: uppercase;
            }
            #resultsTable tr.group-header td { border-bottom: none; }

            /* The header is already gone, and these are the columns that
               answer "what / how much / from whom" less than Item/Qty/Price/
               Bot do. Spelled with the #id so they out-specify the
               display:block above (an ID selector). Qty stays - "how much
               is on offer" matters as much on a phone as a price does,
               same reasoning as keeping Price/Bot. */
            #resultsTable.no-loc td.bot-col,
            #resultsTable.no-loc td.loc-col,
            #resultsTable td.trend-col,
            #resultsTable td.loc-col { display: none; }

            /* Item name and its icon on one line, badges beneath. */
            #resultsTable .itemicon { width: 22px; height: 22px; }

            /* Filter bar: the three side buttons share a row, and the two
               switches take their own full-width row instead of being
               shoved to the right edge by a margin-left:auto written for
               the desktop single row. */
            .filter-bar { gap: 6px; }
            .side-btn[data-side] { flex: 1 1 0; min-width: 0; padding: 10px 6px; letter-spacing: 1px; }
            .npc-toggle-btn, .reset-btn { margin-left: 0; width: 100%; justify-content: center; }

            /* Roomier touch targets for the controls tapped most often. */
            .bot-btn { padding: 9px 14px; }
            .range-btn { padding: 6px 12px; }
            .panel-body { padding: 14px; }

            /* ---- Navigation. Full width up to the cap (a tablet would
               otherwise stretch two segments across half the page), kept
               at 10px vertical so the touch target stays ~40px tall. ---- */
            .tabbar { width: 100%; max-width: 340px; margin: 14px 0 18px; }
            .tabbar .tab { padding: 10px 8px; letter-spacing: 1.5px; }

            /* The strip's own padding is what pushed the clock line into
               wrapping "— by server" onto a second row at 390px. */
            .meta-bar { padding: 8px 12px 9px; }

            /* ---- Risers/Falls and Rarest items. The dotted leader is a
               desktop luxury: on a phone it claimed 34px of a 160px name
               column, which is what wrapped "Iron Plate Mail" onto a second
               line and left its badge stranded under it. Cell padding comes
               down as well - the rank and figure columns never needed 14px
               a side. ---- */
            .leader-fill { display: none; }
            #trendUp td, #trendDown td, #rarestItems td { padding: 6px 4px; font-size: 13px; }
            td.rank-col { width: 24px; }
            #trendUp .itemicon, #trendDown .itemicon, #rarestItems .itemicon {
                width: 20px; height: 20px; margin-right: 6px;
            }
            #trendUp .rarity-badge, #trendDown .rarity-badge, #rarestItems .rarity-badge {
                margin-left: 4px;
            }

            /* ---- Favourites. As a centred wrapping flex chip a long name
               broke across two or three centred lines ("Red Dragon Helm of
               Life and Mana"), leaving the panel ragged and very tall. One
               per row instead: name left-aligned and free to wrap against
               the chip's own left edge, percentage held out at the right. ---- */
            .popular-row { flex-direction: column; gap: 6px; }
            .fav-chip { width: 100%; }
            .fav-chip .fav-remove { flex: none; }
            .fav-chip .popular-item { flex: 1; min-width: 0; text-align: left; gap: 6px; padding: 9px 10px; }
            .popular-item .pop-name { flex: 1 1 auto; min-width: 0; }
            /* The rarity badge goes on a phone. On desktop its whole point
               is the cursor: help tooltip explaining how the tier was
               worked out - a phone has no hover, so it spends 68px of a
               225px row saying only what tapping the item already shows in
               the chart header below. That 68px is what left
               "Red Dragon Helm of Life and Mana" wrapping over five
               centred lines with the name squeezed to 88px. */
            .popular-item .rarity-badge { display: none; }
            .popular-item .pop-pct { margin-left: auto; flex: none; }
            .popular-item .itemicon { width: 22px; height: 22px; }
        }

        /* ===================== Night =====================
           The same parchment after dark: the sheet becomes a dark leaf,
           the ink becomes lamplight, and the gilt stays gold because gold
           is what the whole identity is made of. Not a second design -
           the day theme's tokens, re-lit - which is why most of this is
           one override block, plus the handful of rules that keep their
           colours outside the token layer.

           <html> carries data-theme="dark" when night is on; the head
           script sets it before first paint from the saved choice, or the
           OS preference when nothing is saved. A browser with scripting
           off never gets the attribute and sees the theme the site has
           always had - the right fallback for a page whose content is a
           market table. */
        :root[data-theme="dark"] {
            color-scheme: dark;

            /* Ink, inverted: lamplight on dark leather rather than dark
               ink on parchment. The faint end still clears 4.5:1 on these
               surfaces - it carries real content (owners, hints, "no
               offers"), not decoration. */
            --ink:          #ecd9b0;
            --ink-soft:     #dcc79c;
            --ink-muted:    #bda87f;
            --ink-faint:    #b5a07a;

            /* The sheet, its panels, and the wells between them: the day
               ramp flipped, so "lit" is still the raised end. */
            /* The ramp mirrors the day relationships, not just the
               lightness: there, panels are the *darkest* surface (a card
               sitting a step deeper than the sheet), the chips are barely
               below the sheet itself, and fields/raised things are the
               lightest. The first night pass inverted that - panels floated
               above the sheet and chips read as holes punched in them - so
               the page stopped looking like the same design. */
            --parch-lit:    #43341f;
            --parch:        #3a2c18;
            --parch-page:   #2b2113;
            --parch-sunk:   #2a2013;
            --parch-deep:   #16110a;
            --parch-panel:  #131009;

            /* Gilt: same hues, lifted a little so frames and panel headers
               still catch light against the dark leaf. The button faces
               are a special case - see --face below. */
            --gilt:         #b08a48;
            --gilt-lo:      #75592f;
            --gilt-inset:   #4e3c20;
            --gilt-hi:      #b3945c;
            --gilt-hi-lo:   #8a6a40;
            --gilt-lit:     #d4b268;
            --gilt-lit-lo:  #b0924e;
            --gilt-bright:  #f0d488;
            --leather:      #0f0a05;
            --umber:        #0b0704;

            /* The gilt family after dark. In daylight nearly everything
               that is not parchment is gold, so the metal reads as
               structure; at night the same brightness turns every chip
               into a lit tile and the gold stops meaning anything. Rest
               and hover are dark bronze; the *lit* face stays gold, but
               deep antique gold rather than the day's bright leaf - a
               day-gold face with its cream text measured 2.3:1 and was
               the brightest thing on the page. "Lit" means warm now, not
               loud. */
            --face:         linear-gradient(180deg, #6d5836 0%, #5a472a 46%, #3e301c 100%);
            --face-hi:      linear-gradient(180deg, #816a43 0%, #6c5533 46%, #4c3b22 100%);
            --face-on:      linear-gradient(180deg, #8f7130 0%, #745c26 46%, #57431c 100%);

            --on-dark:      #f2e4c4;
            --on-dark-soft: #e4d2ac;
            /* Light lettering on the lit (deep antique) gold faces - the
               mirror of the day token. */
            --on-gilt:      #f6e8c8;

            /* Depth: a dark page has less light to catch, so the
               highlights drop and the shadows deepen. */
            --raise:        0 2px 4px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,236,196,0.10);
            --raise-hi:     0 3px 6px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,236,196,0.10);
            --pressed:      inset 0 2px 5px rgba(0,0,0,0.6);

            /* Accents, re-lit for text on dark - and *muted*, which matters
               more than the lightness. The day palette's --*-deep exists to
               take a mid tone down to pass contrast against parchment; at
               night the relationship flips, so the "deep" name holds the
               lighter of the pair. But a mid-saturation accent that reads as
               a quiet note on cream turns into neon against near-black: the
               teal nav link, the blue FEW SLOTS tag and the green dots were
               all doing exactly that (chroma 0.37-0.57 against everything
               else here at 0.14-0.25). Each keeps its hue - teal still means
               "a page away", green still means up/ready, rust still means
               wanted - at an earthy chroma. test_the_night_accents_are_muted
               holds the budget. */
            --danger:       #d99a86;
            --danger-deep:  #e8b4a2;
            --success:      #86bd8a;
            --success-deep: #a3cba6;
            --info:         #a3b3c9;
            --link-hover:   #e8c88a;
            --up:           #8fc4a0;
            --down:         #d9a19b;
            --tool:         #8fb3ae;
            --tool-lit:     #a9c9c3;
            /* The star and the struck-through price: a neon yellow and a
               fire-engine red, the two loudest pixels on the page. */
            --star:         #d4b968;
            --danger-fill:  #a05f4a;

            --panel-shadow: 0 2px 7px rgba(0,0,0,0.45);

            /* The chart, re-lit with the page. Same series hues as the day
               palette, lightened so they read on a dark plot; they
               separate by lightness under deutan, as the day pair
               separates by hue. The hover bubble keeps its dark card -
               it floats over the plot and must read against either. */
            --chart-surface: #382c1a;
            --chart-surface-2: #2a2013;
            --chart-grid:    #4e3f28;
            --chart-axis:    #bda87f;
            --chart-ink:     #ecd9b0;
            --chart-band:    rgba(214,170,105,0.07);
            --chart-sub:     rgba(184,152,112,0.75);
            --chart-sell:    #4fd39a;
            --chart-buy:     #f07d6a;
            --chart-rarity:  #c39ce8;
        }

        /* The page and its sheet: same lighting, fewer lumens. The body
           stays a shade darker than the container so the sheet still
           reads as a leaf lying on leather. */
        :root[data-theme="dark"] body {
            background-color: #0a0b10;
            background-image:
                radial-gradient(ellipse 90% 60% at 50% -8%, rgba(214,160,90,0.22), transparent 72%),
                radial-gradient(ellipse 120% 85% at 50% 45%, transparent 42%, rgba(0,0,0,0.7) 100%),
                repeating-linear-gradient(0deg,  transparent 0 2px, rgba(0,0,0,0.05) 2px 4px),
                repeating-linear-gradient(90deg, transparent 0 2px, rgba(0,0,0,0.05) 2px 4px);
        }
        :root[data-theme="dark"] .container {
            background-image:
                radial-gradient(ellipse 60% 45% at 50% -6%, rgba(226,180,110,0.26), transparent 62%),
                radial-gradient(ellipse 50% 45% at 84% 26%, rgba(0,0,0,0.30), transparent 60%),
                radial-gradient(ellipse 60% 40% at 28% 74%, rgba(0,0,0,0.26), transparent 62%),
                radial-gradient(ellipse 45% 40% at 76% 96%, rgba(214,170,105,0.06), transparent 60%);
            box-shadow:
                0 0 0 1px var(--gilt),
                inset 0 0 0 1px rgba(255,236,196,0.10),
                inset 0 0 50px rgba(0,0,0,0.45),
                0 0 90px rgba(190,130,60,0.10),
                0 25px 60px rgba(0,0,0,0.8);
        }
        /* Panels step up off the sheet instead of blending into it: the
           page went almost black so the sheet could stay a legible brown,
           and the same gap has to open again between sheet and card. */
        :root[data-theme="dark"] .panel {
            /* The day card is mottled parchment; the night card gets the same
               idea in one pass - a little light caught along the top edge,
               falling away down the card. */
            background-image: linear-gradient(180deg, rgba(255,236,196,0.045), rgba(255,236,196,0) 58%),
                              var(--grain);
            box-shadow: inset 0 1px 0 rgba(255,236,196,0.09),
                        inset 0 -1px 0 rgba(0,0,0,0.5),
                        var(--panel-shadow);
        }
        :root[data-theme="dark"] .container::before,
        :root[data-theme="dark"] .container::after {
            background:
                radial-gradient(circle at 11px 44%,
                    rgba(255,236,196,0.45) 0 2.1px,
                    rgba(180,150,92,0.95) 2.1px 3.4px,
                    rgba(24,16,9,0.95)    3.4px 4.6px,
                    transparent            5.0px) 0 0 / 22px 100% repeat-x,
                linear-gradient(to bottom, #4e3c20 0%, #33240f 45%, #1c1309 100%);
            box-shadow: inset 0 1px 0 rgba(255,228,178,0.14), inset 0 -1px 0 rgba(0,0,0,0.6);
        }
        /* Engraved shadows read as shadows only while the light comes from
           above; against a dark surface they invert to a faint glow. */
        :root[data-theme="dark"] h1 {
            text-shadow: 0 1px 0 rgba(255,236,196,0.12), 0 -1px 0 rgba(0,0,0,0.5);
        }
        :root[data-theme="dark"] .bot-heading {
            text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
        }
        :root[data-theme="dark"] .meta-bar {
            background: linear-gradient(180deg, rgba(255,236,196,0.06), rgba(255,236,196,0.01));
        }
        :root[data-theme="dark"] .site-foot { border-top-color: var(--gilt-lo); }
        :root[data-theme="dark"] .removed-day-tag { color: var(--ink-faint); }
        :root[data-theme="dark"] .suggest-item { border-bottom-color: var(--gilt-lo); }
        :root[data-theme="dark"] .empty::before { color: var(--ink-faint); }
        :root[data-theme="dark"] .link-btn { text-decoration-color: var(--gilt-lo); }
        :root[data-theme="dark"] .suggest { box-shadow: 0 12px 24px rgba(0,0,0,0.6); }

        /* Inputs and wells: dark text on parchment was the one place the
           ink was literal, because a colour inherited from the chrome
           would have been the wrong one on a lit field. */
        :root[data-theme="dark"] .search-input,
        :root[data-theme="dark"] .bot-filter {
            color: var(--ink);
        }
        :root[data-theme="dark"] .search-input {
            box-shadow: inset 2px 2px 5px rgba(0,0,0,0.5), 0 2px 6px rgba(0,0,0,0.4);
        }
        /* Content edges go quiet at night: a hundred gilt-framed tiles is a
           golden mesh, and the metal should mean structure (the sheet, the
           headers, the active control), not every chip. */
        :root[data-theme="dark"] .bot-btn { border-color: #4a3a22; }

        /* The checkbox at night: an empty box a shade lighter than the
           button (as in day), and a *checked* box in a saturated red - the
           day's dark maroon read as an empty hole on a dark button, and the
           active button's pale pink face left light text on light pink. */
        :root[data-theme="dark"] .npc-toggle-box {
            background: var(--parch-lit);
            border-color: #8a6a40;
        }
        :root[data-theme="dark"] .npc-toggle-btn.active .npc-toggle-box {
            background: #a63c28;
            border-color: #c05a44;
        }
        :root[data-theme="dark"] .fav-remove {
            background: linear-gradient(to bottom, var(--parch-deep), #3a2c1a);
        }

        /* Hovers that were day literals. Each of these flipped to a *light*
           surface or a dark ink at night - the favourite star lit up like a
           lamp, the NPC toggle went pale, a hovered generic row flashed
           parchment, and "unknown owner" text turned dark-on-dark. Same
           lift, re-lit. */
        :root[data-theme="dark"] .fav-star-btn:hover { background: #3d3020; }
        /* The resting star was a shade too dim on the new field surface
           (4.4:1); the lit gold clears it. */
        :root[data-theme="dark"] .fav-star-btn { color: var(--gilt-lit); }
        :root[data-theme="dark"] .suggest-item:hover,
        :root[data-theme="dark"] .suggest-item.active { background: #46381f; }
        :root[data-theme="dark"] .npc-toggle-btn:hover { background: #3a2e1b; }
        :root[data-theme="dark"] .npc-toggle-btn.active {
            background: #3a231f;
            border-color: #7a3a2f;
        }
        :root[data-theme="dark"] .fav-remove:hover {
            background: linear-gradient(to bottom, #5a4a1e, #47390f);
            color: #ffe08a;
        }
        :root[data-theme="dark"] th.sortable:hover {
            background: linear-gradient(to bottom, #6b5730, #5a4826);
        }
        :root[data-theme="dark"] tbody tr:hover { --row-bg: #33281a; }
        :root[data-theme="dark"] #sourceLabel a:hover { color: var(--link-hover); }

        /* The tooltip is a dark card in the day theme because dark-on-light
           was the readable direction; at night it goes the other way and
           becomes a raised light-on-dark card, or --ink inverts into a
           light box with light text in it. */
        :root[data-theme="dark"] #itemTooltip {
            background: var(--parch-lit);
            color: var(--ink);
        }

        /* Panel and table chrome. The gold headers keep their faces (the
           tokens they mix from are gold in both themes); only the edges
           and bevels need the night treatment. */
        :root[data-theme="dark"] .panel-header {
            background: linear-gradient(180deg, #6b5330 0%, #57431f 55%, #443318 100%);
            border-bottom-color: #4e3c20;
            box-shadow: inset 0 1px 0 rgba(255,238,196,0.22), inset 0 -1px 0 rgba(0,0,0,0.45);
        }
        /* Table headers take the same treatment: the day gradient mixed
           from the gilt tokens, which at night left cream text on a 3.3:1
           metal. */
        :root[data-theme="dark"] th {
            background: linear-gradient(180deg, #6b5330 0%, #57431f 55%, #443318 100%);
        }
        /* And the active range button (7D/30D/1Y), which painted its face
           with the raw --gilt token. */
        :root[data-theme="dark"] .range-btn.active {
            background: #6b5330;
            color: #ffeec2;
            border-color: #4e3c20;
        }
        :root[data-theme="dark"] .tabbar {
            background: linear-gradient(180deg, #4c3a22 0%, #33240f 55%, #241708 100%);
        }
        :root[data-theme="dark"] tr.group-header td {
            background: linear-gradient(180deg, #6f5936 0%, #5b4828 100%);
            border-bottom-color: #4e3c20;
        }
        :root[data-theme="dark"] td,
        :root[data-theme="dark"] #resultsTable td {
            /* the table grid, likewise: a data grid, not a jeweller's */
            border-bottom-color: #2e2415;
            border-right-color: #241d12;
        }
        /* Row washes: same sides, same alternation, just dark enough to
           tint a leaf rather than a page. */
        /* Hand-tuned, not mixed from --success/--danger: those tokens are
           light (they carry text at night), and folding a light colour into
           a dark surface washes the hue out - the first attempt came back
           olive and brown, i.e. exactly the "which side is this?" problem
           the tints exist to solve. Kept green-vs-red dominant but pulled
           toward the leather: the SELL/BUY chip carries the side, so the
           wash only has to hint, and a saturated forest-green slab over a
           maroon one was the loudest thing on the night page. */
        :root[data-theme="dark"] tbody tr.row-sell:nth-child(odd)  { --row-bg: #222a17; }
        :root[data-theme="dark"] tbody tr.row-sell:nth-child(even) { --row-bg: #262f1a; }
        :root[data-theme="dark"] tbody tr.row-buy:nth-child(odd)   { --row-bg: #2e1e16; }
        :root[data-theme="dark"] tbody tr.row-buy:nth-child(even)  { --row-bg: #33211a; }
        /* Hover has to out-rank the nth-child rules above, so it repeats
           them; the day hover tints are literals and never applied here. */
        :root[data-theme="dark"] tbody tr.row-sell:nth-child(odd):hover,
        :root[data-theme="dark"] tbody tr.row-sell:nth-child(even):hover { --row-bg: #2e3920; }
        :root[data-theme="dark"] tbody tr.row-buy:nth-child(odd):hover,
        :root[data-theme="dark"] tbody tr.row-buy:nth-child(even):hover  { --row-bg: #3d271d; }
        /* The bot page's side banners, same reasoning. */
        /* A step deeper than their rows, same warm-leaning hue - the old
           forest green and maroon matched the old saturated row slabs. */
        :root[data-theme="dark"] tr.sell-head td {
            background: #2a3319;
        }
        :root[data-theme="dark"] tr.buy-head td {
            background: #38241a;
        }
        :root[data-theme="dark"] tbody tr.npc-row td {
            background-image: repeating-linear-gradient(45deg,
                rgba(150,124,84,0.20) 0 9px, rgba(90,160,110,0.20) 9px 18px);
        }
        :root[data-theme="dark"] tbody tr.npc-row:hover td {
            background-image: repeating-linear-gradient(45deg,
                rgba(150,124,84,0.30) 0 9px, rgba(90,160,110,0.30) 9px 18px);
        }
        :root[data-theme="dark"] tbody tr.real-npc-row-sell td {
            background-image: repeating-linear-gradient(45deg,
                rgba(96,140,196,0.20) 0 9px, rgba(90,160,110,0.20) 9px 18px);
        }
        :root[data-theme="dark"] tbody tr.real-npc-row-sell:hover td {
            background-image: repeating-linear-gradient(45deg,
                rgba(96,140,196,0.30) 0 9px, rgba(90,160,110,0.30) 9px 18px);
        }
        :root[data-theme="dark"] tbody tr.real-npc-row-buy td {
            background-image: repeating-linear-gradient(45deg,
                rgba(96,140,196,0.20) 0 9px, rgba(186,110,86,0.20) 9px 18px);
        }
        :root[data-theme="dark"] tbody tr.real-npc-row-buy:hover td {
            background-image: repeating-linear-gradient(45deg,
                rgba(96,140,196,0.30) 0 9px, rgba(186,110,86,0.30) 9px 18px);
        }

        /* Pills and warning tags whose day form is a mid-tone fill with
           white text. At night that combination inverts into light fills
           with white text (SELL on #7cc480 was 1.9:1) and, worse, the
           near-black border became a cream one. The night form is the same
           relationship the day theme has: a deep tinted fill, a border a
           shade darker still, and light tinted text. */
        :root[data-theme="dark"] .side-chip.sell,
        :root[data-theme="dark"] .bot-score.good {
            background: #274a2b; color: #cdeecb; border-color: #3c6b41;
        }
        :root[data-theme="dark"] .side-chip.buy,
        :root[data-theme="dark"] .bot-score.poor {
            background: #4f2620; color: #ffd8cd; border-color: #7a3a2f;
        }
        :root[data-theme="dark"] .bot-score.mid {
            background: #4a3a12; color: #ffe9ad; border-color: #7a6222;
        }
        /* Tags at night: the fill is only a faint tint of the leather
           (the night counterpart of the day tags' translucent tint), the
           hue carried by the border and the lettering. As saturated solid
           fills they sat on the rows like stickers - RARE worst, a violet
           slab with glowing lavender letters, the brightest thing in a
           table of prices. Opaque rather than rgba so the contrast test can
           measure the pair; every lettering/fill pair here clears 7:1. */
        :root[data-theme="dark"] .warn-badge.full {
            background: #3a1f19; border-color: #7a3a2c; color: #f0b3a3;
        }
        :root[data-theme="dark"] .warn-badge.low-slots {
            background: #272a2c; border-color: #4a5560; color: #bccad4;
        }
        :root[data-theme="dark"] .warn-badge.low-emu {
            background: #382c16; border-color: #7a5f24; color: #ecd29a;
        }
        :root[data-theme="dark"] .warn-badge.game-npc {
            background: #272a2c; border-color: #4a5560; color: #bccad4;
        }
        :root[data-theme="dark"] .rarity-badge.uncommon {
            background: #272a2c; color: #bccad4; border-color: #4a5560;
        }
        :root[data-theme="dark"] .rarity-badge.rare {
            background: #2c2330; color: #d3c1e0; border-color: #5a4868;
        }
        :root[data-theme="dark"] .rarity-badge.very_rare {
            background: #33291a; color: #e6cf92; border-color: #6e5828;
        }
        /* The active side buttons mixed their fill with the *light* gold
           token, which on a dark page lit up like a lamp; they take the
           deep-tinted-fill treatment instead. */
        :root[data-theme="dark"] .side-btn.btn-sell.active {
            background: linear-gradient(to bottom, #2f5c33, #24471f);
            color: #d9f2d6;
        }
        :root[data-theme="dark"] .side-btn.btn-buy.active {
            background: linear-gradient(to bottom, #5c2f26, #47221b);
            color: #ffd9cf;
        }
        /* The struck-through price sits on the tinted buy row, so it needs a
           lighter red than the muted --danger-fill the fills use; and the
           "flag this bot" button takes the same night form as CLEAR. */
        :root[data-theme="dark"] tr.no-gc-buy .price { color: #d9a08d; }
        /* Count pill on the gold headers: a 20% shade of black over the deep
           gilt was still bright enough to leave its cream text at 4.4:1. */
        :root[data-theme="dark"] .panel-header .count {
            background: rgba(0,0,0,0.36);
        }
        /* Status lights: same hue, night strength - #059669 at full strength
           on a dark card is a tiny neon LED. */
        :root[data-theme="dark"] .status-dot.up {
            background: #4e8f68; box-shadow: 0 0 5px rgba(78,143,104,0.55);
        }
        :root[data-theme="dark"] .status-dot.warn {
            background: #a8762c; box-shadow: 0 0 5px rgba(168,118,44,0.5);
        }
        :root[data-theme="dark"] .status-dot.down {
            background: #a44a3c; box-shadow: 0 0 5px rgba(164,74,60,0.5);
        }
        :root[data-theme="dark"] .no-gc-btn {
            background: #3a1f19;
            color: #f0b3a3;
            border-color: #7a3a2c;
            text-shadow: none;
        }
        :root[data-theme="dark"] .no-gc-btn:hover { background: #4a2820; }
        /* Needs restating: the rule above outranks .no-gc-btn.flagged, so
           a flagged bot's "Unflag" stayed alarm-red at night. */
        :root[data-theme="dark"] .no-gc-btn.flagged {
            background: var(--parch); color: var(--ink-soft); border-color: var(--gilt);
        }
        :root[data-theme="dark"] .no-gc-btn.flagged:hover { background: var(--parch-lit); color: var(--ink); }

        /* Badges whose fill came from an ink token - --ink-muted inverts to
           a light tone, which would have left light text on a light chip. */
        :root[data-theme="dark"] .warn-badge.npc {
            background: #4e3c20;
            border-color: #6f5732;
            color: #f2e4c4;
        }
        :root[data-theme="dark"] .bot-owner-unknown {
            background: rgba(214,170,105,0.10);
            border-color: rgba(214,170,105,0.30);
            color: #e0b878;
        }
        :root[data-theme="dark"] {
            scrollbar-color: var(--gilt) #0a0b10;
        }
        :root[data-theme="dark"]::-webkit-scrollbar-thumb { border-color: #0a0b10; }


    
    
