/* ============================================================================
   ada — design system
   Hand-written, no framework. Tokens first, then base, then components.
   Includes the base styles used by every page; no vendor CSS is required.
   ========================================================================= */

/* ---------------------------------------------------------------- tokens -- */

:root {
    /* type */
    --font-sans: "Poppins", ui-sans-serif, system-ui, "Segoe UI", "Microsoft YaHei", sans-serif;
    --font-mono: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;

    --text-xs: 0.75rem;
    --text-sm: 0.8125rem;
    --text-base: 0.875rem;
    --text-md: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.375rem;
    --text-2xl: 1.75rem;

    /* space — 4px base */
    --sp-1: 0.25rem;
    --sp-2: 0.5rem;
    --sp-3: 0.75rem;
    --sp-4: 1rem;
    --sp-5: 1.5rem;
    --sp-6: 2rem;
    --sp-7: 3rem;

    /* shape */
    --radius-sm: 4px;
    --radius: 8px;
    --radius-lg: 12px;
    --radius-full: 999px;

    /* motion */
    --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
    --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
    --dur-fast: 120ms;
    --dur: 200ms;
    --dur-slow: 360ms;

    /* semantic colour — profit/loss follows the CN convention (red = up).
       Referenced by the profit views; safe to flip in one place if wrong. */
    --gain: #e5484d;
    --loss: #30a46c;
}

/* light (default) */
:root {
    color-scheme: light;
    --bg: #f5f6f8;
    --surface: #ffffff;
    --surface-2: #f0f2f5;
    --surface-3: #e8ebef;
    --border: #e3e6ea;
    --border-strong: #cdd3db;

    /* the box a log is read in. Its own token because it has to sit *inside* a
       card and still read as a box: on light that means a shade down from the
       card, on dark a shade further down than the page. */
    --log-surface: #f0f2f5;

    --text: #14181f;
    --text-2: #58616f;
    --text-3: #8a94a2;

    --accent: #4f46e5;
    --accent-hover: #4338ca;
    --accent-text: #ffffff;
    --accent-soft: #eef1fe;
    --accent-ring: rgb(79 70 229 / 0.35);

    --success: #16a34a;
    --success-soft: #e8f6ed;
    --warn: #b45309;
    --warn-soft: #fdf3e3;
    --danger: #dc2626;
    --danger-soft: #fdecec;

    --shadow-sm: 0 1px 2px rgb(16 24 40 / 0.05);
    --shadow: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
    --shadow-lg: 0 12px 28px -8px rgb(16 24 40 / 0.16);
}

/* dark — matters here: operators watch this during night sessions */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;
        --bg: #0d1117;
        --surface: #161b22;
        --surface-2: #1c222c;
        --surface-3: #242c38;
        --border: #262d38;
        --border-strong: #39424f;

        --log-surface: #06090d;

        --text: #e6edf3;
        --text-2: #9aa5b1;
        --text-3: #6b7684;

        --accent: #7c74f0;
        --accent-hover: #918bf5;
        --accent-text: #0d1117;
        --accent-soft: #1d1b35;
        --accent-ring: rgb(124 116 240 / 0.4);

        --success: #3fb950;
        --success-soft: #14241a;
        --warn: #d29922;
        --warn-soft: #251d10;
        --danger: #f85149;
        --danger-soft: #2a1517;

        /* profit pages follow the CN convention (red = up); the light-theme
           values sit heavily on the dark ground, so they are brightened
           here rather than inherited. */
        --gain: #ff7b72;
        --loss: #3dd68c;

        --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3);
        --shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 1px 2px rgb(0 0 0 / 0.3);
        --shadow-lg: 0 12px 28px -8px rgb(0 0 0 / 0.6);
    }
}

/* explicit toggle wins over the OS preference in both directions */
:root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #0d1117;
    --surface: #161b22;
    --surface-2: #1c222c;
    --surface-3: #242c38;
    --border: #262d38;
    --border-strong: #39424f;

    --log-surface: #06090d;

    --text: #e6edf3;
    --text-2: #9aa5b1;
    --text-3: #6b7684;

    --accent: #7c74f0;
    --accent-hover: #918bf5;
    --accent-text: #0d1117;
    --accent-soft: #1d1b35;
    --accent-ring: rgb(124 116 240 / 0.4);

    --success: #3fb950;
    --success-soft: #14241a;
    --warn: #d29922;
    --warn-soft: #251d10;
    --danger: #f85149;
    --danger-soft: #2a1517;

    /* profit pages follow the CN convention (red = up); the light-theme
       values sit heavily on the dark ground, so they are brightened
       here rather than inherited. */
    --gain: #ff7b72;
    --loss: #3dd68c;

    --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3);
    --shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 1px 2px rgb(0 0 0 / 0.3);
    --shadow-lg: 0 12px 28px -8px rgb(0 0 0 / 0.6);
}

/* ------------------------------------------------------------------ base -- */

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

html {
    -webkit-text-size-adjust: 100%;
}

/* Author display rules must never reveal an inactive panel or control. */
[hidden] {
    display: none !important;
}

/* The base rules used by our components, previously supplied by theme.css. */
:where(.app) :is(p, ul, ol, dl, pre) {
    margin-top: 0;
    margin-bottom: var(--sp-4);
}

:where(.app) dd {
    margin-left: 0;
    margin-bottom: var(--sp-2);
}

:where(.app) :is(button, input, select, optgroup, textarea) {
    margin: 0;
    font: inherit;
}

:where(.app) textarea {
    overflow: auto;
    resize: vertical;
}

:where(.app) ::file-selector-button {
    font: inherit;
}

:where(.app) label {
    display: inline-block;
    margin: 0;
}

:where(.app) :is(pre, code, kbd, samp) {
    font-family: var(--font-mono);
}

:where(.app) pre {
    overflow: auto;
}

:where(.app) summary {
    cursor: pointer;
}

body.app {
    margin: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
    transition:
        background-color var(--dur) var(--ease),
        color var(--dur) var(--ease);
}

