/*
 * Palette sampled off the Kaspa Silver banner rather than picked by eye. The
 * background there is not grey: it is a near-black with a cyan cast (#010609),
 * the surfaces are deep teal (#0D2023, #123E41), and the wordmark is a mint
 * sitting around hue 172 (#7DD8CA). Keeping those relationships is what makes
 * the panel read as the same thing as the channel.
 */
:root {
    --bg: #050b0d;
    --panel: #0a1417;
    --panel-2: #102025;
    --line: #1a3238;
    --text: #e6f4f2;
    --muted: #7d9b9c;
    --accent: #7dd8ca;
    --accent-dim: #2e6f68;
    /* The brighter mint of the glow ring, for things that should look lit. */
    --accent-glow: #4fe3ce;

    /* Status colours are pulled away from the accent hue on purpose. A mint
       accent and a green "ok" next to each other are hard to tell apart, so ok
       is warmer and danger is pushed towards coral. */
    --danger: #ff6b62;
    --warn: #e3a93c;
    --ok: #46ce86;

    --radius: 10px;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    /* The banner's light comes from one neon ring. These are that effect, kept
       in two strengths so it stays a highlight rather than a wash. */
    --glow-soft: 0 0 26px -10px rgba(125, 216, 202, 0.55);
    --glow-hard: 0 0 0 1px rgba(125, 216, 202, 0.28), 0 0 20px -6px rgba(125, 216, 202, 0.6);
}

@media (prefers-color-scheme: light) {
    :root {
        --bg: #eef3f3;
        --panel: #ffffff;
        --panel-2: #e7efee;
        --line: #cadbd9;
        --text: #0c1a1c;
        --muted: #55706f;
        /* Darkened well past the banner mint: #7DD8CA on white is unreadable. */
        --accent: #0f7d71;
        --accent-dim: #9ad4cc;
        --accent-glow: #0f7d71;
        --danger: #c2372f;
        --warn: #9a6a05;
        --ok: #1a7f4b;
        --glow-soft: 0 1px 3px rgba(12, 26, 28, 0.09);
        --glow-hard: 0 0 0 1px rgba(15, 125, 113, 0.3);
    }
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

.hidden { display: none !important; }

/* ------------------------------------------------------------------ login */

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

.login-card {
    width: min(380px, 100%);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 28px;
    display: grid;
    gap: 14px;
}

/* ---------------------------------------------------------------- shell */

#app {
    display: flex;
    min-height: 100vh;
}

/* --- sidebar --- */

.sidebar {
    /* Width is driven by this one custom property: the collapsed and drawer
       rules below just redefine it. Overriding `width`/`flex-basis` directly
       does not reliably beat the `flex` shorthand once var() is involved, so
       changing the variable is the dependable way to resize this. */
    /* Wide enough that every service name fits whole beside an Install
       button. A name that has to be hovered to be read is a name the sidebar
       is failing to show, and 20 pixels of page is a cheap price for it. */
    --sidebar-w: 252px;
    width: var(--sidebar-w);
    flex: 0 0 var(--sidebar-w);
    min-width: 0;
    background: var(--panel);
    border-right: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    z-index: 30;
}
/* Width is intentionally not transitioned: animating the width of a flex item
   relayouts the whole content column every frame, which stutters on the
   table-heavy Mining tab. The drawer below animates `transform` instead, which
   is composited and cheap. */

.sidebar.collapsed {
    --sidebar-w: 62px;
}

/* Collapsing hides every text label in one rule, so new nav entries and
   footer items inherit the behaviour without extra CSS. */
.sidebar.collapsed .label,
.sidebar.collapsed .nav-group-title,
.sidebar.collapsed .badge,
.sidebar.collapsed #logout {
    display: none;
}
.sidebar.collapsed .sidebar-head { justify-content: center; }
.sidebar.collapsed .nav-item { justify-content: center; padding-inline: 0; }
.sidebar.collapsed #sidebar-toggle svg { transform: rotate(180deg); }

.sidebar-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 14px 12px;
    border-bottom: 1px solid var(--line);
    min-height: 56px;
}

.brand {
    font-weight: 650;
    display: flex;
    align-items: center;
    gap: 9px;
    white-space: nowrap;
    overflow: hidden;
}

.dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--muted);
    flex: 0 0 auto;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--muted) 25%, transparent);
}
.dot.ok { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 25%, transparent); }
.dot.warn { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 25%, transparent); }
.dot.bad { background: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 25%, transparent); }

.icon-button {
    background: none;
    border: 0;
    color: var(--muted);
    padding: 6px;
    border-radius: 7px;
    cursor: pointer;
    display: inline-flex;
    line-height: 0;
}
.icon-button:hover { background: var(--panel-2); color: var(--text); }
.icon-button svg { width: 18px; height: 18px; transition: transform 0.18s ease; }

/* Scoped rather than global: these are the line icons, and a blanket svg rule
   would fight anything drawn later (charts, logos). */
.sidebar svg,
.topbar svg {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.nav {
    flex: 1;
    overflow-y: auto;
    padding: 10px 8px 16px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.nav-group-title {
    margin: 14px 0 4px;
    padding: 0 8px;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--muted);
    opacity: 0.75;
}
.nav-group-title:first-child { margin-top: 2px; }

.nav-item {
    display: flex;
    align-items: center;
    gap: 9px;
    flex: 1;
    min-width: 0;
    /* Little right padding: the row's own gap already separates the name from
       the control, and every pixel here is a pixel of name. */
    padding: 9px 2px 9px 10px;
    border: 0;
    border-radius: 8px 0 0 8px;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: 14px;
    text-align: left;
    cursor: pointer;
    white-space: nowrap;
}
/* The row is a name and a control, and the control is the part that must not
   move. Without this the name is nowrap with nothing to clip it, so a long one
   ran straight under the Install button instead of giving way to it. */
.nav-item .label {
    flex: 1;
    min-width: 0;
    /* The sidebar is sized so nothing here needs truncating. This stays as the
       floor: a longer name added later gives way to the button rather than
       running underneath it, which is the failure that is hard to notice. */
    overflow: hidden;
    text-overflow: ellipsis;
}
.nav-item svg { width: 18px; height: 18px; flex: 0 0 18px; }
.nav-item.active { color: var(--text); }
.nav-item.active svg { color: var(--accent); }

/* Highlighting the row rather than the button keeps the switch inside the
   selected surface; on the button alone it looked detached from its entry. */
.nav-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding-right: 6px;
    border-radius: 8px;
}
.nav-row:hover { background: var(--panel-2); }
.nav-row:hover .nav-item { color: var(--text); }
.nav-row.active {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    box-shadow: inset 2px 0 0 var(--accent);
}

.sidebar-foot {
    border-top: 1px solid var(--line);
    padding: 12px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}

.badge {
    font-family: var(--mono);
    font-size: 12px;
    padding: 3px 8px;
    border: 1px solid var(--line);
    border-radius: 999px;
    color: var(--muted);
}
.badge.local {
    color: var(--warn);
    border-color: color-mix(in srgb, var(--warn) 40%, transparent);
    cursor: help;
}

/* --- content column --- */

.content {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.topbar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 0 20px;
    min-height: 56px;
    background: var(--panel);
    border-bottom: 1px solid var(--line);
    position: sticky;
    top: 0;
    z-index: 20;
}

.topbar h1 {
    font-size: 16px;
    font-weight: 600;
    margin: 0;
}

/* The hamburger only exists for the drawer layout. */
#sidebar-open { display: none; }

.sidebar-scrim {
    position: fixed;
    inset: 0;
    background: rgb(0 0 0 / 0.5);
    z-index: 25;
}

