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

:root {
    /* Tell the engine this UI is dark so NATIVE widgets follow: <select> popups,
       form-control chrome, scrollbars. Load-bearing now that the content pane
       owns the page's only scrollbar. */
    color-scheme: dark;

    --bg: #0f1117;
    --sidebar: #101219;
    --surface: #1a1d27;
    --surface2: #242836;
    --border: #2e3345;
    --text: #e1e4ed;
    --text-dim: #8b8fa3;
    --accent: #cc0000;
    --accent-hover: #e60000;
    --green: #22c55e;
    --yellow: #eab308;
    --red: #ef4444;
    /* Not a state colour — it marks a channel (the beta badge), which has to be legible at a
       glance WITHOUT reading as good/bad. The three above are all spoken for by status. */
    --violet: #a78bfa;

    /* Semantic aliases — use these for state, not the raw brand colors */
    --color-success: var(--green);
    --color-warning: var(--yellow);
    --color-error: var(--red);
    --color-in-stock: var(--green);

    /* Shell geometry. Referenced by the grid and by anything docking against
       the chrome, so the two can never drift apart. */
    --sidebar-w: 210px;
    --viewbar-h: 44px;

    /* A console that fills the pane: the viewport, less the view header and
       everything the section wraps it in — .main's padding (24 + 24), the
       section's bottom margin (24), its header (41) and body padding (16),
       plus a few px of slack. Measured, not guessed: at 133 the pane has no
       scrollbar of its own, which is the whole point of a filling console.
       Derived from --viewbar-h rather than a bare 100vh so the two can't
       drift when the header height changes. */
    --console-h: calc(100dvh - var(--viewbar-h) - 133px);
}

/* Keyboard focus — all interactive elements get a visible ring */
.btn:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible,
button:focus-visible,
[role="button"]:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
}

/* Login — hidden by default. A tiny inline <head> script on each admin
   page adds .no-token to <html> when localStorage has no token, which
   reveals the login box before the first paint. showLogin() uses an
   inline style to override this for the "auth expired" case. */
#login-view {
    display: none;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
}

html.no-token #login-view {
    display: flex;
}

.login-box {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 2rem;
    width: 360px;
}

.login-box h1 {
    font-size: 1.25rem;
    margin-bottom: 1rem;
    text-align: center;
}

.login-box input {
    width: 100%;
    padding: 0.6rem 0.8rem;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-size: 0.9rem;
    margin-bottom: 0.75rem;
}

.login-box input:focus {
    outline: none;
    border-color: var(--accent);
}

.login-error {
    color: var(--red);
    font-size: 0.85rem;
    margin-bottom: 0.5rem;
    display: none;
}

/* Buttons */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 6px;
    font-size: 0.85rem;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s;
}

/* Nothing else greyed a disabled .btn, so one mid-request looked live and invited a second click */
.btn:disabled {
    opacity: 0.6;
    cursor: default;
}

.btn-primary {
    background: var(--accent);
    color: #fff;
    width: 100%;
    justify-content: center;
    padding: 0.6rem;
}

.btn-primary:hover { background: var(--accent-hover); }