:where(.app) :is(h1, h2, h3, h4, h5, h6) {
    margin: 0;
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: -0.01em;
    color: var(--text);
}

:where(.app) h1 {
    font-size: var(--text-2xl);
}
:where(.app) h2 {
    font-size: var(--text-xl);
}
:where(.app) h3 {
    font-size: var(--text-lg);
}

:where(.app) a {
    color: var(--accent);
    text-decoration: none;
    transition: color var(--dur-fast) var(--ease);
}

:where(.app) a:where(:hover) {
    color: var(--accent-hover);
}

.app :is(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* numeric data reads better tabular and monospaced */
.num {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

/* `.stamp` is deliberately absent from this file. It is a JS hook, not a
   style: the master serves every instant as plain unix milliseconds with no
   zone, and fleet.html fills these spans in the viewer's own locale after each
   htmx swap. Nothing here should style it — give it a class of its own if it
   needs to look like something. */

/* things that are literally typed somewhere else and must be read character by
   character: a shell command, a cron expression. Distinct from .num, which is
   about aligning digits, and from .input-code, which is for editable fields */
.mono {
    font-family: var(--font-mono);
    overflow-wrap: anywhere;
}

/* raw config files read better in monospace (TOML editor) */
.input-code {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}

.muted {
    color: var(--text-2);
}
.dim {
    color: var(--text-3);
}

.icon {
    width: 1em;
    height: 1em;
    flex: none;
    vertical-align: -0.125em;
}
.icon-lg {
    width: 1.25em;
    height: 1.25em;
}

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

.app-header {
    position: sticky;
    top: 0;
    z-index: 50;
    background: color-mix(in srgb, var(--surface) 82%, transparent);
    backdrop-filter: blur(12px) saturate(1.6);
    border-bottom: 1px solid var(--border);
}

/* backdrop-filter needs a solid fallback where unsupported */
@supports not (backdrop-filter: blur(1px)) {
    .app-header {
        background: var(--surface);
    }
}

.app-nav {
    max-width: 1280px;
    margin-inline: auto;
    padding: var(--sp-3) var(--sp-5);
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

.brand {
    font-size: var(--text-lg);
    font-weight: 600;
    letter-spacing: 0.08em;
    color: var(--text);
    margin-right: var(--sp-4);
}

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

.nav-links {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    margin: 0;
    padding: 0;
    list-style: none;
}

.nav-link {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius);
    color: var(--text-2);
    font-weight: 500;
    white-space: nowrap;
    transition:
        background-color var(--dur-fast) var(--ease),
        color var(--dur-fast) var(--ease);
}

.nav-link:hover {
    background: var(--surface-2);
    color: var(--text);
}

.nav-link[aria-current="page"] {
    background: var(--accent-soft);
    color: var(--accent);
}

.nav-spacer {
    flex: 1;
}

.app-main {
    flex: 1;
    width: 100%;
    max-width: 1280px;
    margin-inline: auto;
    padding: var(--sp-6) var(--sp-5);
}

.app-footer {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-1);
    border-top: 1px solid var(--border);
    padding: var(--sp-5);
    text-align: center;
    color: var(--text-3);
    font-size: var(--text-xs);
    /* Quiet but crisp: a whisper of tracking stops the small latin from
       smearing, and tabular figures keep "2023 - 2026" and the ICP number
       on a steady grid. */
    letter-spacing: 0.02em;
    font-variant-numeric: tabular-nums;
}

/* Standalone sign-in, signup and server-error pages share the same tokens. */
.auth-main {
    flex: 1;
    display: grid;
    place-items: center;
    padding: var(--sp-6) var(--sp-4);
}

.auth-card {
    width: 100%;
    max-width: 26rem;
    padding: var(--sp-6);
}

.auth-title {
    text-align: center;
    margin-bottom: var(--sp-5);
}

.auth-card .input {
    font-size: var(--text-md);
}

.auth-submit {
    width: 100%;
}

.auth-links {
    margin-top: var(--sp-4);
    text-align: center;
    font-size: var(--text-sm);
}

.auth-links .btn {
    white-space: normal;
}

.auth-message {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
    margin-top: var(--sp-4);
}

.auth-message-text {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
}

.auth-error {
    text-align: center;
}

.auth-error-icon {
    width: var(--sp-7);
    height: var(--sp-7);
    color: var(--danger);
    margin-bottom: var(--sp-4);
}

.error-card {
    max-width: 48rem;
    margin-inline: auto;
}

.error-card .card-head {
    background: var(--danger-soft);
    color: var(--danger);
}

.user-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-2);
}

.page-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--sp-4);
    flex-wrap: wrap;
    margin-bottom: var(--sp-5);
}

.page-head p {
    margin: var(--sp-1) 0 0;
    color: var(--text-2);
    font-size: var(--text-sm);
}

/* --------------------------------------------------------------- buttons -- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--text);
    font: inherit;
    font-weight: 500;
    line-height: 1.4;
    cursor: pointer;
    white-space: nowrap;
    transition:
        background-color var(--dur-fast) var(--ease),
        border-color var(--dur-fast) var(--ease),
        color var(--dur-fast) var(--ease),
        transform var(--dur-fast) var(--ease),
        box-shadow var(--dur-fast) var(--ease);
}

.btn:hover {
    background: var(--surface-3);
}

.btn:active {
    transform: translateY(1px);
}

.btn-primary {
    background: var(--accent);
    color: var(--accent-text);
    box-shadow: var(--shadow-sm);
}

.btn-primary:hover {
    background: var(--accent-hover);
    color: var(--accent-text);
    box-shadow: 0 2px 8px var(--accent-ring);
}

.btn-outline {
    background: transparent;
    border-color: var(--border-strong);
    color: var(--text-2);
}

.btn-outline:hover {
    background: var(--surface-2);
    border-color: var(--accent);
    color: var(--accent);
}

.btn-ghost {
    background: transparent;
    color: var(--text-2);
}

.btn-ghost:hover {
    background: var(--surface-2);
    color: var(--text);
}

.btn-danger {
    background: var(--danger);
    color: #fff;
}

.btn-danger:hover {
    background: var(--danger);
    filter: brightness(1.08);
    color: #fff;
}

.btn-sm {
    padding: var(--sp-1) var(--sp-3);
    font-size: var(--text-sm);
}

.btn-icon {
    padding: var(--sp-2);
    aspect-ratio: 1;
}

.btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.55;
    pointer-events: none;
}

/* ----------------------------------------------------------------- cards -- */

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    overflow-wrap: break-word;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}

