/* ═══════════════════════════════════════════════
   palec — Dark Theme Override
   ═══════════════════════════════════════════════ */
[data-theme="dark"] {
    color-scheme: dark;
    --p: 0.55 0.07 220;          /* primary: muted steel blue */
    --pc: 0.95 0.01 220;
    --s: 0.50 0.05 220;          /* secondary: darker steel */
    --sc: 0.90 0.01 220;
    --a: 0.65 0.10 165;          /* accent: muted teal */
    --ac: 0.13 0.03 165;
    --n: 0.18 0.015 260;         /* neutral */
    --nc: 0.70 0.02 260;
    --b1: 0.17 0.015 260;        /* base-100: #1c1d28 */
    --b2: 0.13 0.013 265;        /* base-200: #13141c */
    --b3: 0.21 0.017 260;        /* base-300: #252735 */
    --bc: 0.70 0.02 260;         /* base-content: muted gray */
    --in: 0.65 0.08 220;         /* info */
    --inc: 0.13 0.03 220;
    --su: 0.65 0.12 170;         /* success: teal green */
    --suc: 0.13 0.03 170;
    --wa: 0.70 0.12 75;          /* warning */
    --wac: 0.13 0.03 75;
    --er: 0.65 0.17 15;          /* error */
    --erc: 0.95 0.02 15;
    --rounded-box: 0.75rem;
    --rounded-btn: 0.5rem;
    --rounded-badge: 1.9rem;
    --animation-btn: 0.25s;
    --animation-input: 0.2s;
    --btn-focus-scale: 0.97;
    --border-btn: 1px;
    --tab-border: 1px;
    --tab-radius: 0.5rem;
}

/* ═══════════════════════════════════════════════
   Global
   ═══════════════════════════════════════════════ */

@keyframes fade-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
    animation: fade-in 0.3s ease-out;
}

/* ── Sidebar ── */
.sidebar-shell {
    background: linear-gradient(180deg, #1c1d28 0%, #13141c 100%);
    border-right: 1px solid rgba(255, 255, 255, 0.04);
    box-shadow: 1px 0 24px -4px rgba(0, 0, 0, 0.4);
}

.logo-glow::before {
    content: '';
    position: absolute;
    inset: -4px;
    background: radial-gradient(circle, rgba(100, 200, 160, 0.12) 0%, transparent 70%);
    border-radius: 50%;
    z-index: 0;
    transition: opacity 0.3s ease;
    opacity: 0.5;
}
.group:hover .logo-glow::before { opacity: 1; }

.sidebar-divider {
    height: 1px;
    margin: 0 1rem;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05) 50%, transparent);
}

.nav-section-title {
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: rgba(255, 255, 255, 0.2);
    padding: 0.25rem 0.75rem;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
}

.nav-link {
    border-radius: 0.5rem;
    border-left: 2px solid transparent;
    transition: all 0.15s ease;
    font-weight: 400;
    color: rgba(255, 255, 255, 0.45);
}
.nav-link:hover {
    background: rgba(255, 255, 255, 0.04);
    color: rgba(255, 255, 255, 0.7);
    border-left-color: rgba(255, 255, 255, 0.08);
}

.menu li > a.active,
.nav-link.active {
    background: rgba(100, 200, 160, 0.08);
    color: rgba(100, 200, 160, 0.9);
    font-weight: 600;
    border-left: 2px solid rgba(100, 200, 160, 0.5);
}

/* ── HTMX ── */
#devices-table { contain: layout; }
.htmx-swapping { opacity: 0.5; transition: opacity 0.12s ease; pointer-events: none; }
.htmx-settling { transition: opacity 0.12s ease; }

/* ── Log viewer ── */
.log-viewer {
    white-space: pre-wrap;
    word-break: break-all;
    color: rgba(255, 255, 255, 0.5);
    background: #13141c;
    border-radius: 0 0 var(--rounded-box, 0.75rem) var(--rounded-box, 0.75rem);
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.8rem;
    line-height: 1.6;
}

/* ── Cards ── */
.card {
    transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.card:hover {
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02);
}
.card .table { background: transparent; }
.card .table tbody tr:last-child td,
.card .table tbody tr:last-child { border-bottom: 0; }