.btn-success { background: var(--green); color: #fff; }
.btn-success:hover { opacity: 0.9; }

.btn-warning { background: var(--yellow); color: #000; }
.btn-warning:hover { opacity: 0.9; }

.btn-danger { background: var(--red); color: #fff; }
.btn-danger:hover { opacity: 0.9; }

.btn-secondary { background: #555; color: #fff; }
.btn-secondary:hover { opacity: 0.9; }

.btn-outline {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-dim);
}

.btn-outline:hover { border-color: var(--text-dim); color: var(--text); }

.btn-sm { padding: 0.3rem 0.6rem; font-size: 0.8rem; }

/* ── admin shell ──────────────────────────────────────────────────────────────
   A fixed left rail + a fixed view header over the one and only scrolling pane,
   modelled on the Purchaser UI. Replaces the old horizontal row of text links.

   The shell is exactly one viewport tall and owns its own scrolling, so the
   page behind it never scrolls and the rail can't be dragged off-screen. That
   is scoped to .shell rather than html/body on purpose — the login view is
   outside the shell and still centres itself in the normal way.

   Hidden by default; auth.js sets an inline `display: grid` once the token
   checks out, which is also what keeps the shell from flashing pre-auth. */
.shell {
    display: none;
    height: 100dvh;
    grid-template-columns: var(--sidebar-w) 1fr;
    grid-template-areas: "sidebar content";
    overflow: hidden;
}

.sidebar {
    grid-area: sidebar;
    background: var(--sidebar);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* Chrome is not a document — dragging across it should not select text. */
    user-select: none;
    -webkit-user-select: none;
}

/* Compact identity block, sized like a rail header rather than a masthead:
   one line of 0.8rem, once per window, no emoji. */
.brand {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.85rem 0.85rem 0.7rem;
    flex-shrink: 0;
}

.brand .mark {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    flex-shrink: 0;
    /* The favicon's bullseye, in CSS — same mark in the window and the tab. */
    background: radial-gradient(circle at 50% 50%,
        var(--accent) 0 26%, #fff 26% 44%, var(--accent) 44% 100%);
}

.brand .who { min-width: 0; line-height: 1.25; }
.brand .name, .brand .sub { display: block; }
.brand .name { font-size: 0.8rem; font-weight: 600; color: var(--text); white-space: nowrap; }
.brand .sub {
    font-size: 0.68rem;
    color: var(--text-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidenav {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 0.25rem 0.5rem;
    overflow-y: auto;
    /* Fill the rail so a short window overflows into this scroller instead of
       squashing the rows. `min-height: 0` is what makes the overflow real (a
       flex item won't shrink under its content without it), and it only works
       together with `flex-shrink: 0` on the rows below. */
    flex: 1;
    min-height: 0;
}

.sidenav a {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    height: 34px;
    flex-shrink: 0;
    padding: 0 0.6rem;
    border-radius: 6px;
    color: var(--text-dim);
    text-decoration: none;
    font-size: 0.875rem;
    font-weight: 500;
    white-space: nowrap;
    transition: color 0.15s, background 0.15s;
}

.sidenav a:hover { background: rgba(255, 255, 255, 0.045); color: var(--text); }
.sidenav a.active { background: var(--surface2); color: var(--text); }

/* Accent marker on the selected row, inset so it reads as part of the row
   rather than a separate rule against the window edge. */
.sidenav a.active::before {
    content: "";
    position: absolute;
    left: 0;
    top: 7px;
    bottom: 7px;
    width: 2px;
    border-radius: 0 2px 2px 0;
    background: var(--accent);
}

.sidenav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.sidenav .ic {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: 0.85;
}

.sidenav a.active .ic { opacity: 1; }

.sidenav .nav-label {
    display: block;
    flex-shrink: 0;
    padding: 9px 9px 2px;
    font-size: 10.5px;
    font-weight: 600;
    color: var(--text-dim);
    letter-spacing: 0.01em;
    user-select: none;
}
.sidenav .nav-label:first-child { padding-top: 2px; }

.content {
    grid-area: content;
    display: flex;
    flex-direction: column;
    /* Both required: without them a wide table or a long list forces the grid
       track open instead of scrolling inside .main. */
    min-width: 0;
    min-height: 0;
}

/* The view header — page title on the left, build + connection state on the
   right, where the old top bar used to carry them. */
.viewbar {
    height: var(--viewbar-h);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0 1.5rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
    user-select: none;
    -webkit-user-select: none;
}

.viewbar h1 { font-size: 0.95rem; font-weight: 600; letter-spacing: 0.01em; }

.header-status {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.checker-version {
    font-size: 0.75rem;
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
}

/* An unstamped build is a dev run, not a deploy — call it out rather than letting a
   bare "dev" read as a real version. */
.checker-version.dev { opacity: 0.75; font-style: italic; }

.checker-version:empty { display: none; }

.conn-status {
    font-size: 0.8rem;
    color: var(--text-dim);
}

.conn-status .dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 4px;
    background: var(--red);
}

.conn-status.connected .dot { background: var(--green); }

/* The one and only scrolling pane. No max-width column: the rail already
   anchors the layout on the left, and capping the content as well leaves a
   dead gutter down the right of a maximised window. */
.main {
    flex: 1;
    overflow-y: auto;
    padding: 1.5rem;
}

/* Status cards */
.status-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 1rem;
    margin-bottom: 1.5rem;
}

/* Pool page: time-per-check histograms. One series, so one hue (validated against --surface);
   the share and count are written beside every bar in text colours, never the bar colour. */
.dist-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1rem;
    margin-bottom: 1.5rem;
}
.dist { display: flex; flex-direction: column; gap: 6px; margin-top: 0.6rem; }
.dist-row {
    display: grid;
    grid-template-columns: 5.5rem 1fr 5.5rem;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.8rem;
    padding: 2px 0;
    border-radius: 4px;
}
.dist-row:hover { background: var(--border); }
.dist-label { color: var(--text-dim); white-space: nowrap; }
.dist-track { height: 10px; background: var(--bg); border-radius: 4px; overflow: hidden; }
.dist-bar { display: block; height: 100%; background: #9479ee; border-radius: 0 4px 4px 0; min-width: 0; }
.dist-value { color: var(--text); text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.dist-value .dim { color: var(--text-dim); }
.dist .empty { color: var(--text-dim); font-size: 0.8rem; }

.status-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border);
    border-radius: 8px;
    padding: 1rem;
    transition: border-left-color 0.2s;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.status-card.state-running { border-left-color: var(--color-success); }
.status-card.state-paused  { border-left-color: var(--color-warning); }
.status-card.state-slow    { border-left-color: var(--color-warning); }

.status-card .label {
    font-size: 0.75rem;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 0.3rem;
}

.status-card .value {
    font-size: 1.4rem;
    font-weight: 600;
}

.status-card .value.running { color: var(--green); }
.status-card .value.paused { color: var(--yellow); }
.status-card .value.slow { color: var(--yellow); }

/* Generic health states — the Browser pool card here and the Pool page's tiles. */
.status-card.state-ok    { border-left-color: var(--color-success); }
.status-card.state-warn  { border-left-color: var(--color-warning); }
.status-card.state-bad   { border-left-color: var(--color-error); }
.status-card .value.ok   { color: var(--green); }
.status-card .value.warn { color: var(--yellow); }
.status-card .value.bad  { color: var(--red); }

.status-card .status-sub {
    font-size: 0.8rem;
    color: var(--text-dim);
    margin-top: 0.3rem;
}

/* Purchasers page */

.purchaser-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--border);
    flex-wrap: wrap;
}

.purchaser-item:last-child { border-bottom: none; }

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

.purchaser-name { font-weight: 500; font-size: 0.9rem; }

/* Which instance of that purchaser — several can run on one machine. */
.purchaser-instance { font-weight: 400; color: var(--text-dim); }

.purchaser-version {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    font-family: monospace;
    font-size: 0.75rem;
    color: var(--text-dim);
    margin-top: 0.2rem;
}

/* The separator is the ASCII ESCAPE for U+00B7, not the character: a stylesheet's bytes are
   decoded by whatever charset the response names, and an escape renders the same dot however
   it was read. */
.purchaser-platform::before { content: '\00B7'; margin-right: 0.4rem; }

/* External IP of the purchaser's machine. Same dot separator as the platform chip, and
   selectable text on purpose — it gets pasted into SSH/allowlists. */
.purchaser-ip::before { content: '\00B7'; margin-right: 0.4rem; }
.purchaser-ip { user-select: text; }

.purchaser-badge {
    font-family: inherit;
    font-size: 0.65rem;
    font-weight: 600;
    padding: 0.05rem 0.35rem;
    border-radius: 4px;
}

/* Which channel the row's version came from — a beta instance is running a build the stable
   instances are not, and that wants saying beside its version. */
.purchaser-badge.beta {
    color: var(--violet);
    background: color-mix(in srgb, var(--violet) 15%, transparent);
}

.purchaser-time {
    color: var(--text-dim);
    font-size: 0.75rem;
    white-space: nowrap;
}

/* Right-hand count in a section header, e.g. "3 connected" */
.section-count {
    font-weight: 400;
    font-size: 0.8rem;
    color: var(--text-dim);
}

/* Controls bar */
.controls {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
    flex-wrap: wrap;
}

/* Sections */
.section {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    margin-bottom: 1.5rem;
}

.section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border);
    font-weight: 600;
    font-size: 0.9rem;
}

.section-body { padding: 1rem; }

/* TCIN toolbar */
.tcin-toolbar {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
    align-items: center;
    flex-wrap: wrap;
}

.tcin-toolbar input {
    flex: 1;
    min-width: 160px;
    padding: 0.4rem 0.7rem;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-size: 0.85rem;
}

.tcin-toolbar input:focus {
    outline: none;
    border-color: var(--accent);
}

.tcin-toolbar select {
    padding: 0.4rem 0.7rem;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-size: 0.85rem;
    cursor: pointer;
}

.tcin-toolbar select:focus {
    outline: none;
    border-color: var(--accent);
}

/* TCIN table */
/* No height cap and no overflow of its own, matching .data-table-wrap on every other page: the
   scroll region is .main, so a long watchlist scrolls the page rather than a box inside it. The
   sticky thead below then sticks against .main instead of against this element — which is the
   whole reason there is no overflow here. Setting overflow-x alone would not work: with
   overflow-y visible, the spec computes it to auto, restoring the inner scroller. */
.tcin-table-wrap {
    border-radius: 6px;
    border: 1px solid var(--border);
}

.tcin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
}