.card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-4) var(--sp-5);
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}

.card-title {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--text-md);
    font-weight: 600;
}

.card-body {
    flex: 1 1 auto;
    padding: var(--sp-5);
}

.card-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    flex-wrap: wrap;
    padding: var(--sp-4) var(--sp-5);
}

.card-row + .card-row {
    border-top: 1px solid var(--border);
}

/* Action form revealed inside a card body (toml list header buttons) — the
   bottom border keeps it visually distinct from the content below it, and
   the generous margin keeps it from crowding the file list. */
.card-form {
    padding-bottom: var(--sp-5);
    margin-bottom: var(--sp-6);
    border-bottom: 1px solid var(--border);
}

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

.pill {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 0.15rem var(--sp-3);
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    background: var(--surface-3);
    color: var(--text-2);
}

.pill-ok {
    background: var(--success-soft);
    color: var(--success);
}
.pill-warn {
    background: var(--warn-soft);
    color: var(--warn);
}
.pill-bad {
    background: var(--danger-soft);
    color: var(--danger);
}

.dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: currentColor;
    flex: none;
}

/* live indicator — only animates when data is genuinely arriving */
.dot-live {
    position: relative;
}

.dot-live::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: currentColor;
    animation: ping 1.8s var(--ease) infinite;
}

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

/* The node picker's checkbox-list styles left with the picker itself: the
   overview/detail split pinned every form to one node, so there is nothing
   left on any page that ticks several. */

.batch-results td {
    vertical-align: top;
}

/* ----------------------------------------------------------------- grids -- */

.grid {
    display: grid;
    gap: var(--sp-4);
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
}

.node-card {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    padding: var(--sp-4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition:
        border-color var(--dur) var(--ease),
        box-shadow var(--dur) var(--ease),
        transform var(--dur) var(--ease);
}

.node-card:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow);
    transform: translateY(-2px);
}

.node-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
}

.node-id {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-weight: 600;
    color: var(--text);
}

.node-name {
    font-size: var(--text-sm);
    color: var(--text-2);
    word-break: break-all;
}

/* ----------------------------------------------------------------- table -- */

.table-wrap {
    overflow-x: auto;
}

.table {
    margin: 0;
    width: 100%;
    border-collapse: collapse;
    color: var(--text);
    font-size: var(--text-sm);
}

.table :is(th, td) {
    padding: var(--sp-3) var(--sp-4);
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--border);
}

.table th {
    vertical-align: bottom;
    font-weight: 600;
    color: var(--text-2);
    background: var(--surface-2);
    position: sticky;
    top: 0;
    white-space: nowrap;
}

.table tbody tr {
    transition: background-color var(--dur-fast) var(--ease);
}

.table tbody tr:hover {
    background: var(--surface-2);
}

.table tbody tr:last-child :is(th, td) {
    border-bottom: 0;
}

/* ----------------------------------------------------------------- forms -- */

.field {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
}

.label {
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--text-2);
}

.input {
    width: 100%;
    min-width: 0;
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    font: inherit;
    transition:
        border-color var(--dur-fast) var(--ease),
        box-shadow var(--dur-fast) var(--ease);
}

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

.input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}

/* inline form feedback — one-off messages, not persistent alerts */
.msg {
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--radius);
    font-size: var(--text-sm);
    line-height: 1.5;
}

.msg-err {
    background: var(--danger-soft);
    color: var(--danger);
}

.msg-ok {
    background: var(--success-soft);
    color: var(--success);
}

/* a middle severity between .msg-ok and .msg-err: something happened that an
   operator should see but that is not itself a failure — a run lost with its
   node, a command that had to be killed */
.msg-warn {
    background: var(--warn-soft);
    color: var(--warn);
}

/* a checkbox and its wording on one line, inside a .field */
.check {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--text-sm);
    color: var(--text-2);
}

.check input {
    width: auto;
    margin: 0;
}

/* several single-button POST forms sitting in one table cell: each is its own
   <form> because these are state changes, but they must not each take a line */
.form-inline {
    display: inline;
}

/* a run log: fixed-width so the master's timestamp column lines up, scrolling
   in its own box rather than stretching the page, and wrapping nothing — a log
   line means what it says at the width it was written */
.log-view {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.45;
    max-height: 60vh;
    overflow: auto;
    padding: var(--sp-3);
    margin: 0;
    /* An element that paints its own background paints its own foreground.
       Inheriting the colour left every ancestor free to decide how readable a
       log is — and on a dark page the inherited grey of a muted container is
       how output ends up unreadable at exactly the hour someone is reading it.
       Stated here, so the box and the text on it are chosen together. */
    color: var(--text);
    background: var(--log-surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    white-space: pre;
    tab-size: 4;
}

/* run history gets a row of its own spanning the table rather than a panel
   hanging out of one cell — a log is wider than an actions column, and forcing
   it there overflows the table it sits in */
.job-log-row > td {
    padding-top: 0;
    border-top: 0;
}

/* The log panel must not widen the table it sits in. An auto-layout table
   measures every cell for its column widths, and a colspan cell holding a
   long log line would shove the columns above it around each time a log
   opens — width:0 removes the panel from that measurement, min-width:100%
   hands it the full row back at render, and what still doesn't fit scrolls
   inside the panel instead of stretching the table. */
.job-log-row .log-panel {
    width: 0;
    min-width: 100%;
    overflow-x: auto;
}

.job-log-row summary {
    cursor: pointer;
    padding: var(--sp-1) 0;
}

.job-log-row details[open] > summary {
    margin-bottom: var(--sp-3);
}

/* ---------------------------------------------- node description (fleet) -- */

/* The description shares the title line with the node id, and its editor drops
   out of that same line. A <details> carries it so no script is involved — the
   grid is polled and reswapped by htmx, and anything holding open state in JS
   would lose it on every poll.

   The summary is one grid row with fixed roles: identity (icon + id +
   address + state), the description on the flexible share, then the edit
   affordance — all left-aligned. A long description truncates into its own
   column (hover carries the full text) instead of pushing its neighbours. */
.node-head {
    flex: 1 1 auto;
    min-width: 0;
}

.node-head-row {
    display: grid;
    grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--sp-3);
    /* No cursor:pointer on purpose: the row is display-only — the edit
       button beside it is the one thing that opens the description form. */
}