/* ── Table ── */
.table tbody tr.hover:hover { background: rgba(255, 255, 255, 0.02); }
.table tbody tr td { transition: background 0.15s ease; }
.table thead th {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: rgba(255, 255, 255, 0.25);
}

/* ── Scrollbar ── */
.log-viewer::-webkit-scrollbar,
.overflow-x-auto::-webkit-scrollbar { width: 5px; height: 5px; }
.log-viewer::-webkit-scrollbar-track,
.overflow-x-auto::-webkit-scrollbar-track { background: transparent; }
.log-viewer::-webkit-scrollbar-thumb,
.overflow-x-auto::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.06); border-radius: 3px; }
.log-viewer::-webkit-scrollbar-thumb:hover,
.overflow-x-auto::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.12); }

/* ═══════════════════════════════════════════════
   Badges
   ═══════════════════════════════════════════════ */
.status-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px 3px 8px;
    border-radius: 9999px;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    line-height: 1;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.status-running {
    background: rgba(100, 200, 160, 0.1);
    color: rgba(100, 200, 160, 0.9);
    border: 1px solid rgba(100, 200, 160, 0.2);
}
.status-dead {
    background: rgba(240, 100, 120, 0.1);
    color: rgba(240, 100, 120, 0.9);
    border: 1px solid rgba(240, 100, 120, 0.2);
}
.status-stopped {
    background: rgba(255, 255, 255, 0.03);
    color: rgba(255, 255, 255, 0.3);
    border: 1px solid rgba(255, 255, 255, 0.06);
}
.status-adb {
    background: rgba(100, 180, 230, 0.08);
    color: rgba(100, 180, 230, 0.7);
    border: 1px solid rgba(100, 180, 230, 0.12);
}

.port-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    padding: 2px 8px;
    border-radius: 6px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.7rem;
    font-weight: 600;
    background: rgba(255, 255, 255, 0.04);
    color: rgba(255, 255, 255, 0.35);
    border: 1px solid rgba(255, 255, 255, 0.04);
}
.port-badge.port-adb {
    background: rgba(100, 180, 230, 0.1);
    color: rgba(100, 180, 230, 0.8);
    border-color: rgba(100, 180, 230, 0.18);
}

/* ═══════════════════════════════════════════════
   Buttons
   ═══════════════════════════════════════════════ */
.tbl-btn {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 10px;
    border-radius: 7px;
    font-size: 0.7rem;
    font-weight: 500;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    border: 1px solid transparent;
    white-space: nowrap;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    transition: all 0.12s ease;
}
.tbl-btn svg { flex-shrink: 0; }
.tbl-btn-success {
    background: rgba(100, 200, 160, 0.1);
    color: rgba(100, 200, 160, 0.85);
    border-color: rgba(100, 200, 160, 0.18);
}
.tbl-btn-success:hover { background: rgba(100, 200, 160, 0.18); }
.tbl-btn-danger {
    background: rgba(240, 100, 120, 0.1);
    color: rgba(240, 100, 120, 0.85);
    border-color: rgba(240, 100, 120, 0.18);
}
.tbl-btn-danger:hover { background: rgba(240, 100, 120, 0.18); }
.tbl-btn-primary {
    background: rgba(100, 180, 230, 0.1);
    color: rgba(100, 180, 230, 0.85);
    border-color: rgba(100, 180, 230, 0.18);
}
.tbl-btn-primary:hover { background: rgba(100, 180, 230, 0.18); }
.tbl-btn-ghost {
    background: rgba(255, 255, 255, 0.03);
    color: rgba(255, 255, 255, 0.35);
    border-color: rgba(255, 255, 255, 0.04);
}
.tbl-btn-ghost:hover {
    background: rgba(255, 255, 255, 0.07);
    color: rgba(255, 255, 255, 0.55);
}
.tbl-btn-delete { background: transparent; color: rgba(240, 100, 120, 0.3); }
.tbl-btn-delete:hover { background: rgba(240, 100, 120, 0.1); color: rgba(240, 100, 120, 0.8); }
/* ON states of the Record / Screens toggles. Slightly hotter than -danger so
   "Record ON" does not read as another "Stop All". */