.tcin-table thead {
    position: sticky;
    top: 0;
    z-index: 2;
}

.tcin-table th {
    background: var(--surface2);
    color: var(--text-dim);
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    padding: 0.5rem 0.6rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

.tcin-table td {
    padding: 0.4rem 0.6rem;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

.tcin-table tbody tr:hover {
    background: var(--surface2);
}

.tcin-table tbody tr:last-child td {
    border-bottom: none;
}

/* Column widths */
.col-img { width: 40px; }
.col-tcin { width: 90px; }
.col-price { width: 70px; }
.col-stock { width: 170px; }
.col-actions { width: 48px; white-space: nowrap; }

/* Table cell content */
.tcin-table img {
    width: 32px;
    height: 32px;
    object-fit: contain;
    border-radius: 3px;
    background: #fff;
    display: block;
}

.tcin-table .tcin-title {
}

.tcin-table .tcin-title a {
    color: var(--text);
    text-decoration: none;
}

.tcin-table .tcin-title a:hover {
    color: var(--accent);
    text-decoration: underline;
}

.tcin-table .tcin-id {
    font-family: 'JetBrains Mono', 'Fira Code', monospace;
    font-size: 0.8rem;
    color: var(--text-dim);
}

.tcin-table .tcin-price {
    font-family: 'JetBrains Mono', 'Fira Code', monospace;
    font-size: 0.8rem;
}

/* Row states */
.tcin-table tr.pending-row {
    opacity: 0.6;
}

.tcin-table tr.pending-row .tcin-title {
    font-style: italic;
}

.tcin-table tr.archived-row {
    opacity: 0.45;
}

.tcin-table tr.archived-row .tcin-title {
    font-style: italic;
}

.tcin-label {
    font-size: 0.75rem;
    color: var(--text-dim);
    margin-top: 0.15rem;
}

/* Shown instead of the add form when the C# checker owns the watchlist: this page lists it
   read-only. Amber once the copy is stale, like the Licence Keys card. */
.watchlist-source {
    margin-bottom: 0.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface2);
    color: var(--text-dim);
    font-size: 0.85rem;
    overflow-wrap: anywhere;
}

.watchlist-source.stale {
    border-color: var(--yellow);
    color: var(--yellow);
}

/* Responsive
   The rail collapses to icons rather than sliding away behind a hamburger: at
   52px it still costs less width than a menu button, and every destination
   stays one tap away with no open/close state to manage. The labels come back
   in the title attribute so the icons keep their names. */
@media (max-width: 860px) {
    :root { --sidebar-w: 52px; }

    .brand { justify-content: center; padding: 0.7rem 0 0.6rem; }
    .brand .who { display: none; }

    .sidenav { padding: 0.25rem 0.35rem; }
    .sidenav a { justify-content: center; padding: 0; gap: 0; }
    .sidenav .lbl { display: none; }
    /* A 52px rail has no room for a group name, so each one becomes a thin rule
       between its rows and the group above. */
    .sidenav .nav-label {
        height: 1px;
        margin: 6px 8px;
        padding: 0;
        font-size: 0;
        background: var(--border);
    }
    .sidenav .nav-label:first-child { display: none; }
    /* The marker would sit under a centred icon at this width. */
    .sidenav a.active::before { display: none; }

    .viewbar { padding: 0 0.75rem; }
}

@media (max-width: 700px) {
    .main { padding: 0.75rem; }

    .status-grid { grid-template-columns: repeat(2, 1fr); gap: 0.5rem; }

    .controls { flex-wrap: wrap; }

    .tcin-toolbar { flex-direction: column; }
    .tcin-toolbar input { width: 100%; }

    .col-img, .tcin-table td:nth-child(1),
    .col-tcin, .tcin-table td:nth-child(3),
    .col-price, .tcin-table td:nth-child(4) {
        display: none;
    }

    .tcin-table .tcin-title { max-width: none; }

        }

/* Stock history */
.history-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--border);
}