/* icon + id + address + state — one unbreakable identity block; the pieces
   are what an operator identifies a node by, so they stay together. */
.node-ident {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    white-space: nowrap;
}

.node-id {
    font-size: var(--text-md);
    font-weight: 600;
}

/* Where the node id currently answers. Toned down so it reads as a qualifier
   on the id, not a second name. */
.node-addr {
    flex: none;
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--text-2);
}

/* Descriptive, not identifying — its column is what flexes, one line,
   ellipsized; the full text is the hover the template attaches to it. */
.node-desc {
    min-width: 0;
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--text-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A <span>, not a <button>: it lives inside a <summary>, where a real button
   would swallow the click that toggles the disclosure. It carries the button
   look and the summary is what takes the activation, including from the
   keyboard. */
.node-desc-edit {
    flex: none;
}

/* The details box is sized by its summary — the title line — so without a
   floor the textarea inherits whatever width the node id happened to need. */
.node-desc-form {
    min-width: min(28rem, 100%);
}

.node-desc-form:not([hidden]) {
    margin-top: var(--sp-3);
}

/* the string an operator must retype to remove a node. Selectable on purpose —
   it is not a secret, and the point is to make removing the *wrong* node take a
   deliberate act, not to make removing the right one a memory test */
.confirm-string {
    margin: 0;
    padding: var(--sp-2) var(--sp-3);
    background: var(--surface-2);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    user-select: all;
    overflow-wrap: anywhere;
}

/* ------------------------------------------------------------ exec panes -- */

/* a live command's output. Shares .log-view's box, but each line is a row of
   [time][text] so the timestamp column stays aligned while text wraps */
.exec-pane .log-view {
    white-space: normal;
    min-height: 6rem;
}

.exec-line {
    display: flex;
    gap: var(--sp-3);
    align-items: baseline;
}

.exec-at {
    flex: none;
    width: 7ch;
    color: var(--text-3);
    font-variant-numeric: tabular-nums;
}

.exec-text {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.exec-stderr .exec-text {
    color: var(--danger);
}

/* lines the console wrote itself, not the command: a gap in the ring, how the
   run ended, why polling stopped. Marked so they cannot be read as output */
.exec-note {
    padding: var(--sp-2) 0;
    color: var(--text-2);
    font-style: italic;
}

.exec-note.exec-bad {
    color: var(--danger);
}

.exec-note.exec-dim {
    color: var(--text-3);
}

.msg-inline {
    color: var(--danger);
    font-size: var(--text-sm);
}

/* --------------------------------------------------------------- dropdown -- */

.menu-wrap {
    position: relative;
}

.menu {
    position: absolute;
    right: 0;
    top: calc(100% + var(--sp-2));
    min-width: 190px;
    padding: var(--sp-2);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    z-index: 60;
}

.menu-item {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-2);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition:
        background-color var(--dur-fast) var(--ease),
        color var(--dur-fast) var(--ease);
}

.menu-item:hover {
    background: var(--surface-2);
    color: var(--text);
}

.menu-sep {
    height: 1px;
    margin: var(--sp-2) 0;
    background: var(--border);
}

/* enter/exit animation without JS timers — needs discrete transitions */
[x-cloak] {
    display: none !important;
}

.menu {
    transition:
        opacity var(--dur) var(--ease),
        transform var(--dur) var(--ease-spring),
        display var(--dur) allow-discrete;
}

.menu[style*="display: none"] {
    opacity: 0;
}

@starting-style {
    .menu {
        opacity: 0;
        transform: translateY(-6px) scale(0.97);
    }
}

/* ------------------------------------------------------------ empty state -- */

.empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-7) var(--sp-5);
    color: var(--text-3);
    text-align: center;
}

.empty .icon {
    width: 2rem;
    height: 2rem;
    opacity: 0.6;
}

/* ------------------------------------------------------------------ htmx -- */

/* content arriving from a swap fades in rather than snapping */
.htmx-added {
    animation: fade-in var(--dur-slow) var(--ease);
}

@keyframes fade-in {
    from {
        opacity: 0;
        transform: translateY(4px);
    }
}

/* htmx sets .htmx-request on the element issuing a request */
.poll-status .idle {
    display: inline-flex;
}
.poll-status .busy {
    display: none;
}

.htmx-request .poll-status .idle,
.poll-status.htmx-request .idle {
    display: none;
}

.htmx-request .poll-status .busy,
.poll-status.htmx-request .busy {
    display: inline-flex;
}

/* ------------------------------------------------------- view transitions -- */

@view-transition {
    navigation: auto;
}

::view-transition-old(root) {
    animation: vt-out var(--dur) var(--ease) both;
}

::view-transition-new(root) {
    animation: vt-in var(--dur-slow) var(--ease) both;
}

@keyframes vt-out {
    to {
        opacity: 0;
    }
}

@keyframes vt-in {
    from {
        opacity: 0;
        transform: translateY(6px);
    }
}

/* --------------------------------------------------------------- a11y/rm -- */

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ------------------------------------------------------------ utilities -- */

.stack > * + * {
    margin-top: var(--sp-5);
}

.row {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    flex-wrap: wrap;
}

.text-xs {
    font-size: var(--text-xs);
}

/* ------------------------------------------------------------------ tabs -- */