@media (max-width: 860px) {
    /* Below this width the sidebar becomes an overlay drawer: keeping it in
       flow would eat most of a phone screen. */
    .sidebar {
        position: fixed;
        left: 0;
        top: 0;
        transform: translateX(-100%);
        transition: transform 0.2s ease;
        box-shadow: 0 0 40px rgb(0 0 0 / 0.4);
    }
    .sidebar.open { transform: translateX(0); }
    /* The drawer is always full width; collapsing is a wide-screen idea. */
    .sidebar.collapsed { --sidebar-w: 252px; }
    .sidebar.collapsed .label,
    .sidebar.collapsed .nav-group-title,
    .sidebar.collapsed .badge,
    .sidebar.collapsed #logout { display: revert; }
    .sidebar.collapsed .nav-item { justify-content: flex-start; padding-inline: 10px; }
    #sidebar-toggle { display: none; }
    #sidebar-open { display: inline-flex; }
}

@media (min-width: 861px) {
    .sidebar-scrim { display: none; }
}

/* ------------------------------------------------------------------ layout */

/* The cap that decides how wide every page is. 1180 left a third of a large
   monitor empty; the tables in particular had room to spare and were still
   scrolling sideways. Raised rather than removed, because full-width prose on
   an ultrawide is worse than the margin. */
/* min-width: 0 is the important part. main is a flex item of .content, and a
   flex item defaults to min-width: auto, which means it refuses to shrink below
   its content. One wide table was therefore pushing the whole page out to the
   cap and giving it a horizontal scrollbar, so Broadcasts and Mining rendered
   wider than every other tab. With this, .scroll-x scrolls the table inside
   itself the way it was always meant to. */
main {
    padding: 20px;
    /* width:100% matters as much as the cap. .content is a column flex box, and
       `margin: 0 auto` cancels the default stretch, so without a definite width
       main sizes to its own content: one wide table pushed the whole page out
       to the cap and gave it a horizontal scrollbar, which is why Broadcasts
       and Mining came out wider than every other tab. With a definite width,
       .scroll-x scrolls the table inside itself as intended. */
    width: 100%;
    max-width: 1480px;
    min-width: 0;
    margin: 0 auto;
}
.tab { display: none; }
.tab.active { display: block; }

/* `hidden` has to beat `display: grid`, or hiding a grid does nothing. The log
   tiles needed the same guard for the same reason. */
.grid[hidden] { display: none; }

.grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px;
    align-items: start;
}
.span2 { grid-column: span 2; }

@media (max-width: 820px) {
    .grid { grid-template-columns: 1fr; }
    .span2 { grid-column: span 1; }
}

.card {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 18px;
}

.card h3 {
    margin: 0 0 12px;
    font-size: 14px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    display: flex;
    align-items: center;
    gap: 10px;
}

/* ------------------------------------------------------------------- bits */

.muted { color: var(--muted); }
.hint { color: var(--muted); font-size: 13px; margin: 10px 0 0; }
.error { color: var(--danger); font-size: 13px; margin: 8px 0 0; }

.bigstat { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.bigstat span { font-size: 34px; font-weight: 650; font-variant-numeric: tabular-nums; }
.bigstat small { color: var(--muted); }

.bar { height: 6px; background: var(--panel-2); border-radius: 999px; overflow: hidden; margin-bottom: 14px; }
.bar div { height: 100%; width: 0; background: var(--accent); transition: width 0.6s ease; }

.kv { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 0; }
.kv.vertical { grid-template-columns: 1fr; gap: 7px; }
.kv > div { min-width: 0; }
.kv dt { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
.kv dd { margin: 2px 0 0; font-family: var(--mono); font-size: 13px; overflow-wrap: anywhere; }
/* A value that reports something switched off, rather than a number. */
.kv dd.warn { color: var(--warn); }
.kv.vertical > div { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
.kv.vertical dt { text-transform: none; letter-spacing: 0; font-size: 13px; }

.verdict { font-size: 13px; margin: 12px 0 0; padding: 8px 10px; border-radius: 8px; background: var(--panel-2); }
.verdict.ok { color: var(--ok); }
.verdict.bad { color: var(--warn); }

.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; margin-top: 12px; }
.row.end { justify-content: flex-end; }

/* --------------------------------------------------------------- controls */

button {
    font: inherit;
    border-radius: 8px;
    padding: 8px 14px;
    cursor: pointer;
    border: 1px solid var(--line);
    background: var(--panel-2);
    color: var(--text);
}
button:hover:not(:disabled) { border-color: var(--accent-dim); }
button:disabled { opacity: 0.45; cursor: not-allowed; }

button.primary { background: var(--accent); border-color: var(--accent); color: #04120f; font-weight: 600; }
button.ghost { background: transparent; }
button.danger, button.ghost.danger { color: var(--danger); }

label { display: block; margin-bottom: 10px; font-size: 13px; color: var(--muted); }
label.inline { display: flex; align-items: center; gap: 8px; }
label.check { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: 14px; }
label.check input { margin: 0; }
label.locked-row { opacity: 0.85; }

input, select, textarea {
    font: inherit;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 7px;
    padding: 7px 9px;
    width: 100%;
    margin-top: 4px;
}
label.inline input, label.inline select { margin-top: 0; width: auto; flex: 1; min-width: 60px; }
input[type="checkbox"] { width: auto; accent-color: var(--accent); }
textarea { font-family: var(--mono); font-size: 13px; resize: vertical; }
select.inline-select { width: auto; margin: 0; }

fieldset { border: 1px solid var(--line); border-radius: 8px; padding: 12px; margin: 0 0 12px; }
legend { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; padding: 0 6px; }

.pill {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    background: var(--accent-dim);
    color: var(--text);
    padding: 2px 7px;
    border-radius: 999px;
}
.locked { color: var(--muted); font-size: 12px; font-style: italic; }
.pill.warn { background: color-mix(in srgb, var(--warn) 30%, transparent); }
.suffix { color: var(--muted); font-family: var(--mono); font-size: 13px; }

/* ---------------------------------------------------------------- tables */

table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { text-align: left; color: var(--muted); font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
tbody tr:last-child td { border-bottom: 0; }
td.port { font-family: var(--mono); }
table.toggles td:nth-child(3), table.toggles td:nth-child(4),
table.toggles th:nth-child(3), table.toggles th:nth-child(4) { text-align: center; width: 90px; }
table.ports td:first-child { font-family: var(--mono); width: 90px; }
table.proxies td:last-child { text-align: right; white-space: nowrap; }
.empty { color: var(--muted); font-style: italic; }

.tag {
    display: inline-block;
    font-size: 11px;
    padding: 2px 7px;
    border-radius: 999px;
    border: 1px solid var(--line);
    color: var(--muted);
    margin-right: 4px;
}
.tag.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.tag.off { color: var(--muted); }
.tag.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }

/* ------------------------------------------------------------------- logs */

.logview, .args, pre {
    font-family: var(--mono);
    font-size: 12.5px;
    line-height: 1.55;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 12px;
    margin: 0;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.logview { height: min(64vh, 620px); }
.args { max-height: 220px; }
.tail { margin-left: auto; font-size: 12px; }

.update-status {
    padding: 11px 13px;
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-radius: 8px;
    font-size: 14px;
    transition: box-shadow .3s ease, border-color .3s ease, background .3s ease, color .3s ease;
}
/* An update waiting is the one thing on this page worth interrupting for, so it
   is lit and it breathes. */
.update-status.available {
    color: var(--accent-glow);
    font-weight: 600;
    border-color: color-mix(in srgb, var(--accent) 55%, transparent);
    background: color-mix(in srgb, var(--accent) 11%, var(--panel-2));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent), 0 0 28px -6px var(--accent-glow);
    animation: update-pulse 2.4s ease-in-out infinite;
}
.update-status.current {
    color: var(--ok);
    border-color: color-mix(in srgb, var(--ok) 45%, transparent);
    background: color-mix(in srgb, var(--ok) 9%, var(--panel-2));
    box-shadow: 0 0 22px -10px var(--ok);
}
.update-status.error {
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 45%, transparent);
    background: color-mix(in srgb, var(--danger) 9%, var(--panel-2));
}
@keyframes update-pulse {
    0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent), 0 0 22px -8px var(--accent-glow); }
    50% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent), 0 0 36px -3px var(--accent-glow); }
}