.history-item:last-child { border-bottom: none; }

.history-item img {
    width: 36px;
    height: 36px;
    object-fit: contain;
    border-radius: 4px;
    background: #fff;
}

.history-item .history-info { flex: 1; }

.history-item .history-title {
    font-size: 0.85rem;
}

.history-item .history-meta {
    font-size: 0.75rem;
    color: var(--text-dim);
    font-family: monospace;
}

.history-item .history-time {
    font-size: 0.75rem;
    color: var(--text-dim);
}

.history-item.history-oos {
    opacity: 0.6;
}

.history-item.history-oos .history-meta {
    color: #e57373;
}

.history-item.history-instock .history-meta {
    color: #81c784;
}

/* Log stream */
.log-container {
    height: var(--console-h);
    overflow-y: auto;
    background: var(--bg);
    border-radius: 6px;
    padding: 0.75rem;
    font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
    font-size: 0.78rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-all;
}

.log-line { color: var(--text-dim); }
.log-line.stock { color: var(--green); font-weight: 600; }
.log-line.error { color: var(--red); }
.log-line.warning { color: var(--yellow); }

/* Empty state */
.empty {
    text-align: center;
    padding: 2rem;
    color: var(--text-dim);
    font-size: 0.9rem;
}

/* Log file list */
.log-file-list {
    display: flex;
    flex-direction: column;
}