/* The fleet page's four views (create / import / exec / fleet) behind one
   bar. Selection is a `hidden` toggle on the panel (see fleet.html), so the
   bar itself holds no state beyond aria-selected. */
.tabs {
    display: flex;
    gap: var(--sp-1);
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
    /* The tab marker overlaps the bottom border by one pixel. */
    overflow-y: hidden;
}

.tab {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    /* Sitting on the bar's own bottom border: the active tab's marker must
       cover it, not stack a pixel above it. */
    margin-bottom: -1px;
    color: var(--text-2);
    font: inherit;
    font-size: var(--text-sm);
    cursor: pointer;
    white-space: nowrap;
}

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

.tab[aria-selected="true"] {
    color: var(--text);
    border-bottom-color: var(--accent);
    font-weight: 600;
}

.tab-panel {
    margin-top: var(--sp-5);
}

/* A panel may itself be (or contain) a flex/grid box; author `display` beats
   the UA rule behind the hidden attribute, so hiding must win explicitly. */
.tab-panel[hidden] {
    display: none;
}

/* The fleet panel hosts the polled grid, which carries its own cards — no
   extra spacing or chrome around it. */
.tab-panel-flat {
    margin-top: var(--sp-4);
}

/* ----------------------------------------------- fleet job table (grid) -- */

/* Density, scoped to the fleet table only: its rows carry two button rows
   (actions + log) that page-level .table padding would let set the height,
   and a fleet is scanned, not read — tighter rows fit more tasks per screen.
   Buttons shrink with the cells so they stop inflating the line box. */
.table-compact :is(th, td) {
    padding: var(--sp-2);
    /* One line per row, full stop: a fleet is scanned, and any second line
       (a wrapped time, a wrapped button row) costs more than the horizontal
       scroll that absorbs the width instead. Left-aligned throughout — a
       ragged column of mixed alignments reads as noise before it reads as
       data. The tight padding is what keeps the whole table inside its
       card; the scrollbar it prevents is not a feature. */
    white-space: nowrap;
    text-align: left;
}

/* The schedule column is the one whose content varies without bound (two
   cron expressions joined). Capped and ellipsized — the hover carries both
   expressions in full — so one long pair cannot re-widen the table after
   everything else was fitted. */
.table-compact .job-plan {
    max-width: 11rem;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The state pills are the column's visual anchor: 等待, 运行中, 已停用
   (and the rest) all sit on one floor, so the column reads as a state
   *field* rather than a row of differently-sized labels. */
.table-compact .job-state .pill {
    min-width: 5.5rem;
    justify-content: center;
}

.table-compact .btn-sm {
    /* Compact to the label: the action row carries up to five buttons, and
       generous padding is what pushed 删除 off the visible edge. */
    padding: 0.125rem var(--sp-1);
}

/* No minimum width: the ordinary rows are all two-character labels and
   equal naturally; the faulted row's 取消运行/清除故障 take exactly what
   their four characters need, which reads fine because they are that
   row's primary information. */
.job-actions .btn-sm {
    justify-content: center;
    text-align: center;
}

/* Times and buttons never wrap: a wrapped time column reads as two values,
   and wrapped buttons double the row height the compacting exists to stop.
   The wrap container scrolls horizontally instead. */
.table-compact .stamp {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    white-space: nowrap;
}

/* Flex wrap is what still pushed 删除 onto a second line; the whole row is
   one line by design, and overflow belongs to the horizontal scroll. */
.job-actions {
    gap: var(--sp-1);
    flex-wrap: nowrap;
    white-space: nowrap;
    justify-content: flex-start;
}

/* The actions cell groups by consequence: 编辑 and the state actions sit
   together at one tight gap, and the destructive 删除 stands apart with a
   wider step so a tired click past 停止 lands on nothing irreversible. */
.job-actions .btn-danger {
    margin-left: var(--sp-2);
}

/* A one-shot's name on grey ground: the mark sits on the thing it marks,
   with no pill trailing it. */
.job-once {
    background: var(--surface-3);
    padding: 0.0625rem var(--sp-2);
    border-radius: var(--radius-sm);
}

/* 主力合约页: 红色主力 = 最新快照相对上一条发生了更换 —— 换月提示。 */
.major-changed {
    color: var(--danger);
    font-weight: 600;
}

/* The log toggle is the row's one disclosure. Reads as engaged while its
   row below is open, so the highlighted pair explains itself. */
.job-log-toggle[aria-expanded="true"] {
    color: var(--text);
    background: var(--surface-3);
}

/* -------------------------------------------- fleet node task-list toggle -- */

/* The node's whole task table drops out of the head line's own button; the
   details it flips has no summary (the head's description disclosure is a
   separate, unrelated one). Chevron mirrors the log rows: pointing up
   means "open — click to collapse". */
.node-jobs-toggle {
    flex: none;
}

.node-jobs-toggle .icon {
    transition: transform var(--dur-fast) var(--ease);
}

.card:has(.node-jobs[open]) .node-jobs-toggle .icon {
    transform: rotate(180deg);
}

/* The meta row shares the head with the title disclosure and the toggle;
   keeping it from stretching keeps the toggle pinned to the end. */
.node-meta {
    flex: none;
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }

    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
        animation: none !important;
    }
}

/* ---------------------------------------------------------------- mobile -- */

@media (max-width: 720px) {
    .app-nav {
        flex-wrap: wrap;
        padding: var(--sp-3) var(--sp-4);
    }

    .nav-links {
        order: 3;
        width: 100%;
        overflow-x: auto;
        padding-bottom: var(--sp-1);
    }

    .nav-spacer {
        flex: 1;
    }

    .app-main {
        padding: var(--sp-5) var(--sp-4);
    }

    .grid {
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    }
}

/* ------------------------------------------------------- profit analysis -- */

/* Filter row: the three dropdowns sit on one line, the multi-select below. */
.profit-filters {
    align-items: flex-end;
    flex-wrap: wrap;
}

/* 三个筛选等宽、不伸展 —— 收起的下拉按钮与策略/账户的输入框同宽,
   合约不再吃掉整行剩余空间(原先 flex:1 会把按钮撑到很宽)。 */