/* ---------------------------------------------------------------- dialog */

dialog {
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 22px;
    width: min(560px, calc(100vw - 32px));
    max-height: 90vh;
}
dialog.wide { width: min(680px, calc(100vw - 32px)); }
dialog::backdrop { background: rgb(0 0 0 / 0.6); }
dialog h3 { margin-top: 0; }
.dialog-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.dialog-head h3 { margin-bottom: 12px; }
.dialog-close {
    background: none;
    border: 0;
    color: var(--muted);
    font-size: 22px;
    line-height: 1;
    padding: 0 6px;
    margin: -2px -4px 0 0;
    cursor: pointer;
    border-radius: 8px;
}
.dialog-close:hover { color: var(--text); background: var(--panel-2); }
dialog .row label { flex: 1; min-width: 110px; }

/* ----------------------------------------------------------------- toast */

.toast {
    position: fixed;
    left: 50%;
    bottom: 24px;
    transform: translateX(-50%);
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 10px 20px;
    z-index: 60;
    font-size: 14px;
    box-shadow: 0 10px 30px rgb(0 0 0 / 0.35);
}
.toast.bad { border-color: var(--danger); color: var(--danger); }
.toast.good { border-color: var(--accent-dim); }

/* ---------------------------------------------------------------- mining */

.grid.stats {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px;
    margin-bottom: 14px;
}
@media (max-width: 700px) {
    .grid.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.stat {
    background: var(--panel-2);
    border-radius: 8px;
    padding: 12px 14px;
    min-width: 0;
}
.stat span {
    display: block;
    font-size: 24px;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}
.stat small { color: var(--muted); font-size: 12px; }
/* The fiat line sits between the KAS figure and its caption, and holds its
   height so the row does not jump when there is no price to show. */
.stat .fiat { display: block; min-height: 14px; color: var(--accent); font-weight: 600; font-variant-numeric: tabular-nums; }

/* The projection's hashrate row: label, box, unit, the live toggle, the tag. */
.earn-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.earn-row > label[for="earn-hashrate"] { color: var(--muted); }
.earn-row input#earn-hashrate { width: auto; flex: 0 1 10em; }
.earn-row input#earn-hashrate.locked { opacity: 0.6; }
.switch-field { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.switch-field > span:last-child { color: var(--muted); font-size: 13px; }

/* Wide tables scroll inside their card rather than the page. */
.scroll-x { overflow-x: auto; }
/* Scoped to the genuinely wide tables. Applied to every table in a .scroll-x
   it also forced the ports table to 720px, which is why that one scrolled no
   matter how much its content was trimmed. */
.scroll-x table.workers,
.scroll-x table.blocks { min-width: 720px; }

table.workers td, table.blocks td { font-family: var(--mono); font-size: 12.5px; }
table.workers td.name, table.blocks td.name { font-family: inherit; font-size: 14px; }
.trunc { display: inline-block; max-width: 200px; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; white-space: nowrap; }

.status-online  { color: var(--ok); }
.status-idle    { color: var(--warn); }
.status-offline { color: var(--muted); }

table.instances input[type="number"] { width: 100%; margin: 0; }
table.instances td { padding: 6px 8px; }
table.instances td:last-child { width: 40px; text-align: right; }

/* ------------------------------------------------------------------ apps */

/* Sections that read the chain say so in a banner at the top of themselves.
   They used to be locked shut, padlock and all, which meant nobody could read
   what mining offered or set a difficulty until after an overnight sync. */

/* --------------------------------------------------------------- subtabs */

/* Panels within one sidebar destination (Kaspad: Overview / Settings / Log). */
.subnav {
    display: flex;
    gap: 2px;
    border-bottom: 1px solid var(--line);
    margin-bottom: 16px;
}
.subtab-btn {
    background: none;
    border: 0;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    color: var(--muted);
    padding: 9px 14px;
    cursor: pointer;
    font: inherit;
    font-size: 14px;
}
.subtab-btn:hover { color: var(--text); }
.subtab-btn.active { color: var(--text); border-bottom-color: var(--accent); }

.subtab { display: none; }
.subtab.active { display: block; }

/* The service rows are read down the Service column, so that one gets the
   weight and the rest stay out of its way. */
.publish-table td { vertical-align: middle; }
.publish-table .service-name { font-weight: 600; }
.publish-table .service-detail { color: var(--muted); font-size: 12px; display: block; margin-top: 2px; }
.publish-table .row-actions { text-align: right; white-space: nowrap; }

/* ------------------------------------------------------------ setup wizard */

.step-head { font-size: 15px; font-weight: 600; margin: 0 0 6px; }
.step-num {
    display: inline-block;
    width: 22px;
    height: 22px;
    line-height: 22px;
    text-align: center;
    border-radius: 999px;
    background: var(--panel-2);
    border: 1px solid var(--line);
    margin-right: 8px;
    font-size: 12px;
}
.steps { margin: 10px 0 14px; padding-left: 22px; color: var(--text); }
.steps li { margin: 5px 0; }
.plan { list-style: none; margin: 10px 0 14px; padding: 0; }
.plan li {
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    margin-bottom: 6px;
    background: var(--panel-2);
}
.plan li.done { opacity: 0.55; }
.plan li small { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; }

.domain-choices { margin: 10px 0 4px; }
.domain-choice {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    margin-bottom: 6px;
    cursor: pointer;
}
.domain-choice.taken { opacity: 0.5; cursor: default; }
/* Inputs are full width by default in this panel, which is right for a text box
   and wrong for a radio sitting beside a label. */
.domain-choice input[type="radio"] { width: auto; flex: 0 0 auto; margin: 0; accent-color: var(--accent); }
.domain-choice span { flex: 1; min-width: 0; }
.domain-choice small { display: block; color: var(--muted); font-size: 12px; }

.protect { margin: 4px 0 10px; }
.protect summary { cursor: pointer; color: var(--muted); font-size: 13px; padding: 4px 0; }
.protect summary:hover { color: var(--text); }

/* The maintenance row under the services table: quiet, and pushed right. */
.proxy-tools { align-items: center; gap: 8px; }
.proxy-tools .spacer { flex: 1; }

/* The one thing the panel cannot do for you, said before the table rather than
   after an hour of building. */
.notice {
    border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
    background: color-mix(in srgb, var(--warn) 7%, transparent);
    border-radius: 10px;
    padding: 12px 14px;
    margin: 0 0 14px;
}
/* A tab for something that is not installed: the real page, blurred, with the
   one action that is available on top of it. Showing the page at all is
   deliberate -- it is a preview of what installing gets you -- but reading its
   empty numbers as though they meant something should not be possible. */
.tab.not-installed { position: relative; }
.tab.not-installed > *:not(.install-gate) {
    filter: blur(5px);
    opacity: 0.5;
    pointer-events: none;
    user-select: none;
}
.install-gate {
    position: absolute;
    inset: 0;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding-top: 12vh;
    z-index: 3;
}
.install-gate-card {
    /* Sticky rather than parked at the top: these tabs are long, and an overlay
       whose only action scrolls out of sight leaves a blurred page and no way
       forward. */
    position: sticky;
    top: 12vh;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 26px 30px;
    text-align: center;
    max-width: 30rem;
    box-shadow: 0 18px 50px rgb(0 0 0 / 0.45);
}
.install-gate-card h3 { margin-top: 0; }
.install-gate-card p { margin-bottom: 18px; }

/* Anything that installs, starts, stops or removes a container takes the whole
   screen while it runs.

   The log used to print inside the tab's own overlay, which left the rest of
   the panel clickable: a build running, and every other switch still inviting
   somebody to queue a second job behind it on the same docker daemon. Covering
   everything is the honest version -- one thing is happening, this is it, and
   the way out is for it to finish. */
.action-overlay {
    position: fixed;
    inset: 0;
    z-index: 70;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: rgb(0 0 0 / 0.55);
    backdrop-filter: blur(7px);
}
/* display:flex would otherwise beat the hidden attribute. */
.action-overlay[hidden] { display: none; }

.action-card {
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: min(760px, 100%);
    max-height: 86vh;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 22px 24px;
    box-shadow: 0 24px 70px rgb(0 0 0 / 0.55);
}
.action-card p { margin: 0; }
.action-head { display: flex; align-items: center; gap: 10px; }
.action-head h3 { margin: 0; font-size: 17px; }
/* The log grows to fill the card rather than being a fixed height: a stop
   prints four lines and should not open a screen of empty black. */
.action-log {
    flex: 1 1 auto;
    height: auto;
    min-height: 8rem;
    max-height: 60vh;
    font-size: 12px;
}

.spinner {
    flex: 0 0 auto;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 2px solid color-mix(in srgb, var(--accent) 30%, transparent);
    border-top-color: var(--accent);
    animation: spin 0.8s linear infinite;
}
.spinner[hidden] { display: none; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 3s; }
}
button.big { font-size: 15px; padding: 11px 22px; }

/* Shown in place of the switch until a service exists at all. Kept narrow: it
   sits where the switch will, and every pixel it takes is one the name loses. */
.nav-install[hidden] { display: none; }
.nav-install {
    flex: 0 0 auto;
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
    color: var(--text);
    border-radius: 999px;
    font-size: 11px;
    line-height: 1.6;
    padding: 1px 7px;
    cursor: pointer;
    white-space: nowrap;
}
.nav-install:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); }
.uninstall-card .notice { border-color: color-mix(in srgb, var(--danger) 35%, transparent); background: color-mix(in srgb, var(--danger) 7%, transparent); }