.tbl-btn-error {
    background: rgba(245, 90, 100, 0.16);
    color: rgba(245, 120, 130, 0.95);
    border-color: rgba(245, 90, 100, 0.32);
}
.tbl-btn-error:hover { background: rgba(245, 90, 100, 0.24); }
.tbl-btn-warning {
    background: rgba(235, 175, 80, 0.14);
    color: rgba(235, 180, 90, 0.92);
    border-color: rgba(235, 175, 80, 0.26);
}
.tbl-btn-warning:hover { background: rgba(235, 175, 80, 0.22); }

/* Toolbar size — the dashboard header wants a roomier button than the table rows */
.tbl-btn-md { padding: 6px 14px; font-size: 0.8rem; }

/* ── Segmented group ──
   Squares the inner corners so a pair of related .tbl-btn reads as one control.
   Keyed off :first-child/:last-child on purpose: the Record/Screens toggles
   replace THEMSELVES (hx-swap="outerHTML") while this wrapper stays put, so a
   marker class would have to be re-emitted by the partial on every swap. */
.tbl-group { display: inline-flex; align-items: center; }
.tbl-group > .tbl-btn { border-radius: 0; }
.tbl-group > .tbl-btn:first-child { border-radius: 7px 0 0 7px; }
.tbl-group > .tbl-btn:last-child { border-radius: 0 7px 7px 0; }
.tbl-group > .tbl-btn:only-child { border-radius: 7px; }
.tbl-group > .tbl-btn + .tbl-btn { margin-left: -1px; }
.tbl-group > .tbl-btn:hover { position: relative; z-index: 1; }

.collapse-arrow > .collapse-title::after { opacity: 0.3; }

/* ═══════════════════════════════════════════════
   HTMX Action Buttons
   ═══════════════════════════════════════════════ */
.action-btn-slot { width: 72px; }
.action-btn-slot .htmx-action-btn { width: 100%; justify-content: center; }
.htmx-action-btn .action-icon { width: 12px; height: 12px; flex-shrink: 0; }
.htmx-action-btn .action-spinner { display: none; }
.htmx-action-btn.htmx-request .action-icon { display: none; }
.htmx-action-btn.htmx-request .action-spinner { display: inline-block; }
.htmx-action-btn.htmx-request { pointer-events: none; opacity: 0.5; }

/* ── Dropdown ── */
.card .overflow-x-auto { overflow: visible; }
.table .dropdown-content {
    margin-top: 2px;
    background: #1c1d28;
    border: 1px solid rgba(255, 255, 255, 0.06);
    box-shadow: 0 8px 32px -4px rgba(0, 0, 0, 0.5);
}

/* ═══════════════════════════════════════════════
   Typography
   ═══════════════════════════════════════════════ */
h1 {
    font-family: 'Inter', system-ui, sans-serif;
    font-weight: 700;
    letter-spacing: -0.02em;
}

/* ═══════════════════════════════════════════════
   Form Inputs
   ═══════════════════════════════════════════════ */