.profit-filters .field {
    min-width: 10rem;
    max-width: 14rem;
    flex: 0 1 auto;
}

/* 筛选卡: 下拉浮层要伸出卡片下缘, 而 .card 的 overflow:hidden 会把
   它裁掉(z-index 救不了祖先裁剪)——这两处卡片放行溢出。卡片无自绘
   背景的子元素, 圆角不受影响。 */
.filters-card {
    overflow: visible;
}

/* 可勾选下拉: 按钮占位与输入框同款, 面板浮在下方面。 */
.chk-dd {
    position: relative;
}

.chk-dd-btn {
    text-align: left;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chk-dd-panel {
    position: absolute;
    z-index: 30;
    top: calc(100% + 4px);
    left: 0;
    /* 浮层自定宽度: 不跟随按钮(按钮窄是为了行内省地), 勾选列表
       里的合约代码要能完整读。 */
    width: max-content;
    min-width: 12rem;
    max-width: 24rem;
    max-height: 18rem;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    padding: var(--sp-2);
}

.chk-dd-item {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-1) var(--sp-2);
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.chk-dd-item:hover {
    background: var(--surface-2);
}

.chk-dd-all {
    font-weight: 600;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    margin-bottom: var(--sp-1);
}

/* The chart owns a fixed-height box: Chart.js needs a definite height to
   size its canvas responsively. */
.profit-chart-body {
    height: 32rem;
    position: relative;
}

.profit-canvas {
    position: absolute;
    inset: var(--sp-3);
}

.profit-empty {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    color: var(--text-3);
}

/* 每日总结: 日期选择器内联在卡片标题里(无独立表单), 尺寸贴合标题行。 */
.summary-head .card-title {
    gap: var(--sp-3);
}

.summary-day-picker {
    width: auto;
    padding: var(--sp-1) var(--sp-2);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
}

/* 延时统计: 卡片头部的内联筛选(品种/天数), 与总结页日期选择器同族。 */
.latency-head {
    flex-wrap: wrap;
    gap: var(--sp-3);
}

.latency-head-controls {
    gap: var(--sp-3);
    flex: none;
}

.latency-head-controls .field {
    margin: 0;
    min-width: 7rem;
}

.latency-head-controls .label {
    font-size: var(--text-xs);
    color: var(--text-3);
    margin-bottom: var(--sp-1);
}

.head-input {
    padding: var(--sp-1) var(--sp-2);
    font-size: var(--text-sm);
}

/* 手续费查询筛选行: 顶对齐 + 统一提示行。
   .field-hint 固定一行高, 三列输入框严格同一水平线; 提示文字超长
   截断(悬停看全文), 不再把某一列撑高。 */
.fee-filters {
    align-items: flex-start;
    flex-wrap: wrap;
}

.fee-filters .field {
    min-width: 11rem;
    max-width: 15rem;
    margin-bottom: 0;
}

.field-hint {
    min-height: 1.25em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* 亏损数字: CN 约定红涨绿跌(--gain 红 / --loss 绿, tokens 已按主题调整)。 */
.mono.loss {
    color: var(--loss);
}

/* ------------------------------------------------------- agent drawer -- */
/* The console assistant. A panel rather than a page so a question can carry
   the page it was asked from, and fixed rather than in flow because the body
   is replaced on every boosted navigation — nothing here holds state, and
   agent-chat.js reattaches to a running turn on each swap.

   Above the header (z-index 50) and the menus (60), because a drawer that
   slides under the thing it overlaps reads as a rendering bug. */

.agent-launch {
    position: fixed;
    right: var(--sp-5);
    bottom: var(--sp-5);
    z-index: 70;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-weight: 500;
    box-shadow: var(--shadow-lg);
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
}

.agent-launch:hover {
    background: var(--surface-2);
    border-color: var(--accent);
    color: var(--accent);
}

.agent-launch[aria-expanded="true"] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
}

.agent-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 71;
    width: min(30rem, 100vw);
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border-left: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
}

/* `hidden` is what the script toggles, and a display rule elsewhere in the
   cascade would win over it. Stated so it cannot. */
.agent-drawer[hidden] {
    display: none;
}

.agent-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
    flex: none;
}

.agent-title {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin: 0;
    font-size: var(--text-md);
    font-weight: 600;
}

.agent-log {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: var(--sp-4);
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
}

.agent-hint {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.6;
}

/* A question and an answer are the same text at different weights of
   attribution — the model's words are the page's own voice, the operator's are
   quoted back. */
.agent-msg {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: var(--text-base);
    line-height: 1.6;
}

.agent-msg-user {
    align-self: flex-end;
    max-width: 85%;
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius) var(--radius) var(--radius-sm) var(--radius);
    background: var(--accent-soft);
    color: var(--text);
}

.agent-msg-bot {
    color: var(--text);
}

/* Progress, not the answer: available and quiet. */
.agent-think {
    font-size: var(--text-xs);
    color: var(--text-3);
}

.agent-think > summary {
    cursor: pointer;
    user-select: none;
}

.agent-think-body {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    padding: var(--sp-2) 0 0 var(--sp-3);
    border-left: 2px solid var(--border);
    margin-top: var(--sp-2);
}

.agent-tool {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
}

.agent-tool-args {
    color: var(--text-3);
    overflow-wrap: anywhere;
}

.agent-tool-link {
    margin-left: auto;
    color: var(--text-3);
    text-decoration: none;
    white-space: nowrap;
}

.agent-tool-link:hover {
    color: var(--accent);
    text-decoration: underline;
}

.agent-tool-result {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
    flex-wrap: wrap;
}

.agent-tool-body {
    flex: 1 1 100%;
    font-size: var(--text-xs);
}

.agent-tool-body > summary {
    cursor: pointer;
    user-select: none;
    color: var(--text-3);
}

.agent-tool-body .log-view {
    max-height: 14rem;
    margin-top: var(--sp-2);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* A refusal says what would change it, so it is read rather than folded away. */
.agent-tool-why {
    flex: 1 1 12rem;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-2);
}