.notice p { margin: 0 0 10px; font-size: 13px; }
.notice .row { align-items: center; gap: 10px; }
.notice .muted { font-size: 12px; }
/* Above the sub-nav, so it is read before the thing it applies to. */
.node-banner { margin-bottom: 16px; }
.node-banner p:last-child { margin-bottom: 0; }

.port-result { margin: 10px 0 0; font-size: 13px; }
.port-result li { margin: 3px 0; list-style: none; }
.port-result li::before { content: '· '; color: var(--muted); }
.port-result li.ok::before { content: '✓ '; color: var(--ok); }
.port-result li.bad::before { content: '✗ '; color: var(--danger); }

/* ----------------------------------------------------------- port toggles */

/* `display` on a class beats the hidden attribute's UA rule, so a switch told
   to hide stayed on screen and sat on top of the label beside it. Every element
   in this file that carries a display and can be hidden needs this. */
.switch[hidden] { display: none; }

.switch {
    position: relative;
    display: inline-block;
    width: 40px;
    height: 22px;
    flex: 0 0 40px;
    vertical-align: middle;
}
.switch input {
    position: absolute;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
    z-index: 1;
}
.switch .track {
    position: absolute;
    inset: 0;
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-radius: 999px;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.switch .track::before {
    content: "";
    position: absolute;
    left: 3px;
    top: 50%;
    transform: translateY(-50%);
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--muted);
    transition: transform 0.15s ease, background 0.15s ease;
}
.switch input:checked + .track {
    background: color-mix(in srgb, var(--accent) 30%, transparent);
    border-color: var(--accent-dim);
}
.switch input:checked + .track::before {
    transform: translate(18px, -50%);
    background: var(--accent);
}
.switch input:disabled { cursor: wait; }
.switch input:disabled + .track { opacity: 0.5; }
.switch input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 2px; }

table.ports td.toggle { width: 56px; }
table.ports td.note { color: var(--muted); font-size: 12.5px; font-family: inherit; }

/* ------------------------------------------------------------ sync detail */

.sync-detail {
    font-family: var(--mono);
    font-size: 12.5px;
    color: var(--muted);
    margin: -6px 0 12px;
    overflow-wrap: anywhere;
}

/* The UTXO set announces no total, so its slice of the bar is an estimate.
   Stripes say so without needing a caption. */
#sync-bar.estimated {
    background-image: repeating-linear-gradient(
        45deg,
        transparent 0 6px,
        color-mix(in srgb, var(--bg) 45%, transparent) 6px 12px
    );
}

.sync-steps {
    list-style: none;
    margin: 0 0 14px;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 4px 16px;
    font-size: 12.5px;
}
.sync-steps li { display: flex; align-items: center; gap: 6px; color: var(--muted); }
.sync-steps .mark { width: 10px; display: inline-block; text-align: center; }
.sync-steps li.done { color: var(--ok); }
.sync-steps li.current { color: var(--text); font-weight: 600; }
.sync-steps li.todo { opacity: 0.55; }

/* ------------------------------------------------------------- log grid */

.logs-toolbar {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 14px;
    flex-wrap: wrap;
}
.logs-toolbar input[type="text"], .logs-toolbar #log-filter { width: auto; margin: 0; }

.log-grid {
    display: grid;
    /* Two at most. auto-fit packed in as many as would fit, which on a wide
       screen meant four narrow columns of wrapped log lines, and log output is
       long horizontal text: fewer, wider tiles read far better than more. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
    align-items: start;
}
@media (max-width: 820px) {
    .log-grid { grid-template-columns: minmax(0, 1fr); }
}

.log-tile {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    /* Every tile the same height so the grid stays a grid as logs fill up. */
    height: 320px;
}
/* Expanded is a centred overlay, not a wider grid cell. The tile keeps its
   place in the grid and is lifted out of it visually, so the element carries on
   receiving its stream: no re-parenting, no reconnecting, nothing to lose. */
.log-tile.expanded {
    position: fixed;
    /* Percentages, not vw: vw counts the scrollbar and the containing block for
       a fixed element does not, so the two sides resolve against different
       widths and it lands off-centre. */
    inset: 6%;
    height: auto;
    z-index: 60;
    box-shadow: 0 24px 70px rgb(0 0 0 / 0.55);
}
/* The grid still owns a slot for it, which would otherwise collapse and shuffle
   every other tile around behind the overlay. */
.log-tile.expanded::after {
    content: "";
    display: block;
}

.log-scrim {
    position: fixed;
    inset: 0;
    z-index: 55;
    background: rgb(0 0 0 / 0.55);
    backdrop-filter: blur(2px);
}
/* .log-tile sets display:flex, which would otherwise beat the hidden attribute. */
.log-tile[hidden] { display: none; }