.log-file-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--border);
    cursor: pointer;
    transition: background 0.15s;
}

.log-file-row:last-child { border-bottom: none; }

.log-file-row:hover { background: var(--surface2); }

.log-file-name {
    font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
    font-size: 0.85rem;
}

.log-file-meta {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.78rem;
    color: var(--text-dim);
    flex-shrink: 0;
    margin-left: 1rem;
}

/* Log file viewer */
.log-viewer {
    height: var(--console-h);
    overflow-y: auto;
    background: var(--bg);
    border-radius: 6px;
    padding: 0.75rem;
    font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
    font-size: 0.78rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-all;
}

.file-viewer-header {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* TCIN loading overlay */
.tcin-section-body { position: relative; }

.spinner {
    width: 32px;
    height: 32px;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}

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

/* Detections page */

.detection-list .history-item:last-child { border-bottom: none; }

.detection-day-heading {
    padding: 0.6rem 0.25rem 0.3rem;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-dim);
    border-bottom: 1px solid var(--border);
    margin-top: 0.5rem;
}

.detection-day-heading:first-child { margin-top: 0; }

.history-time-abs {
    font-size: 0.75rem;
    color: var(--text-dim);
}

.history-time-rel {
    font-size: 0.7rem;
    color: var(--text-dim);
    opacity: 0.7;
}

.load-more-wrap {
    text-align: center;
    padding: 1rem;
}

/* ---------------------------------------------------------------
   Shared data table (the Pool and Purchasers pages). Specific pages
   layer their own column widths and cell classes on top via child
   selectors.
   --------------------------------------------------------------- */
.data-table-wrap {
    border-radius: 6px;
    border: 1px solid var(--border);
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
}

.data-table thead {
    position: sticky;
    top: 0;
    z-index: 2;
}

.data-table th {
    background: var(--surface2);
    color: var(--text-dim);
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    padding: 0.5rem 0.6rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

.data-table td {
    padding: 0.4rem 0.6rem;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

.data-table tbody tr:hover { background: var(--surface2); }
.data-table tbody tr:last-child td { border-bottom: none; }

/* In-stock row highlight — makes the app's primary signal unmissable. */
.data-table tr.instock-row,
.tcin-table tr.instock-row {
    background: rgba(34, 197, 94, 0.06);
}

.data-table tr.instock-row td:first-child,
.tcin-table tr.instock-row td:first-child {
    box-shadow: inset 3px 0 0 var(--color-in-stock);
}

.data-table tr.instock-row:hover,
.tcin-table tr.instock-row:hover {
    background: rgba(34, 197, 94, 0.1);
}

/* Shared toast — one at a time, auto-dismisses. Replaces alert(). */
.toast {
    position: fixed;
    bottom: 1.5rem;
    left: 50%;
    transform: translateX(-50%) translateY(1rem);
    background: var(--surface2);
    border: 1px solid var(--border);
    border-left: 3px solid var(--text-dim);
    border-radius: 6px;
    color: var(--text);
    font-size: 0.85rem;
    padding: 0.7rem 1rem;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s, transform 0.2s;
    z-index: 1000;
    max-width: min(90vw, 420px);
}

.toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    pointer-events: auto;
    cursor: pointer;
}

.toast.toast-error   { border-left-color: var(--color-error); }
.toast.toast-success { border-left-color: var(--color-success); }
.toast.toast-info    { border-left-color: var(--accent); }

/* ── Settings page ──────────────────────────────────────────────────────────
   A label column and a control column. The help text lives WITH the label
   rather than under the input, so a wide screen reads as one sentence per row
   and the controls stay in a single scannable column. */
.settings-lede {
    margin-bottom: 1.5rem;
    padding: 0.9rem 1.1rem;
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text-dim);
    font-size: 0.88rem;
    line-height: 1.5;
}
.settings-lede strong { color: var(--text); }