/* Lines the drawer wrote itself, not the model: a cancellation, a lost
   transcript, a dropped connection. Marked so they cannot be read as an
   answer — the same distinction .exec-note draws in the fleet console. */
.agent-note {
    font-size: var(--text-xs);
    font-style: italic;
    color: var(--text-2);
}

.agent-note-bad {
    color: var(--danger);
    font-style: normal;
}

.agent-note-warn {
    color: var(--warn);
    font-style: normal;
}

.agent-note-dim {
    color: var(--text-3);
}

.agent-usage {
    font-size: var(--text-xs);
    color: var(--text-3);
    font-variant-numeric: tabular-nums;
}

.agent-form {
    flex: none;
    padding: var(--sp-3) var(--sp-4) var(--sp-4);
    border-top: 1px solid var(--border);
    background: var(--surface-2);
}

.agent-form .input {
    resize: vertical;
    min-height: 3.5rem;
    max-height: 12rem;
    font-size: var(--text-base);
}

.agent-actions {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
}

.agent-status {
    margin-right: auto;
}

.agent-form .msg-inline:empty {
    display: none;
}

.agent-form .msg-inline {
    margin: var(--sp-2) 0 0;
}

/* On a phone the drawer is the page. The launcher keeps only its icon, so it
   does not sit on top of whatever is in the bottom-right corner. */
@media (max-width: 40rem) {
    .agent-drawer {
        width: 100vw;
    }

    .agent-launch-label {
        display: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .agent-launch {
        transition: none;
    }
}

/* The transcript and the composer are toggled by `hidden`, and these rules are
   load-bearing: `.agent-log` sets `display: flex`, and an author declaration
   beats the UA stylesheet's `[hidden] { display: none }`. Without them a
   "hidden" element stays on screen. */

.agent-log[hidden],
.agent-form[hidden] {
    display: none;
}

/* The header carries a title and actions, so the title takes the slack rather
   than the gaps doing it unevenly. */
.agent-title {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------------------------------------------------- agent context meter -- */
/* How large the next question's prompt will be, shown above the composer —
   where the cost is about to be incurred, rather than in the transcript where
   it would be a receipt. Hidden until a turn has actually reported a number:
   a bar at zero would be a claim we cannot make yet. */

.agent-meter {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4) 0;
    background: var(--surface-2);
}

.agent-meter[hidden] {
    display: none;
}

.agent-meter-bar {
    flex: none;
    width: 4rem;
    height: 4px;
    border-radius: var(--radius-full);
    background: var(--surface-3);
    overflow: hidden;
}

.agent-meter-bar > i {
    display: block;
    height: 100%;
    width: 0;
    border-radius: inherit;
    background: var(--text-3);
    transition: width var(--dur) var(--ease), background var(--dur) var(--ease);
}

.agent-meter-text {
    font-size: var(--text-xs);
    color: var(--text-3);
    line-height: 1.4;
}

/* Two thresholds, two colours. Warn is advice; over-budget is a bill already
   being paid on every question. */
.agent-meter.is-warn .agent-meter-bar > i { background: var(--warn); }
.agent-meter.is-warn .agent-meter-text { color: var(--warn); }

.agent-meter.is-over .agent-meter-bar > i { background: var(--danger); }
.agent-meter.is-over .agent-meter-text { color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
    .agent-meter-bar > i { transition: none; }
}

/* ── The run rail ─────────────────────────────────────────────────────────
   Sits between the transcript and the meter. Its height is driven by state
   rather than by the reader: gone when nothing runs, one row when everything
   is healthy, and a failed run promotes itself so attention is never
   something the operator has to go looking for. */
.agent-runs {
    border-top: 1px solid var(--border);
    background: var(--surface-2);
}

.agent-runs[hidden] {
    display: none;
}

.agent-runs-head {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    width: 100%;
    padding: var(--sp-2) var(--sp-4);
    border: 0;
    background: none;
    color: var(--text-2);
    font: inherit;
    font-size: var(--text-xs);
    text-align: left;
    cursor: pointer;
}

.agent-runs-head:hover {
    color: var(--text-1);
}

.agent-runs-caret {
    flex: none;
    /* Collapsed points right; expanded points down. One icon, rotated, so the
       sprite does not need a second chevron. */
    transform: rotate(-90deg);
    transition: transform var(--dur) var(--ease);
}

.agent-runs-head[aria-expanded="true"] .agent-runs-caret {
    transform: rotate(0deg);
}

/* The newest line of output, on the collapsed row. Truncated rather than
   wrapped: this row must stay exactly one row tall. */
.agent-runs-latest {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.agent-runs-list {
    margin: 0;
    padding: 0 var(--sp-4) var(--sp-2);
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
}

.agent-runs-list[hidden] {
    display: none;
}

.agent-run {
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--text-xs);
    color: var(--text-2);
}

.agent-run-name {
    flex: none;
    font-variant-numeric: tabular-nums;
}

.agent-run-line {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: var(--text-3);
}

.agent-run-dot {
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--text-3);
}

.agent-run.is-running .agent-run-dot {
    background: var(--success);
}

.agent-run.is-failed .agent-run-dot {
    background: var(--danger);
}

.agent-run.is-failed {
    color: var(--danger);
}

/* A run's own output, once expanded. Its own scroll region and a hard ceiling:
   a run at 1 Hz produces three hundred lines in five minutes, and none of them
   may push the composer off the bottom of the drawer. */
.agent-run-output {
    margin: var(--sp-1) 0 var(--sp-2);
    padding: var(--sp-2);
    max-height: 9rem;
    overflow-y: auto;
    border-radius: var(--radius-sm);
    background: var(--surface-3);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-2);
    white-space: pre-wrap;
    word-break: break-word;
}

/* A render spec whose `kind` this build does not know. Richer kinds — forms,
   images, markup — are a later release, and a guest that emits one early gets
   a visible placeholder rather than a blank strip or a thrown renderer. */
.agent-run-unsupported {
    color: var(--text-3);
    font-style: italic;
}