.log-tile-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 12px;
    border-bottom: 1px solid var(--line);
    font-size: 13px;
    font-weight: 600;
    background: var(--panel-2);
}
.log-tile-head .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.log-tile-head .count { color: var(--muted); font-weight: 400; font-family: var(--mono); font-size: 11px; }
.log-tile-head button {
    background: none;
    border: 0;
    color: var(--muted);
    padding: 2px 6px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 12px;
}
.log-tile-head button:hover { background: var(--bg); color: var(--text); }

.log-tile pre {
    flex: 1;
    margin: 0;
    border: 0;
    border-radius: 0;
    font-size: 11.5px;
    line-height: 1.45;
    overflow: auto;
    background: var(--bg);
}

.log-grid .empty-tile {
    grid-column: 1 / -1;
    color: var(--muted);
    font-style: italic;
    padding: 20px;
}

/* --------------------------------------------------- on/off toggle header */

/* A section's main on/off control: the switch is the primary action, so it
   leads, and there is no separate Apply to forget to press. */
.toggle-bar { margin-bottom: 16px; }
.toggle-row {
    display: flex;
    align-items: center;
    gap: 14px;
}
.toggle-row .toggle-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.toggle-row .toggle-text strong { font-size: 15px; }
.toggle-row .toggle-text .muted { font-size: 13px; }
.toggle-row .switch { width: 46px; height: 25px; flex-basis: 46px; }
.toggle-row .switch .track::before { width: 17px; height: 17px; }
.toggle-row .switch input:checked + .track::before { transform: translate(20px, -50%); }

/* ------------------------------------------------------------ miner setup */

.connect-block { margin-bottom: 18px; }
.connect-block:last-child { margin-bottom: 0; }
.connect-block h4 {
    margin: 0 0 4px;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text);
}
.connect-block > .muted { font-size: 13px; margin: 0 0 8px; }

/* The stratum URL is the thing people paste into a miner, so it gets the
   emphasis and a copy button rather than being one cell among many. */
.copyable {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}
.copyable code {
    font-size: 13.5px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 4px 8px;
}
.copy-btn {
    border: 1px solid var(--line);
    background: var(--panel-2);
    color: var(--muted);
    border-radius: 6px;
    padding: 4px 9px;
    font-size: 12px;
    cursor: pointer;
    white-space: nowrap;
}
.copy-btn:hover { color: var(--text); border-color: var(--accent-dim); }
.copy-btn.copied { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }

#scan-results td { font-size: 13px; }
#scan-results a { color: var(--accent); }

.hint.tight { margin-top: 4px; margin-bottom: 12px; }

/* The node's own card: switch and stats together at the top of Kaspad. */
#node-card .nodestats {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}
@media (max-width: 700px) {
    #node-card .nodestats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ------------------------------------------------- sidebar service switches */

/* The nav entry and its switch are siblings, not nested: a checkbox inside a
   button is invalid, and the button would swallow the clicks meant for it. */
.switch.mini {
    width: 30px;
    height: 17px;
    flex: 0 0 30px;
    /* Optical centring: the switch is shorter than the row's text line, so it
       needs no nudging vertically, only breathing room from the edge. */
    margin: 0;
}
.switch.mini .track::before { width: 11px; height: 11px; left: 2px; }
.switch.mini input:checked + .track::before { transform: translate(13px, -50%); }
.switch.mini input:disabled + .track { opacity: 0.3; }

/* No room for them on the icon rail, and nothing to label them with. */
.sidebar.collapsed .switch.mini { display: none; }
@media (max-width: 860px) {
    .sidebar.collapsed .switch.mini { display: inline-block; }
}

/* --------------------------------------------------- the node's single card */

.node-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 14px;
}
.node-head h3 { margin: 0; flex: 0 0 auto; }
.node-head .tag { margin-right: auto; }

#node-card .nodestats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px 16px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--line);
}

/* The pruning line carries a countdown and a sentence explaining it, so it
   takes the full width rather than being squeezed into half a column. */
#node-card .nodestats .wide { grid-column: 1 / -1; }
#node-card .nodestats .wide dd { font-weight: 600; }
#node-card .nodestats .wide dd .muted { font-weight: 400; }

/* Two columns: what the node is doing on the left, how it can be reached on
   the right. They are separate concerns that both belong to the same node, so
   one card with a divider beats two cards. */
.node-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    gap: 0 26px;
}
.node-col { min-width: 0; }
.node-ports {
    padding-left: 26px;
    border-left: 1px solid var(--line);
}

/* Each block of the left column is one idea: what it is, how far along, who it
   talks to. The rules keep them apart without needing separate cards. */
.node-section {
    padding-top: 16px;
    margin-top: 16px;
    border-top: 1px solid var(--line);
}
.node-section:first-of-type { border-top: 0; margin-top: 0; }
#node-card .nodestats + .node-section { border-top: 0; margin-top: 0; }

/* Half a card is not much for a six-column table. */
.node-ports h3 { margin-top: 0; }
.node-ports .ports { font-size: 13px; }
.node-ports .ports th, .node-ports .ports td { padding: 7px 6px; }
.node-ports .ports td:first-child { width: 58px; }
.node-ports .bindrow { margin-top: 12px; }
.node-ports .bindrow input { min-width: 90px; }

@media (max-width: 1100px) {
    .node-body { grid-template-columns: minmax(0, 1fr); }
    .node-ports {
        padding-left: 0;
        border-left: 0;
        margin-top: 16px;
        padding-top: 16px;
        border-top: 1px solid var(--line);
    }
}