.setting {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    gap: 0.4rem 1.5rem;
    align-items: start;
    padding: 1rem 0;
    border-bottom: 1px solid var(--border);
}
.setting:first-child { padding-top: 0.25rem; }
.setting:last-child { padding-bottom: 0.25rem; border-bottom: none; }

.setting-label label {
    display: block;
    font-size: 0.9rem;
    font-weight: 500;
    margin-bottom: 0.3rem;
}
.setting-help {
    color: var(--text-dim);
    font-size: 0.8rem;
    line-height: 1.5;
}
.setting-help code, .settings-lede code {
    background: var(--surface2);
    border-radius: 3px;
    padding: 0.05rem 0.3rem;
    font-size: 0.95em;
}
/* Not an error — a condition that makes the field beside it mean something
   other than what it looks like (proxylist.txt winning, no password set). */
.setting-warn {
    margin-top: 0.45rem;
    color: var(--yellow);
    font-size: 0.8rem;
    line-height: 1.5;
}

.setting-control {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.setting-control input,
.setting-control textarea {
    flex: 1;
    min-width: 0;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    padding: 0.5rem 0.7rem;
    font-size: 0.85rem;
    font-family: inherit;
}
.setting-control textarea {
    resize: vertical;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    line-height: 1.5;
}
.setting-control input:focus,
.setting-control textarea:focus {
    outline: none;
    border-color: var(--accent);
}
/* A number box wide enough for a URL invites you to type one into it. Capped so the row
   reads as "10 seconds" rather than as an empty text field with a word after it. */
.setting-control input[type="number"] { flex: 0 0 auto; width: 8rem; }

/* A select reads like the inputs beside it; a checkbox keeps its own size rather than taking
   the stretch-to-fill and padding meant for text boxes. */
.setting-control select {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    padding: 0.5rem 0.7rem;
    font-size: 0.85rem;
    cursor: pointer;
}
.setting-control select:focus { outline: none; border-color: var(--accent); }
.setting-control input[type="checkbox"] {
    flex: 0 0 auto;
    width: 1.1rem;
    height: 1.1rem;
    padding: 0;
    accent-color: var(--accent);
    cursor: pointer;
}

.setting-unit {
    color: var(--text-dim);
    font-size: 0.8rem;
    white-space: nowrap;
}

/* Sticky at the foot of the scrolling pane so Save is reachable from any field
   without scrolling back, and so what-will-change sits next to the button that
   will do it. */
.save-bar {
    position: sticky;
    /* Negative, and load-bearing: a sticky element's offset is measured from the scrollport's
       PADDING box, so `bottom: 0` parks the bar 1.5rem short of the pane's edge and the last
       section scrolls through the gap underneath it. This pulls it flush; the matching
       negative side margins bleed it to the full width so the rule reads as a real edge. */
    bottom: -1.5rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin: -0.5rem -1.5rem 0;
    padding: 0.9rem 1.5rem;
    background: var(--bg);
    border-top: 1px solid var(--border);
}
.save-bar .save-status { flex: 1 1 auto; }
/* .btn-primary carries width:100% for the login box's Sign In button, so a Save wearing that
   class fills the bar and shoves the status line into a 100px column. */
.save-bar .btn { flex: 0 0 auto; width: auto; }

.save-status {
    color: var(--text-dim);
    font-size: 0.83rem;
    line-height: 1.4;
    min-width: 0;
}
.save-status.error { color: var(--color-error); }

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

/* ── Dashboard: the watchlist redesign ───────────────────────────────────────
   Filter chips, a stock pill per row and a ⋯ menu instead of a row of buttons,
   bulk add, and the item drawer. On a phone (below) each row becomes a card. */

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

.uptime { font-size: 0.75rem; color: var(--text-dim); }
.uptime:empty { display: none; }

.status-card.state-ok   { border-left-color: var(--color-success); }
.status-card.state-warn { border-left-color: var(--color-warning); }
.status-card .value.ok   { color: var(--green); }
.status-card .value.warn { color: var(--yellow); }

/* .btn-primary is full-width for the login box; these sit in a row. */
.section-header .btn-primary,
.bulk-add .btn-primary { width: auto; padding: 0.3rem 0.8rem; }

.btn-outline-danger {
    background: transparent;
    border: 1px solid rgba(239, 68, 68, 0.55);
    color: #f87171;
}
.btn-outline-danger:hover { background: rgba(239, 68, 68, 0.12); }

.tcin-toolbar { flex-wrap: wrap; align-items: center; }

.chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-right: auto;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 32px;
    padding: 0 0.8rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 0.8rem;
    cursor: pointer;
}
.chip:hover { border-color: var(--text-dim); }
.chip[aria-pressed="true"] {
    background: var(--text);
    border-color: var(--text);
    color: var(--bg);
    font-weight: 600;
}
.chip-count { font-variant-numeric: tabular-nums; opacity: 0.75; }