/* A failure has to be visible even when the rail is collapsed again. */
.agent-runs.is-alert .agent-runs-head {
    color: var(--danger);
}

/* The expanded row, so it is clear which output belongs to which run. */
.agent-run.is-open .agent-run-name {
    color: var(--text-1);
    font-weight: 600;
}

.agent-run:hover .agent-run-name {
    color: var(--text-1);
}

/* The output pane is its own list item, so the list stays valid markup and the
   pane can be moved between redraws without disturbing the rows. */
.agent-runs-list > li:has(> .agent-run-output) {
    list-style: none;
}

/* ─────────────────────────────────────────────── fleet overview tiles ── */
/* 集群总览的节点方块格: 一格一节点(名称/介绍/任务数), 双击进入详情。
   格子的尺寸由网格轨道(minmax)与最小高度共同给出一 "长度和宽度合适"
   是一排看得齐的格子, 而不是按内容伸缩的参差卡片。 */
.node-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: var(--sp-4);
}

.node-tile {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    min-height: 9.5rem;
    padding: var(--sp-4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    cursor: pointer;
    transition:
        border-color var(--dur) var(--ease),
        box-shadow var(--dur) var(--ease),
        transform var(--dur) var(--ease);
}

.node-tile:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow);
    transform: translateY(-2px);
}

/* 双击的可发现性: 悬停时用标题行右侧的空隙说不清, 直接给一句常驻的
   小字成本最低 —— 放在 footer 左侧与主机名同排会挤, 就并入标题下方
   的介绍占位行。这里只调光标与焦点, 文案在模板里。 */
.node-tile:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-lg);
}

.node-tile-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
}

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

/* 介绍行固定两行高: 有无介绍、介绍长短, 格子都一样高 —— 参差的高度
   会让"方块格"散成一堆卡片。悬停有全文(title)。 */
.node-tile-desc {
    margin: 0;
    min-height: 3em;
    color: var(--text-2);
    font-size: var(--text-sm);
    line-height: 1.5;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.node-tile-meta {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
    font-size: var(--text-xs);
    color: var(--text-2);
}

.node-tile-meta strong {
    color: var(--text);
    font-weight: 600;
}

.node-tile-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-top: auto;
    padding-top: var(--sp-2);
    border-top: 1px solid var(--border);
}

/* 主机名过长时让给「详情」按钮。 */
.node-tile-foot .text-xs {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Update paths and digests can be long; keep them readable on narrow screens. */
.fleet-update-details {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--sp-2) var(--sp-4);
    margin: 0;
}

.fleet-update-details dt {
    color: var(--text-2);
    font-weight: 500;
}

.fleet-update-details dd {
    margin: 0;
    overflow-wrap: anywhere;
}

@media (max-width: 720px) {
    .node-tiles {
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    }
}

@media (prefers-reduced-motion: reduce) {
    .node-tile,
    .node-tile:hover {
        transition: none;
        transform: none;
    }
}

/* 总览顶部的集群摘要行: 与方块格同片段轮询, 数字永远新鲜。 */
.fleet-summary {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    flex-wrap: wrap;
    margin-bottom: var(--sp-4);
    color: var(--text-2);
    font-size: var(--text-sm);
}

.fleet-summary .num {
    color: var(--text);
    font-weight: 600;
}

/* 异常格子的色条: 故障/主机已更换红, 离线带任务黄。inset 阴影而非
   border-left —— 不动布局, 一排格子不因告警抖动; hover 的阴影覆盖
   要把色条补回来。 */
.node-tile.tile-bad {
    box-shadow: inset 3px 0 0 var(--danger), var(--shadow-sm);
}

.node-tile.tile-warn {
    box-shadow: inset 3px 0 0 var(--warn), var(--shadow-sm);
}

.node-tile.tile-bad:hover {
    box-shadow: inset 3px 0 0 var(--danger), var(--shadow);
}

.node-tile.tile-warn:hover {
    box-shadow: inset 3px 0 0 var(--warn), var(--shadow);
}

/* ─────────────────────────────────────── run log panel (node detail) ── */
/* 运行日志面板的工具条(跟随徽章/截断提示/刷新/下载/删除)与历史运行
   的时间胶囊。工具条随日志内容一起被换出, 所以它总是描述当前这份。 */
.run-log-toolbar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
    margin-bottom: var(--sp-2);
}

.run-log-spacer {
    flex: 1;
}

.run-pills {
    gap: var(--sp-2);
}

/* .pill 本是为 span 调的; 作为按钮要多一句 font: inherit, 否则浏览器的
   按钮字体会让它和旁边的文字对不上。 */
.run-pill {
    font: inherit;
    border: 1px solid transparent;
    cursor: pointer;
}

.run-pill:hover {
    border-color: var(--accent);
}

/* ─────────────────────────────────────── toml module table (基础配置) ── */
/* 模块化编辑: 每个 [模块] 一张表, 值单元格即点即改, 自动保存。
   保存成功/失败用边框色 + 光晕闪烁一次(transition 负责淡出)。 */
.toml-cell {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    transition:
        border-color 0.6s var(--ease),
        box-shadow 0.6s var(--ease);
}

.toml-cell.toml-saved {
    border-color: var(--success);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 25%, transparent);
    transition: none;
}

.toml-cell.toml-error {
    border-color: var(--danger);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 25%, transparent);
    transition: none;
}

.toml-status {
    min-height: 1.6em;
    margin-bottom: var(--sp-3);
    color: var(--text-3);
    font-size: var(--text-sm);
}

.toml-status.toml-status-ok {
    color: var(--success);
}

.toml-status.toml-status-bad {
    color: var(--danger);
}

/* 值单元格文本优先: 默认是文字(宽度随内容), 点击才换输入框。
   行内不换行, 列多时交给 table-wrap 横向滚动; hover 给出可编辑暗示。 */
.toml-table td {
    white-space: nowrap;
}

.toml-value {
    cursor: text;
    transition: background-color var(--dur-fast) var(--ease);
}

.toml-value:hover {
    background: var(--surface-2);
}