.input, .select, .textarea {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.85rem;
    background: rgba(0, 0, 0, 0.25);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.input:focus, .select:focus, .textarea:focus {
    border-color: rgba(100, 180, 230, 0.3);
    box-shadow: 0 0 0 2px rgba(100, 180, 230, 0.06);
    outline: none;
}

/* ═══════════════════════════════════════════════
   Legacy colouriser tones
   ═══════════════════════════════════════════════
   No longer used by the DEVICE log, which builds its rows on the server. Still
   the whole styling of the report body and the proxy log, which colourise
   client-side with their own inline regex pass (`.log-colorized` in
   report_view.html / proxy_logs.html) — deleting these turns both into flat
   grey text, with nothing to point at as the cause. */
.log-error { color: #e06070; }
.log-success { color: #60c8a0; }
.log-warning { color: #d0a858; }
.log-debug { color: rgba(255, 255, 255, 0.2); }
.log-timestamp { color: rgba(255, 255, 255, 0.25); }
.log-hidden { display: none; }

/* ═══════════════════════════════════════════════
   Device log rows
   ═══════════════════════════════════════════════
   Replaces the old `.log-error/.log-warning/.log-success/.log-debug` spans,
   which a client-side regex used to wrap around whole lines of `innerHTML`.
   Rows are built on the server now (web/services/logview.py) from the real
   `[LEVEL]` field, so a level is a class on an element rather than a guess about
   the text — which is what makes both the chips and the search box work. */
.log-lines {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.75rem;
    line-height: 1.55;
    /* Overrides `.log-viewer`'s pre-wrap, which is still right for the report and
       proxy viewers (one text blob) and actively wrong here: the newlines BETWEEN
       the row divs would each render as a blank line, double-spacing the whole
       log. Wrapping is the message cell's job now, not the container's. */
    white-space: normal;
}
.lg-row {
    display: grid;
    grid-template-columns: 92px 32px 132px minmax(0, 1fr);
    gap: 0 12px;
    /* A wrapped message makes the row two lines tall; the three fixed columns
       must stay on the FIRST one, or the level badge floats down beside the
       continuation text and stops reading as a column. */
    align-items: start;
    padding: 0 6px 0 5px;
    border-left: 2px solid transparent;
    border-radius: 2px;
}
.lg-row:hover { background: rgba(255, 255, 255, 0.03); }
/* A traceback tail / banner keeps its parent's level (so filtering to ERROR
   still shows what the error said) and lines up under the message column. */
.lg-row.lg-cont .lg-msg { grid-column: 4; }

/* overflow:hidden on both fixed text columns — without it a long module name
   simply paints over the message instead of being clipped, and the grid stops
   looking like columns at all. */
.lg-time { color: rgba(255, 255, 255, 0.22); white-space: nowrap; overflow: hidden; }
.lg-src  { color: rgba(255, 255, 255, 0.20); white-space: nowrap;
           overflow: hidden; text-overflow: ellipsis; }
.lg-lvl  { font-size: 0.6rem; font-weight: 600; letter-spacing: 0.04em;
           line-height: 1.9; opacity: 0.85; }
.lg-msg  { white-space: pre-wrap; word-break: break-word;
           color: rgba(255, 255, 255, 0.55); }
/* Nowrap mode: one line per record, for scanning a wall of long messages. */
.log-lines.lg-nowrap .lg-msg { white-space: pre; overflow: hidden; text-overflow: ellipsis; }

.lg-error .lg-msg, .lg-error .lg-lvl { color: #e06070; }
.lg-error { border-left-color: rgba(224, 96, 112, 0.5); }
.lg-warn  .lg-msg, .lg-warn  .lg-lvl { color: #d0a858; }
.lg-warn  { border-left-color: rgba(208, 168, 88, 0.4); }
.lg-ok    .lg-msg, .lg-ok    .lg-lvl { color: #60c8a0; }
.lg-debug .lg-msg, .lg-debug .lg-lvl, .lg-debug .lg-time, .lg-debug .lg-src {
    color: rgba(255, 255, 255, 0.18);
}
.lg-info .lg-lvl { color: rgba(255, 255, 255, 0.22); }

/* Hiding a level is ONE class on the container — the alternative, touching every
   row, gets slower exactly as the log gets long enough to need filtering. */
.lg-off-error .lg-row.lg-error,
.lg-off-warn  .lg-row.lg-warn,
.lg-off-ok    .lg-row.lg-ok,
.lg-off-info  .lg-row.lg-info,
.lg-off-debug .lg-row.lg-debug { display: none; }
/* Search. Every line is a row now, so this hides INFO too — the old filter could
   only touch lines the colouriser had wrapped, i.e. never the majority. */
.lg-nomatch { display: none; }

.lg-chip {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.6rem; font-weight: 600; letter-spacing: 0.06em;
    padding: 2px 6px; border-radius: 5px; cursor: pointer;
    border: 1px solid transparent;
    background: rgba(255, 255, 255, 0.03);
    color: rgba(255, 255, 255, 0.35);
    transition: opacity 0.12s ease, background 0.12s ease;
}
.lg-chip:hover { background: rgba(255, 255, 255, 0.07); }
.lg-chip-n { opacity: 0.55; margin-left: 2px; }
.lg-chip-error { color: rgba(224, 96, 112, 0.8); background: rgba(224, 96, 112, 0.08); }
.lg-chip-warn  { color: rgba(208, 168, 88, 0.8); background: rgba(208, 168, 88, 0.08); }
.lg-chip-ok    { color: rgba(96, 200, 160, 0.8); background: rgba(96, 200, 160, 0.08); }
/* An off chip stays legible — it is still the count of what you just hid. */
.lg-chip.lg-chip-off { opacity: 0.3; text-decoration: line-through; }

/* ═══════════════════════════════════════════════
   Log status strip
   ═══════════════════════════════════════════════ */
.log-strip {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.25rem;
    padding: 0.5rem 0.875rem;
    background: #1c1d28;
    border: 1px solid rgba(255, 255, 255, 0.04);
    border-left: 3px solid rgba(255, 255, 255, 0.06);
    border-radius: 0.6rem;
}
.log-strip-ok    { border-left-color: rgba(100, 200, 160, 0.6); }
.log-strip-warn  { border-left-color: rgba(235, 180, 80, 0.7); }
.log-strip-error { border-left-color: rgba(240, 100, 120, 0.7); }
.log-strip-phase {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.7rem; letter-spacing: 0.04em;
}
.log-strip-facts { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.875rem; }
.log-strip-k {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.1em;
    color: rgba(255, 255, 255, 0.2);
}
.log-strip-v {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.72rem; color: rgba(255, 255, 255, 0.45);
}
/* Pushed right so the destructive controls never land under the operator's
   cursor while they are reading the facts to the left of them. */
.log-strip-actions { display: flex; align-items: center; gap: 0.375rem; margin-left: auto; }

.screen-frame-empty {
    display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
    padding: 2rem 0.75rem; text-align: center;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.6rem; line-height: 1.5;
    color: rgba(255, 255, 255, 0.25);
}

/* ═══════════════════════════════════════════════
   Report Badges
   ═══════════════════════════════════════════════ */
.report-status-badge {
    display: inline-block;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 2px 8px;
    border-radius: 4px;
}
.report-status-error { background: rgba(240, 100, 120, 0.1); color: rgba(240, 100, 120, 0.85); border: 1px solid rgba(240, 100, 120, 0.15); }
.report-status-warning { background: rgba(210, 170, 90, 0.1); color: rgba(210, 170, 90, 0.85); border: 1px solid rgba(210, 170, 90, 0.15); }
.report-status-info { background: rgba(100, 180, 230, 0.08); color: rgba(100, 180, 230, 0.8); border: 1px solid rgba(100, 180, 230, 0.12); }
.report-status-neutral { background: rgba(255, 255, 255, 0.03); color: rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.06); }

/* ═══════════════════════════════════════════════
   Stat Cards
   ═══════════════════════════════════════════════ */
.stat-card {
    background: #1c1d28;
    border: 1px solid rgba(255, 255, 255, 0.04);
    border-radius: 0.75rem;
    padding: 1rem;
    position: relative;
    overflow: hidden;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.stat-card::before {
    content: '';
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
    border-radius: 3px 0 0 3px;
}
.stat-card:hover { border-color: rgba(255, 255, 255, 0.07); }
.stat-card-primary::before { background: rgba(100, 180, 230, 0.6); }
.stat-card-success::before { background: rgba(100, 200, 160, 0.6); }
.stat-card-warning::before { background: rgba(210, 170, 90, 0.6); }
.stat-card-info::before    { background: rgba(120, 165, 235, 0.6); }
.stat-card-neutral::before { background: rgba(255, 255, 255, 0.08); }

.stat-card-label {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: rgba(255, 255, 255, 0.3);
}
.stat-card-value {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 1.875rem;
    font-weight: 700;
    line-height: 1.1;
    margin-top: 0.25rem;
    letter-spacing: -0.02em;
}
.stat-card-sub {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.65rem;
    color: rgba(255, 255, 255, 0.2);
    margin-top: 0.25rem;
}
.stat-card-icon {
    width: 2.5rem; height: 2.5rem;
    border-radius: 0.75rem;
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0;
}
.stat-card-bar {
    height: 3px;
    background: rgba(255, 255, 255, 0.04);
    border-radius: 3px;
    overflow: hidden;
    position: relative;
}
.stat-card-bar-fill {
    height: 100%; border-radius: 3px;
    transition: width 0.5s ease;
    min-width: 2px;
}

/* ═══════════════════════════════════════════════
   Device Table Rows
   ═══════════════════════════════════════════════ */
/* The table had no width discipline at all — no `table-layout`, no per-column
   width, nothing — so eleven columns were laid out by their content, wrapped into
   three-line rows and overran the box anyway. `fixed` makes the <th> widths in
   partials/devices.html the single source of truth: a cell that does not fit
   truncates (and keeps its tooltip) instead of growing the row.

   `tabular-nums` is not redundant next to JetBrains Mono: the badges and the two
   `--` placeholders fall back to Inter, and Cost/Runs/Speed are read down the
   column. */
.device-table {
    table-layout: fixed;
    width: 100%;
    font-variant-numeric: tabular-nums;
}
.device-table th { padding: 0.5rem; }
.device-table td { padding: 0.375rem 0.5rem; vertical-align: middle; }

/* The quiet second line of a cell — backend, ADB address, trait icons. One class
   so Device/Status/Workflow/Cost share a rhythm instead of each inventing a size. */
.dt-sub {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.625rem;
    line-height: 1.15;
    color: rgba(255, 255, 255, 0.28);
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A chip whose text is not ours to bound — a run verdict comes from a free-text
   `set_status` in the configs repo, a heartbeat label from STATE_LABELS. Any
   width can be exceeded, so the chips ellipsize and the tooltip carries the
   full string.

   The ellipsis lives on an inner block, NOT on the badge: `.badge` is
   `display:inline-flex`, so its text is an anonymous flex item that
   `text-overflow` cannot reach — the chip grew straight through the column
   instead, which is how `SELFIE_UPLOADED · 35h45m` ended up painted over the
   next cell. `overflow:hidden` on that inner block also zeroes the flex item's
   automatic minimum size, which is the only reason it can shrink at all.

   Clipping the badge itself would cost descenders: `badge-xs` is `height:.75rem`
   with a 1px border, i.e. a 10px padding box against a 12px line box, so the
   `p` in `stop after task` would lose its tail.

   The wrapper has to become a block: daisyUI's `.tooltip` is `inline-block`,
   i.e. shrink-to-fit, so the badge's `width:fit-content` would keep resolving
   against its own content and no limit would ever bite. `text-align` undoes
   `.tooltip`'s own `center`, invisible only while the wrapper was shrink-wrapped.
   Two classes deep so the daisyUI CDN cannot win on load order — and strictly
   opt-in, never a blanket `.device-table .tooltip`: the Device sub-line is a
   flex row of tooltips that must stay inline. */
.device-table .dt-chip-wrap { display: block; text-align: left; }
.device-table .dt-chip { max-width: 100%; }
.device-table .dt-chip > .dt-chip-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Hairlines between the clusters: identity | money+throughput | actions. Dimmer
   than the row border on purpose — it groups, it must not draw a grid. */
.dt-sep { border-left: 1px solid rgba(255, 255, 255, 0.035); }

.device-row-indicator {
    width: 3px; height: 100%; min-height: 2.5rem;
    border-radius: 0 2px 2px 0;
}
.device-row-indicator-running { background: rgba(100, 200, 160, 0.7); box-shadow: 0 0 6px rgba(100, 200, 160, 0.2); }
.device-row-indicator-dead,
.device-row-indicator-hung { background: rgba(240, 100, 120, 0.7); box-shadow: 0 0 6px rgba(240, 100, 120, 0.2); }
.device-row-indicator-stopped { background: rgba(255, 255, 255, 0.06); }
/* A running device that is producing nothing. Declared AFTER -running (same
   specificity, so the later rule wins) because the row is still `running`:
   the status is pid-based and the runner is alive and healthy, it just has no
   work (idle, neutral) or is blocked (parked, amber — one step short of the red
   reserved for dead/hung). */
.device-row-indicator-idle { background: rgba(255, 255, 255, 0.18); box-shadow: none; }
.device-row-indicator-parked { background: rgba(235, 180, 80, 0.75); box-shadow: 0 0 6px rgba(235, 180, 80, 0.2); }

.rate-bar {
    width: 3rem; height: 4px;
    background: rgba(255, 255, 255, 0.04);
    border-radius: 2px;
    overflow: hidden; flex-shrink: 0;
}
.rate-bar-fill { height: 100%; border-radius: 2px; transition: width 0.3s ease; }

/* ═══════════════════════════════════════════════
   Port Cards
   ═══════════════════════════════════════════════ */
.port-card { border-radius: 0.5rem; padding: 0.5rem; transition: border-color 0.2s ease; }
.port-card-active { background: #1c1d28; border: 1px solid rgba(100, 200, 160, 0.15); }
.port-card-active:hover { border-color: rgba(100, 200, 160, 0.25); }
.port-card-idle { background: rgba(255, 255, 255, 0.01); border: 1px dashed rgba(255, 255, 255, 0.06); }
.port-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.25rem; }
.port-card-number { font-family: 'Inter', system-ui, sans-serif; font-size: 1rem; font-weight: 700; color: rgba(255, 255, 255, 0.35); }
.port-card-active .port-card-number { color: rgba(100, 200, 160, 0.8); }
.port-card-port { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.6rem; color: rgba(255, 255, 255, 0.15); }
.port-card-body { min-height: 1.5rem; }
.port-card-addr { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.65rem; color: rgba(255, 255, 255, 0.45); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.port-card-meta { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.6rem; color: rgba(255, 255, 255, 0.2); }
.port-card-empty { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.6rem; color: rgba(255, 255, 255, 0.08); text-transform: uppercase; letter-spacing: 0.1em; }

/* ═══════════════════════════════════════════════
   Device settings modal
   ═══════════════════════════════════════════════
   DaisyUI's .modal-box is `max-height: calc(100vh - 5em); overflow-y: auto`,
   and .modal-action sits INSIDE that scrolling content — so on a form of this
   size Save/Cancel were only reachable at the bottom of the scroll. Here the box
   stops scrolling and becomes a flex column: head (title + tabs) and foot
   (Cancel/Save) are pinned, only the middle moves.

   Scoped to .device-modal-* rather than patching .modal-box: the same DaisyUI
   classes carry the import/upload dialogs in web/templates/workflows/index.html.

   `min-height: 0` on the flex children is load-bearing — a flex item defaults to
   `min-height: auto`, which makes it grow to its content and the overflow rule
   below silently do nothing. */
.device-modal-box {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 0;
    max-height: calc(100vh - 5em);
}
.device-modal-form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}
.device-modal-head {
    flex: 0 0 auto;
    padding: 1.25rem 1.5rem 0;
}
.device-modal-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0.75rem 1.5rem 1rem;
}
.device-modal-foot {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    /* More above than below: the rule is what separates the buttons from the
       fields, and a symmetric 0.875rem read as cramped against it.
       Longhands, not the `padding` shorthand — the markup repeats these as
       `pt-5 pb-4 px-6` (identical values) so the footer survives a stale
       stylesheet, and a shorthand here would reset those depending on which
       sheet the browser ordered last. */
    padding-top: 1.25rem;
    padding-bottom: 1rem;
    padding-left: 1.5rem;
    padding-right: 1.5rem;
    border-top: 1px solid rgba(255, 255, 255, 0.07);
}
/* An override input shows the INHERITED value as its placeholder, so "the device
   pins US" and "the device inherits UA" have to be tellable apart at a glance —
   the whole safety argument of the overlay rests on it. The dimming now comes
   from the app-wide placeholder token below (one style everywhere); what stays
   local is the ITALIC, which is the only thing left carrying "this text is a
   value you inherited, not a value you typed". An accent rail marks the pinned
   one — see .device-form-set. */
.device-modal-form .input::placeholder {
    font-style: italic;
}
.device-form-set {
    border-left: 2px solid rgba(100, 180, 230, 0.55);
    padding-left: calc(0.75rem - 2px);
}

/* The group heading inside a tab pane: quieter than a section header, loud
   enough that thirteen override fields read as four groups. */
.device-form-group {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.6rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: rgba(255, 255, 255, 0.28);
    margin: 0.5rem 0 0.25rem;
}

/* ═══════════════════════════════════════════════
   Placeholders — ONE style for the whole dashboard
   ═══════════════════════════════════════════════
   Measured on the rendered device dialog, DaisyUI's own placeholder came out at
   the same brightness as real text (peak ink 144 vs 137): an empty field read as
   a filled one. Every page also spelled its own dimming, or none at all, so the
   same input looked different depending on which template it was in.

   One token, applied to inputs, textareas and selects alike. The only permitted
   modifier is `.device-modal-form .input::placeholder { font-style: italic }`,
   where the placeholder carries a MEANING (the inherited value) rather than a
   hint — and even there the colour comes from here. */
.input::placeholder,
.textarea::placeholder,
.select::placeholder,
input::placeholder,
textarea::placeholder {
    color: rgba(255, 255, 255, 0.16);
    opacity: 1;   /* Firefox dims placeholders again on top of the colour */
}

/* ═══════════════════════════════════════════════
   Proxy page
   ═══════════════════════════════════════════════ */
/* Same discipline as .device-table: the <th> widths are the single source of
   truth and a cell that does not fit truncates instead of growing the row. The
   proxy table used to hand-tune `max-w-[10rem]`/`[13rem]`/`[16rem]` on
   individual <td>s, which the edit row (different cells, same headers) did not
   repeat — so display and edit modes had different column widths. */
.proxy-table {
    table-layout: fixed;
    width: 100%;
    font-variant-numeric: tabular-nums;
}
.proxy-table td { vertical-align: middle; }
.proxy-table .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The routing summary above the lanes table: what the Routing tab is currently
   set to, readable without opening it. */
.proxy-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 8px;
    border-radius: 6px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.65rem;
    line-height: 1.4;
    background: rgba(255, 255, 255, 0.03);
    color: rgba(255, 255, 255, 0.4);
    border: 1px solid rgba(255, 255, 255, 0.05);
    white-space: nowrap;
}
.proxy-chip-key { color: rgba(255, 255, 255, 0.22); }
.proxy-chip-warn {
    background: rgba(235, 175, 80, 0.1);
    color: rgba(235, 180, 90, 0.85);
    border-color: rgba(235, 175, 80, 0.2);
}
.proxy-chip-on {
    background: rgba(100, 200, 160, 0.08);
    color: rgba(100, 200, 160, 0.8);
    border-color: rgba(100, 200, 160, 0.15);
}

/* The save bar of the routing form. The form is long enough that the one button
   which costs every device its exit used to sit below the fold. */
.proxy-savebar {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.75rem 1rem;
    background: rgba(28, 29, 40, 0.92);
    backdrop-filter: blur(8px);
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: 0.75rem;
}

/* ═══════════════════════════════════════════════
   Inline help — the "?" dot
   ═══════════════════════════════════════════════
   Four multi-line paragraphs used to sit on the proxy page permanently. They
   carry knowledge worth keeping (direct = this node's own IP; FakeDNS needs
   iptables on local phones), so they move in here rather than being deleted.

   Not DaisyUI's `tooltip[data-tip]`: that renders its content on a single line
   via a ::before, which turns a four-line paragraph into a page-wide strip.
   No JS — :hover for the mouse, :focus-within for the keyboard. */
.help-dot {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px; height: 14px;
    border-radius: 9999px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.6rem;
    font-weight: 600;
    line-height: 1;
    cursor: help;
    color: rgba(255, 255, 255, 0.35);
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.07);
    vertical-align: middle;
    flex-shrink: 0;
}
.help-dot:hover, .help-dot:focus-within {
    color: rgba(255, 255, 255, 0.7);
    background: rgba(255, 255, 255, 0.1);
}
.help-pop {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 8px);
    transform: translateX(-50%);
    z-index: 60;
    width: max-content;
    max-width: 24rem;
    padding: 0.6rem 0.75rem;
    border-radius: 0.5rem;
    background: #1c1d28;
    border: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow: 0 10px 32px -6px rgba(0, 0, 0, 0.6);
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.65rem;
    line-height: 1.55;
    font-weight: 400;
    text-align: left;
    white-space: normal;
    color: rgba(255, 255, 255, 0.6);
    cursor: auto;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.12s ease;
}
.help-pop strong { color: rgba(255, 255, 255, 0.85); font-weight: 600; }
.help-pop em { color: rgba(235, 180, 90, 0.9); font-style: normal; }
.help-dot:hover .help-pop,
.help-dot:focus-within .help-pop { opacity: 1; visibility: visible; }
/* Near the right edge of a card the centred popover would overflow the viewport;
   opt into a right-anchored one. */
.help-pop-right { left: auto; right: -0.5rem; transform: none; }

/* A .stat-card is `overflow: hidden`, which silently CLIPS a popover inside it —
   the help text simply never appears. Opt a tile out; the accent rail it was
   clipping is already shaped by its own border-radius. */
.stat-card-help { overflow: visible; }

/* ═══════════════════════════════════════════════
   Responsive
   ═══════════════════════════════════════════════ */
@media (max-width: 1024px) {
    .drawer-side { z-index: 50; }
}