/* The one stock answer per row. The dot's SHAPE differs as well as its colour — filled in stock,
   hollow sold out, dashed pending, square archived — so no state rests on hue alone. */
.stock-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    font-size: 0.75rem;
    color: #a3a7b9;
    white-space: nowrap;
}
.pill-dot {
    flex: none;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    border: 1.5px solid #6b7088;
    box-sizing: border-box;
}
.stock-pill.instock {
    color: var(--green);
    border-color: rgba(34, 197, 94, 0.45);
    background: rgba(34, 197, 94, 0.12);
    font-weight: 600;
}
.stock-pill.instock .pill-dot, .pill-dot.instock { background: var(--green); border-color: var(--green); }
.stock-pill.pending { color: var(--yellow); border-color: rgba(234, 179, 8, 0.45); }
.stock-pill.pending .pill-dot { border: 1.5px dashed var(--yellow); }
.stock-pill.archived .pill-dot { border-radius: 2px; border: none; background: #4a5068; }

/* Target's demand marks under the title. Drawing is the one that changes how an item is bought,
   so it is the loud one: solid brand red. Fast selling is an outline — informative, not a state.
   Each carries its word, so neither rests on colour. */
.demand-badges { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.3rem; }
.demand-badge {
    display: inline-flex;
    align-items: center;
    padding: 0.05rem 0.5rem;
    border-radius: 999px;
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
}
.demand-badge.drawing { background: var(--accent); color: #fff; border: 1px solid var(--accent); }
.demand-badge.fast { color: #fb923c; border: 1px solid rgba(251, 146, 60, 0.5); background: rgba(251, 146, 60, 0.1); }

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border-radius: 6px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-dim);
    cursor: pointer;
}
.icon-btn:hover { background: var(--surface2); color: var(--text); }
.icon-btn svg path { fill: none; stroke: currentColor; stroke-width: 1.3; }


.row-menu-wrap { position: relative; display: inline-block; }
.row-menu {
    position: absolute;
    right: 0;
    top: calc(100% + 4px);
    z-index: 5;
    min-width: 170px;
    padding: 4px;
    border-radius: 8px;
    border: 1px solid #353b50;
    background: var(--surface2);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.row-menu button {
    display: block;
    width: 100%;
    padding: 0.5rem 0.7rem;
    border: 0;
    border-radius: 5px;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
    text-align: left;
    cursor: pointer;
}
.row-menu button:hover, .row-menu button:focus-visible { background: #2c3142; }
.row-menu button.danger { color: #f87171; }
.row-menu-sep { height: 1px; margin: 4px 6px; background: #353b50; }

/* Bulk add */
.bulk-add {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-bottom: 1rem;
    padding: 0.9rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
}
.bulk-add[hidden] { display: none; }
.bulk-add label { font-size: 0.85rem; font-weight: 600; }
.bulk-add textarea {
    width: 100%;
    resize: vertical;
    padding: 0.5rem 0.7rem;
    border-radius: 6px;
    border: 1px solid var(--border);
    background: var(--surface2);
    color: var(--text);
    font-family: 'JetBrains Mono', 'Fira Code', monospace;
    font-size: 0.8rem;
}
.bulk-add-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.bulk-preview { margin-right: auto; font-size: 0.8rem; color: var(--text-dim); }
.bulk-results { list-style: none; font-size: 0.8rem; }
.bulk-results:empty { display: none; }
.bulk-results li.ok { color: var(--green); }
.bulk-results li.error { color: #f87171; }

/* Item drawer: a side panel here, a bottom sheet on a phone. */
.drawer[hidden] { display: none; }
.drawer { position: fixed; inset: 0; z-index: 50; }
.drawer-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); }
.drawer-panel {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(440px, 100%);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1.1rem 1.1rem 1.5rem;
    background: var(--surface);
    border-left: 1px solid var(--border);
}
.drawer-grabber { display: none; }
#drawer-body { display: flex; flex-direction: column; gap: 1rem; }
.drawer-head { display: flex; gap: 0.75rem; align-items: flex-start; }
.drawer-img {
    flex: none;
    width: 56px;
    height: 56px;
    border-radius: 8px;
    background: #fff;
    object-fit: contain;
}
div.drawer-img { background: var(--surface2); }
.drawer-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.drawer-title-wrap h2 { font-size: 1rem; line-height: 1.3; }
.drawer-title-wrap a { color: var(--text-dim); }
.drawer-status { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.drawer-section-head { display: flex; align-items: baseline; gap: 0.5rem; justify-content: space-between; }
.drawer-section-head h3 { font-size: 0.85rem; }
.drawer-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: auto; }
.dim { color: var(--text-dim); font-size: 0.8rem; }
.mono { font-family: 'JetBrains Mono', 'Fira Code', monospace; }

.timeline { display: flex; flex-direction: column; gap: 6px; }
.tl-row { display: flex; align-items: center; gap: 0.6rem; }
.tl-day { flex: none; width: 2.8rem; font-size: 0.75rem; color: var(--text-dim); }
.tl-track {
    position: relative;
    flex: 1;
    height: 12px;
    border-radius: 4px;
    background: var(--surface2);
    overflow: hidden;
}
.tl-seg { position: absolute; top: 0; bottom: 0; background: var(--green); border-radius: 2px; }
.tl-axis { display: flex; padding-left: 3.4rem; font-size: 0.7rem; color: #6b7088; }
.tl-axis span { flex: 1; }
.tl-axis span:last-child { flex: none; }

.event-list { list-style: none; display: flex; flex-direction: column; }
.event-list li {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.45rem 0;
    border-bottom: 1px solid var(--border);
    font-size: 0.85rem;
}
.event-list li:last-child { border-bottom: none; }
.event-list li > span:nth-child(2) { flex: 1; }

/* ── Phone: each watchlist row is a card ─────────────────────────────────────
   Read in bed, one-handed. The image, TCIN and price stay hidden by the nth-child rule above;
   the title, stock pill and menu are laid out on a grid, with 44px targets. */
@media (max-width: 700px) {
    /* Uptime answers nothing at a glance in bed; the build and the connection pill stay. */
    .uptime { display: none; }
    .status-card { padding: 0.75rem; }
    .status-card .value { font-size: 1.15rem; }
    .tcin-section-body { padding: 0.6rem; }
    .tcin-toolbar input, .tcin-toolbar select { width: 100%; }
    .chips {
        flex-wrap: nowrap;
        overflow-x: auto;
        margin: 0 -0.6rem;
        padding: 0 0.6rem 2px;
        scrollbar-width: none;
        width: calc(100% + 1.2rem);
    }
    .chip { flex: none; min-height: 36px; font-size: 0.85rem; }

    .tcin-table-wrap { border: none; }
    .tcin-table, .tcin-table tbody { display: block; }
    .tcin-table thead { display: none; }
    .tcin-table tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas:
            "title menu"
            "stock stock";
        align-items: center;
        gap: 0.4rem 0.25rem;
        margin-bottom: 0.6rem;
        padding: 0.75rem 0.5rem 0.75rem 0.85rem;
        border: 1px solid var(--border);
        border-radius: 12px;
        background: var(--surface);
    }
    .tcin-table tbody tr.instock-row { border-color: rgba(34, 197, 94, 0.5); }
    .tcin-table tbody tr:hover { background: var(--surface); }
    .tcin-table tbody td { padding: 0; border: none; }
    .tcin-table tbody tr td:first-child { box-shadow: none; }
    .tcin-table td:nth-child(2) { grid-area: title; font-size: 0.95rem; font-weight: 600; }
    .tcin-table td:nth-child(5) { grid-area: stock; }
    .tcin-table td:nth-child(6) { grid-area: menu; }
    /* The empty state is the first cell too — it must not be hidden with the image column. */
    .tcin-table tbody td.empty { display: block; grid-column: 1 / -1; padding: 1rem; }
    .icon-btn { width: 44px; height: 44px; border-radius: 10px; }

    .drawer-panel {
        top: auto;
        left: 0;
        width: 100%;
        max-height: 88dvh;
        border-left: none;
        border-top: 1px solid var(--border);
        border-radius: 16px 16px 0 0;
        padding-top: 0.5rem;
    }
    .drawer-grabber {
        display: block;
        align-self: center;
        width: 38px;
        height: 4px;
        border-radius: 2px;
        background: #3a3f52;
    }
    .drawer-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .drawer-actions .btn { justify-content: center; min-height: 44px; }
}