.peers-line {
    display: flex;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.peers-count strong { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; }

@media (max-width: 520px) {
    #node-card .nodestats { grid-template-columns: minmax(0, 1fr); }
}

/* The ports table lives in half a card, so it earns its width back here. */
.node-ports .ports .tag { font-size: 10.5px; padding: 1px 5px; }
.node-ports .ports button { padding: 4px 9px; font-size: 12px; }
.node-ports .ports td.toggle { width: 46px; }

/* State moved from its own column to a dot beside the port number: the row's
   tooltip carries the wording, which half a card has no room for. */
/* The dot is laid out inline, not by making the cell a flex container:
   display:flex on a <td> takes it out of the table's row model, which is what
   broke the row separators into short offset segments. */
.node-ports .ports td.port { white-space: nowrap; }
.node-ports .ports td.port .dot {
    width: 7px;
    height: 7px;
    box-shadow: none;
    display: inline-block;
    vertical-align: middle;
    margin-right: 7px;
}
.node-ports .ports .dot.warn { background: var(--warn); }

/* ------------------------------------------------------------- log zoom */

/* The fallback, for a view whose size has not been set yet. Each view carries
   its own --log-size inline, so zooming one leaves the rest where they were. */
.logview,
.log-tile pre {
    font-size: var(--log-size, 11.5px);
}

.zoom {
    display: inline-flex;
    border: 1px solid var(--line);
    border-radius: 7px;
    overflow: hidden;
}
.zoom-btn {
    background: var(--panel-2);
    border: 0;
    border-radius: 0;
    color: var(--muted);
    padding: 3px 10px;
    font-size: 14px;
    line-height: 1.2;
    cursor: pointer;
}
.zoom-btn + .zoom-btn { border-left: 1px solid var(--line); }
.zoom-btn:hover:not(:disabled) { background: var(--bg); color: var(--text); }
.zoom-btn:disabled { opacity: 0.35; cursor: not-allowed; }

/* These bars lost their switch to the sidebar, so they are a title and status
   strip now. Without the switch leading, the text should start at the edge. */
.toggle-bar .toggle-row > .toggle-text:first-child { margin-left: 0; }

/* -------------------------------------------------- mining overview card */

/* Live spans the card; the two tables sit side by side beneath it. */
.mining-tables {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0 24px;
    margin-top: 18px;
    padding-top: 18px;
    border-top: 1px solid var(--line);
}
.mining-col { min-width: 0; }
.mining-col + .mining-col { padding-left: 24px; border-left: 1px solid var(--line); }
.mining-col h3 { margin-top: 0; }

/* Both tables fit their columns now, so neither forces a scrollbar. Workers
   gets the wider share: six columns of numbers against four. */
.scroll-x table.blocks { min-width: 0; }
.scroll-x table.workers { min-width: 0; }
.mining-tables { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }

table.workers td, table.blocks td { padding: 7px 6px; font-size: 12px; }
table.workers th, table.blocks th { padding: 7px 6px; font-size: 10.5px; }
table.workers td.name { display: table-cell; white-space: nowrap; }
table.workers td.name .dot {
    width: 7px;
    height: 7px;
    box-shadow: none;
    display: inline-block;
    vertical-align: middle;
    margin-right: 6px;
}
/* Stale and invalid ride under the share count rather than taking columns. */
table.workers .sub {
    display: block;
    color: var(--muted);
    font-size: 10px;
    line-height: 1.3;
}

@media (max-width: 1100px) {
    .mining-tables { grid-template-columns: minmax(0, 1fr); }
    .mining-col + .mining-col {
        padding-left: 0;
        border-left: 0;
        margin-top: 18px;
        padding-top: 18px;
        border-top: 1px solid var(--line);
    }
}

/* ---------------------------------------------------------------- kachat */

/* A label with its control stacked underneath, for the ones whose wording is
   too long to sit on a single line next to the input. */
label.stack {
    display: block;
    margin: 12px 0 0;
    font-size: 12.5px;
    color: var(--muted);
}
label.stack > .row,
label.stack > textarea {
    margin-top: 6px;
}
label.stack > textarea {
    display: block;
    width: 100%;
    resize: vertical;
    font-family: var(--mono);
    font-size: 12.5px;
    background: var(--panel-2);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 9px 11px;
}

/* Deleting indexed data is not undoable from here, so the card that does it
   is marked as what it is rather than looking like every other card. */
.danger-zone {
    border-color: color-mix(in srgb, var(--danger) 40%, var(--line));
}
.danger-zone h3 {
    color: var(--danger);
}

/* Stat tiles carrying text rather than a number: metric names and byte counts
   do not fit at the 24px the numeric tiles use. */
.stat span.small {
    font-size: 15px;
    line-height: 1.35;
    word-break: break-word;
}

/* The status dot in front of a service name. */
.svc-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 6px;
    background: var(--muted);
}
.svc-dot.ok { background: var(--ok); }
.svc-dot.warn { background: var(--warn); }
.svc-dot.bad { background: var(--danger); }

/* A disclosure for detail that would otherwise dominate a panel: the numbers
   worth a glance stay as tiles, the long tail folds away behind this. */
details.more {
    margin-top: 14px;
    border-top: 1px solid var(--line);
    padding-top: 12px;
}
details.more > summary {
    cursor: pointer;
    color: var(--muted);
    font-size: 13px;
    list-style: none;
}
details.more > summary::-webkit-details-marker { display: none; }
details.more > summary::before {
    content: "▸";
    display: inline-block;
    width: 14px;
    transition: transform 0.15s ease;
}
details.more[open] > summary::before { transform: rotate(90deg); }
details.more > summary:hover { color: var(--text); }

/* Many small readings, so they pack tighter than the standard kv list. */
.kv.dense {
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 6px 16px;
    margin-top: 12px;
    font-size: 12.5px;
}
.kv.dense dt { font-size: 11px; }
.kv.dense dd { font-size: 12.5px; }

/* Eight readings that all fit on one row on a normal desktop, so the whole
   Chats panel lands on a screen without scrolling. Scoped here: the mining and
   node cards want the wider four-up tiles. */
#sub-kachat-chats .grid.stats {
    grid-template-columns: repeat(auto-fit, minmax(146px, 1fr));
    gap: 10px;
    margin-bottom: 0;
}
#sub-kachat-chats .stat { padding: 9px 11px; }
#sub-kachat-chats .stat span { font-size: 19px; }

/* The last few pixels: this panel is two sections in one card, and the default
   spacing between them is tuned for the node card's much longer sections. */
#sub-kachat-chats .node-section { padding-top: 12px; margin-top: 12px; }
#sub-kachat-chats .node-section p.muted { margin-bottom: 10px; }
#sub-kachat-chats label.stack { margin-top: 8px; }
#sub-kachat-chats .row { margin-top: 10px; }
#sub-kachat-chats p.hint { margin-bottom: 0; }
#sub-kachat-chats details.more { margin-top: 10px; padding-top: 10px; }
#sub-kachat-chats article.card { padding: 12px 18px; }

/* The tile header is crowded, so its zoom control is smaller than the one on a
   full-width log and loses its border. */
.log-tile-head .zoom { border: 0; }
.log-tile-head .zoom .zoom-btn { padding: 2px 5px; font-size: 12px; }

/* Group chats carries the same shape as Chats: a setting above its numbers in
   one card, so it gets the same tightening. */
#sub-kachat-groups article.card { padding: 12px 18px; }
#sub-kachat-groups .node-section { padding-top: 12px; margin-top: 12px; }
#sub-kachat-groups .node-section p.muted { margin-bottom: 10px; }
#sub-kachat-groups label.stack { margin-top: 8px; }
#sub-kachat-groups .row { margin-top: 10px; }
#sub-kachat-groups p.hint { margin-bottom: 0; }
#sub-kachat-groups .grid.stats {
    grid-template-columns: repeat(auto-fit, minmax(146px, 1fr));
    gap: 10px;
    margin-bottom: 0;
}

/* A column of cards inside a grid cell. Grid rows are a uniform height, so two
   cards stacked as separate grid items leave a gap when the card beside them is
   taller. Nesting them in one item keeps them together. */
.grid > .col {
    display: grid;
    gap: 16px;
    align-content: start;
    min-width: 0;
}

/* ------------------------------------------------------------------ mark */

/* The Kaspa mark, which also reports node health. It replaced a coloured dot,
   and that dot was the only place the panel said at a glance whether the node
   was up, so the state moved onto the ring around the mark rather than being
   dropped. */
/* An <img>, not inline SVG: the sidebar strokes every svg it contains for the
   line icons, which would repaint a logo. An image is untouched by that, and it
   keeps the artwork in one file that can be swapped for the official one. */
.mark {
    display: block;
    width: 22px;
    height: 22px;
    flex: 0 0 22px;
}
/* No border-radius: the artwork is already round, and an irregular edge is the
   point of it. Clipping to a circle would shave exactly what makes it the
   Kaspa mark rather than a dot. */


/* ----------------------------------------------------------- nav health */

/* One dot per destination: green running, amber running but not right yet, red
   switched on and not there. A service that is deliberately off gets a hollow
   dot, because "off" is not a fault and should not look like one. */
.nav-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    flex: 0 0 7px;
    margin-right: 1px;
    background: transparent;
    border: 1px solid var(--line);
    transition: background 0.2s ease, border-color 0.2s ease;
}
.nav-dot.ok { background: var(--ok); border-color: var(--ok); }
.nav-dot.warn { background: var(--warn); border-color: var(--warn); }
.nav-dot.bad { background: var(--danger); border-color: var(--danger); }
/* Sections that are not a service have nothing to report. */
.nav-dot.none { visibility: hidden; }

.sidebar.collapsed .nav-dot { display: none; }

/* ------------------------------------------------------------- support */

ul.links, ul.plain { list-style: none; margin: 10px 0 0; padding: 0; }
ul.links li {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 7px 0;
    border-bottom: 1px solid var(--line);
}
ul.links li:last-child { border-bottom: 0; }
ul.links .what { color: var(--muted); font-size: 12.5px; min-width: 74px; }
ul.links .handle { font-family: var(--mono); }
ul.plain li { margin: 8px 0; color: var(--muted); font-size: 13.5px; }

/* A link that rides along with a heading: same baseline, deliberately quieter,
   so it reads as "and here is where that lives" rather than as a second title. */
.title-link {
    margin-left: 8px;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    color: var(--accent);
    white-space: nowrap;
}

/* Two short cards stacked in one grid cell, matching the outer gap so the
   seam is invisible. */
.col-stack {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
}

.donate-body {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    margin-top: 10px;
}
/* Always white behind the code, whatever the theme is doing: scanners expect
   dark-on-light and many will not read an inverted one. The padding is the
   quiet zone the SVG already carries, given a surface to sit on. */
.donate-qr {
    flex: 0 0 auto;
    width: 132px;
    height: 132px;
    background: #fff;
    border-radius: 8px;
}
.donate-qr svg { display: block; width: 100%; height: 100%; border-radius: 8px; }
.donate-side {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.donate-side .copy-btn { align-self: flex-start; }

@media (max-width: 620px) {
    .donate-body { flex-direction: column; }
}

/* An address is worth nothing if it is transcribed wrong, so it is shown whole
   rather than truncated, and one click selects all of it for the case where the
   clipboard is unreachable (plain http on a LAN address). */
.address-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin: 0;
}
.address-row .addr {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
    user-select: all;
    font-family: var(--mono);
    font-size: 13px;
    color: var(--text);
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 9px 11px;
}

/* --------------------------------------------------------------- kassigner */

/* The enclosing .grid sets align-items: start, which lets every card shrink to
   its own text and leaves the choices looking like a ragged list. Here they are
   a set to compare, so they get one height: stretch across the row, and
   grid-auto-rows to carry that same height between rows. */
#kassigner-boards {
    align-items: stretch;
    grid-auto-rows: 1fr;
}

/* Board cards sit inside a card, so they need their own surface to read as
   choices rather than as more of the same panel. */
.board-card {
    background: var(--panel-2);
    display: flex;
    flex-direction: column;
}

/* The placeholder is deliberately flatter than the real choices: same footprint
   so the grid stays even, but nothing that invites a click. */
.board-card.soon {
    background: transparent;
    border-style: dashed;
    justify-content: center;
}
.board-card.soon h3,
.board-card.soon p { opacity: 0.62; }
.board-card h3 { margin-top: 0; }
.board-card .row { margin-top: auto; }

/* The buy link sits under the choice, not beside it: it is for the person who
   has not got a board yet, and should not compete with picking one. */
.board-card .buy { margin: 10px 0 0; }

/* One card per broadcast channel: the name and its switch on top, the stored
   count underneath. Same tile as the stats grids elsewhere, so a channel reads
   as a number you can also turn off. */
#kachat-channels { grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); }

.channel-card {
    background: var(--panel-2);
    border-radius: 8px;
    padding: 12px 14px;
    min-width: 0;
}
.channel-card .head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
.channel-card .name {
    flex: 1;
    min-width: 0;
    font-size: 11px;
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
    overflow-wrap: anywhere;
}
.channel-card .count {
    font-size: 22px;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}
/* Off reads as off at a glance, not just by the switch position. */
.channel-card.off .count { color: var(--muted); }

/* Stored but no longer tracked. The rows are real, so hiding it would make the
   total look wrong; the marker says why it is there with its switch off. */
.channel-card.untracked { border: 1px dashed var(--line); }

.channel-card .drop {
    background: none;
    border: 0;
    color: var(--muted);
    cursor: pointer;
    padding: 0 2px;
    font-size: 12px;
}
.channel-card .drop:hover { color: var(--danger); }

/* The totals tile sits in the same grid, with no switch of its own. */
.channel-card.total .count { color: var(--text); }

/* The proxy page while it is off: one card and nothing else, so it is narrowed
   and centred rather than stretched across a screen it has no content for. */
#tab-proxy.alone .proxy-intro {
    max-width: 640px;
    margin: 0 auto;
}

/* ==========================================================================
   Kaspa Silver finish

   Everything above is structure. This is the surface treatment that makes the
   panel look like the channel rather than a default dark theme: one light
   source, mint on near-black, and technical values set in mono so numbers read
   as readings.

   It sits at the end of the file deliberately, so it can lift the components
   above it without any of them needing to know.
   ========================================================================== */

/* The banner is not a flat black. It has a glow off to one side and falls away
   to near-black in the far corner, which is what stops it looking like a slab. */
body {
    background:
        radial-gradient(1100px 620px at 12% -8%, rgba(79, 227, 206, 0.10), transparent 62%),
        radial-gradient(900px 500px at 100% 104%, rgba(18, 62, 65, 0.30), transparent 68%),
        var(--bg);
    background-attachment: fixed;
}

@media (prefers-color-scheme: light) {
    body { background: var(--bg); }
}

/* --- shell ---------------------------------------------------------------- */

/* The sidebar reads as the console frame, so it sits deeper than the content
   it is framing rather than level with it. */
.sidebar {
    background: linear-gradient(180deg, rgba(16, 32, 37, 0.55), rgba(5, 11, 13, 0.85));
    border-right-color: var(--line);
}

.brand { letter-spacing: 0.01em; }

/* The active row gets the light. A bar down the leading edge does more for
   "you are here" than a background wash, and it echoes the rules that bracket
   the banner wordmark. */
.nav-row.active {
    position: relative;
    background: linear-gradient(90deg, rgba(125, 216, 202, 0.13), transparent 78%);
    /* Clears the flat inset bar from the base rule, which would otherwise sit
       full height behind the lit one below and read as two bars. */
    box-shadow: none;
}
.nav-row.active::before {
    content: "";
    position: absolute;
    inset: 4px auto 4px 0;
    width: 2px;
    border-radius: 0 2px 2px 0;
    background: var(--accent);
    box-shadow: 0 0 10px -1px var(--accent-glow);
}
.nav-row.active .nav-item svg { filter: drop-shadow(0 0 6px rgba(125, 216, 202, 0.55)); }

/* --- surfaces ------------------------------------------------------------- */

/* A one pixel highlight along the top edge. It is the cheapest way to suggest a
   light source above the page, and it keeps cards from reading as flat blocks. */
.card {
    position: relative;
    background:
        linear-gradient(180deg, rgba(125, 216, 202, 0.035), transparent 120px),
        var(--panel);
    box-shadow: 0 1px 0 rgba(125, 216, 202, 0.06) inset, 0 2px 14px -10px rgba(0, 0, 0, 0.9);
}

@media (prefers-color-scheme: light) {
    .card { background: var(--panel); box-shadow: 0 1px 2px rgba(12, 26, 28, 0.06); }
    .sidebar { background: var(--panel); }
    .nav-row.active { background: rgba(15, 125, 113, 0.09); }
}

/* Card titles are labels on an instrument, so they get the mono treatment the
   banner uses for its small type. */
.card h3 {
    font-family: var(--mono);
    font-size: 12.5px;
    letter-spacing: 0.1em;
    color: var(--accent);
    opacity: 0.85;
}

.danger-zone h3 { color: var(--danger); opacity: 1; }

/* --- readings ------------------------------------------------------------- */

/* Keys are quiet and wide-tracked; values are mono and tabular so digits line
   up between refreshes instead of jittering. */
.kv dt {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.09em;
}
.kv dd {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
}

.bigstat span {
    font-family: var(--mono);
    letter-spacing: -0.01em;
    text-shadow: 0 0 22px rgba(125, 216, 202, 0.35);
}

/* The sync bar is the one thing on the page that is genuinely alive, so it is
   the one thing that gets to glow properly. */
.bar { background: rgba(26, 50, 56, 0.7); }
.bar div {
    background: linear-gradient(90deg, var(--accent-dim), var(--accent-glow));
    box-shadow: 0 0 12px -2px var(--accent-glow);
}

/* --- controls ------------------------------------------------------------- */

button.primary {
    background: linear-gradient(180deg, var(--accent), #5ec3b4);
    border-color: var(--accent);
    color: #03130f;
    box-shadow: var(--glow-soft);
}
button.primary:hover:not(:disabled) {
    background: linear-gradient(180deg, var(--accent-glow), var(--accent));
    box-shadow: 0 0 22px -6px var(--accent-glow);
}

button:hover:not(:disabled) { border-color: var(--accent-dim); }

/* One visible focus treatment for everything that takes focus. Keyboard users
   should never have to hunt for the cursor on a page this dense. */
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible,
summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 6px;
}

input:focus,
select:focus,
textarea:focus {
    border-color: var(--accent-dim);
    box-shadow: var(--glow-hard);
    outline: none;
}

/* Values people type are almost all technical here: addresses, ports, hashes,
   transaction ids. Mono makes them checkable at a glance. */
input,
select { font-family: var(--mono); font-size: 13.5px; }

/* --- subtabs -------------------------------------------------------------- */

.subtab-btn {
    font-family: var(--mono);
    font-size: 12.5px;
    letter-spacing: 0.04em;
}
.subtab-btn.active {
    border-bottom-color: var(--accent);
    box-shadow: 0 1px 0 var(--accent), 0 6px 14px -10px var(--accent-glow);
}

/* --- status --------------------------------------------------------------- */

.tag {
    font-family: var(--mono);
    font-size: 11.5px;
    letter-spacing: 0.05em;
}

/* The health dots carry real meaning at a glance, so they are lit rather than
   painted. The colours are already far apart in hue; the glow makes them
   readable at the edge of vision too. */
.dot.ok { box-shadow: 0 0 0 3px rgba(70, 206, 134, 0.18), 0 0 10px -1px var(--ok); }
.dot.warn { box-shadow: 0 0 0 3px rgba(227, 169, 60, 0.18), 0 0 10px -1px var(--warn); }
.dot.bad { box-shadow: 0 0 0 3px rgba(255, 107, 98, 0.18), 0 0 10px -1px var(--danger); }

/* --- scrollbars ----------------------------------------------------------- */

* {
    scrollbar-width: thin;
    scrollbar-color: var(--line) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: var(--line);
    border-radius: 999px;
    border: 2px solid transparent;
    background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); background-clip: content-box; }

/* Motion here is decoration, not information. */
@media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
}

/* The language picker on the indexer's Translation tab. Two or three columns
   rather than a list: two dozen checkboxes in one column is a page of scrolling
   to compare a set somebody is choosing as a whole. */
.lang-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: 2px 14px;
    margin: 12px 0;
}
.lang-grid .check { margin: 0; }

/* The bottom of the action overlay: a bar while it runs, a way out when it is
   done. The two never show at once -- the absence of the button is what says
   there is no way out yet, which is the same thing the disabled "Please wait…"
   used to say with more words and less information. */
.action-foot {
    display: flex;
    align-items: center;
    gap: 12px;
}
.action-bar {
    flex: 1 1 auto;
    height: 8px;
    border-radius: 999px;
    background: var(--panel-2);
    border: 1px solid var(--line);
    overflow: hidden;
}
.action-bar[hidden] { display: none; }
.action-bar > span {
    display: block;
    height: 100%;
    width: 0;
    border-radius: inherit;
    background: var(--accent);
    /* Eased in CSS rather than by the animation loop: the loop sets a target a
       few times a second and the browser draws the movement between. */
    transition: width 420ms ease-out;
}
/* A build can spend minutes inside one step, and a bar that has not moved for
   two minutes reads as a hung panel. The stripes say work is happening without
   claiming any progress the log has not reported. */
.action-bar.working > span {
    background-image: linear-gradient(
        115deg,
        color-mix(in srgb, var(--accent) 55%, transparent) 25%,
        var(--accent) 25%,
        var(--accent) 50%,
        color-mix(in srgb, var(--accent) 55%, transparent) 50%,
        color-mix(in srgb, var(--accent) 55%, transparent) 75%,
        var(--accent) 75%
    );
    background-size: 22px 22px;
    animation: bar-stripes 0.9s linear infinite;
}
.action-bar.failed > span { background: var(--warn); background-image: none; animation: none; }
@keyframes bar-stripes { to { background-position: 22px 0; } }
@media (prefers-reduced-motion: reduce) {
    .action-bar.working > span { animation: none; }
}
.action-foot .mono { font-family: var(--mono); font-size: 12px; min-width: 4.5rem; text-align: right; }

/* The message preview on the bot's setup tab. Short, so it sizes to what is in
   it rather than opening a screen of black under a two-line message. */
.logview.preview {
    height: auto;
    max-height: 12rem;
    min-height: 2.4rem;
    font-size: 12.5px;
    padding: 8px 10px;
}

/* ============================================================ glow & pop === */
/* Turned up so state reads at a glance: a running service, a toggle that is on,
   an update waiting -- each lights when it means something. Everything stays on
   the accent ring the banner already uses, and every animation here sits inside
   the prefers-reduced-motion switch above. */

/* Sidebar health dots, lit like the action-overlay dots, so the app list glows
   with whatever is actually running. */
.nav-dot.ok { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 16%, transparent), 0 0 10px -1px var(--ok); }
.nav-dot.warn { box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 16%, transparent), 0 0 10px -1px var(--warn); }
.nav-dot.bad { box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 16%, transparent), 0 0 10px -1px var(--danger); }

/* A toggle that is on is a light that is on. */
.switch input:checked + .track { box-shadow: 0 0 15px -3px var(--accent-glow); }

/* Cards lift toward the light on hover: a small, consistent sign a whole panel
   is interactive. */
.card { transition: box-shadow .28s ease; }
.card:hover {
    box-shadow: 0 1px 0 rgba(125, 216, 202, 0.1) inset, var(--glow-soft), 0 12px 32px -20px rgba(0, 0, 0, 0.9);
}

/* Primary buttons carry a touch more ring; the update button is the loudest
   thing on its page. */
button.primary { transition: box-shadow .2s ease, background .2s ease, transform .08s ease; }
button.primary:active:not(:disabled) { transform: translateY(1px); }
#global-update-btn { box-shadow: 0 0 24px -7px var(--accent-glow); }
#global-update-btn:hover:not(:disabled) { box-shadow: 0 0 32px -3px var(--accent-glow); }

/* State tags get a matching halo so "on"/"key saved" reads lit, not just tinted. */
.tag.ok { box-shadow: 0 0 16px -8px var(--ok); }
.tag.warn { box-shadow: 0 0 16px -8px var(--warn); }

/* Light mode keeps the pop but drops the neon: its shadows are soft, not lit. */
@media (prefers-color-scheme: light) {
    .update-status.available { animation: none; box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent); }
    .nav-dot.ok, .nav-dot.warn, .nav-dot.bad,
    .switch input:checked + .track,
    #global-update-btn, #global-update-btn:hover:not(:disabled),
    .tag.ok, .tag.warn { box-shadow: none; }
}
