The dashboard's Panel singleton and the per-agent UI's own inline Panel IIFE each had their own near-identical implementation of the right-side slide-in drawer used for file previews, diffs, logs, and inbox/todo lists. Both are now thin wrappers around a new <hive-side-panel> shadow-DOM custom element in @hive/shared, following the same house pattern as <hive-menu>: the element owns and builds all its structural chrome itself (backdrop, drawer, resize handle, header, title, close button) in connectedCallback, and only the caller's opaque content node is projected in via a default <slot> so each package's own content-type-specific CSS keeps reaching it. Public API is the union of both originals: open(title, content), openNamed(name, title, content), refresh(name, title, content), close(), and currentOwner(). Drag-to-resize + localStorage width persistence (ported verbatim from the dashboard's original implementation, the only one of the two that had it) is now available to both consumers by default — a deliberate behavior widening for the agent UI, which didn't have resize before. Along the way, fixed a latent bug in the ported CSS: the resize handle was setting a --side-panel-w custom property that no width rule ever consumed, so dragging never actually resized the drawer even though it looked wired up; the new shared stylesheet's width rule reads it properly. Each package's own global stylesheet keeps its content-specific rules (common.css's .side-panel-body .md, agent.css's .side-panel-body .agent-inbox) exactly where they were — those can never be reached from the shared element's shadow tree, same architectural floor as <hive-menu>'s item-row styling. Each wrapper applies a plain 'side-panel-body' compatibility class to its own <hive-side-panel> instance so those existing selectors keep matching by ordinary light-DOM descendant matching, with the shared element itself having no knowledge of what that class name means. Panel.bind() is gone from both packages' public API — the shared element wires its own listeners in connectedCallback, so there's no bind step left to call. tabs.js's one call site (the only bind() caller in either package) was updated to drop it. The two original chrome CSS blocks disagreed on several purely visual details beyond the resize-handle rules (z-index, backdrop color, drawer border/box-shadow, title typography) — the dashboard's values (the more feature-complete of the two) were kept as canonical, which is a small visible style change for the agent UI's panel chrome (thinner border, no box-shadow, no bold purple title). Flagged for visibility since nothing in the original two implementations called this out explicitly. Verified with a real headless-Chromium/CDP harness (bundled the actual component + built page CSS, served statically, drove via raw CDP) for both usage shapes: open/close, backdrop-click dismiss, Escape dismiss, refresh() owner-matching (no-op on wrong owner, applies on matching owner), and drag-to-resize (drawer width updates live during drag and persists to localStorage on release).
505 lines
19 KiB
CSS
505 lines
19 KiB
CSS
/* Shared Catppuccin palette + body typography + terminal pane styles.
|
||
Bundled in front of the page-specific rules via esbuild. */
|
||
@import "@hive/shared/base.css";
|
||
@import "@hive/shared/terminal.css";
|
||
@import "@hive/shared/tabs.css";
|
||
@import "@hive/shared/chrome.css";
|
||
|
||
/* ─── global typography ─────────────────────────────────────────────
|
||
Element-level rules shared across all three pages (index, flow,
|
||
logs). These must not reference page-specific chrome classes. */
|
||
|
||
h1, h2 {
|
||
color: var(--purple);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.15em;
|
||
margin-top: 2em;
|
||
text-shadow: 0 0 8px color-mix(in srgb, var(--purple) 40%, transparent);
|
||
}
|
||
.divider {
|
||
color: var(--purple-dim);
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
margin-bottom: 0.5em;
|
||
}
|
||
ul { list-style: none; padding-left: 0; }
|
||
li { padding: 0.5em 0; }
|
||
.glyph { color: var(--purple); margin-right: 0.5em; }
|
||
a {
|
||
color: var(--cyan);
|
||
text-decoration: none;
|
||
font-weight: bold;
|
||
text-shadow: 0 0 4px color-mix(in srgb, var(--cyan) 50%, transparent);
|
||
}
|
||
a:hover {
|
||
color: var(--fg);
|
||
text-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 90%, transparent);
|
||
}
|
||
code {
|
||
color: var(--amber);
|
||
background: var(--bg-elev);
|
||
padding: 0.1em 0.4em;
|
||
border: 1px solid var(--border);
|
||
border-radius: 2px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
/* ─── shared inline labels ──────────────────────────────────────────
|
||
.meta / .id / .agent / .empty appear in message rows, build-log
|
||
toolbars, and side-panel content rendered by common.js. */
|
||
.meta { color: var(--muted); font-size: 0.85em; margin-left: 0.4em; }
|
||
.id { color: var(--pink); font-weight: bold; margin-right: 0.4em; }
|
||
.agent { color: var(--amber); font-weight: bold; margin-right: 0.6em; }
|
||
.empty { color: var(--muted); font-style: italic; }
|
||
|
||
/* ─── status badges ─────────────────────────────────────────────────
|
||
.badge base + semantic colour variants. Used on container rows
|
||
(tabs.js) and build-log rows (logs.js). */
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 0.05em 0.5em;
|
||
border: 1px solid;
|
||
border-radius: 2px;
|
||
font-size: 0.75em;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
.badge-warn {
|
||
color: var(--amber); border-color: var(--amber);
|
||
text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 50%, transparent);
|
||
}
|
||
.badge-rate-limited {
|
||
color: var(--red); border-color: var(--red);
|
||
text-shadow: 0 0 6px color-mix(in srgb, var(--red) 50%, transparent);
|
||
}
|
||
.badge-muted {
|
||
color: var(--muted); border-color: var(--purple-dim);
|
||
background: color-mix(in srgb, var(--muted) 8%, transparent);
|
||
}
|
||
.badge-loose-ends {
|
||
color: var(--purple); border-color: var(--purple);
|
||
text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent);
|
||
}
|
||
/* Paused agent: turn loop parked, container still up. Clickable (btn-inline):
|
||
clicking sends POST /api/resume/{name} so the badge doubles as a resume button. */
|
||
.badge-paused {
|
||
color: var(--yellow); border-color: var(--yellow);
|
||
background: color-mix(in srgb, var(--yellow) 10%, transparent);
|
||
}
|
||
/* Active Claude model badge on dashboard container rows. */
|
||
.badge-model {
|
||
color: var(--blue); border-color: var(--blue);
|
||
opacity: 0.8;
|
||
}
|
||
/* Context-window usage badges on dashboard container rows. */
|
||
.badge-ctx-ok {
|
||
color: var(--green); border-color: var(--green);
|
||
opacity: 0.85;
|
||
}
|
||
.badge-ctx-caution {
|
||
color: var(--amber); border-color: var(--amber);
|
||
text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 50%, transparent);
|
||
}
|
||
.badge-ctx-warn {
|
||
color: var(--red); border-color: var(--red);
|
||
text-shadow: 0 0 6px color-mix(in srgb, var(--red) 50%, transparent);
|
||
}
|
||
.badge-ok { background: color-mix(in srgb, var(--green) 12%, transparent); color: var(--green); border-color: var(--green); }
|
||
.badge-fail { background: color-mix(in srgb, var(--red) 12%, transparent); color: var(--red); border-color: var(--red); }
|
||
.badge-running { background: color-mix(in srgb, var(--amber) 12%, transparent); color: var(--amber); border-color: var(--amber); }
|
||
|
||
/* ─── buttons ───────────────────────────────────────────────────────
|
||
.btn base + semantic colour/size modifiers. Logs page uses
|
||
.btn-restart for the ↻ refresh buttons; dashboard uses all variants. */
|
||
.btn {
|
||
font-family: inherit;
|
||
font-weight: bold;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.1em;
|
||
background: transparent;
|
||
/* Strip browser UA button chrome (solid fill, system text colour).
|
||
Without this, native <button> + <input type="submit"> elements
|
||
ignore our background:transparent and colour in some browsers. */
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
border: 1px solid;
|
||
padding: 0.25em 0.8em;
|
||
cursor: pointer;
|
||
text-shadow: 0 0 4px currentColor;
|
||
box-shadow: 0 0 0 0 currentColor;
|
||
transition: box-shadow 0.15s ease;
|
||
}
|
||
.btn:hover {
|
||
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
||
text-shadow: 0 0 10px currentColor;
|
||
box-shadow: 0 0 10px -2px currentColor;
|
||
}
|
||
.btn:disabled,
|
||
.btn[disabled] {
|
||
opacity: 0.32;
|
||
cursor: not-allowed;
|
||
text-shadow: none;
|
||
box-shadow: none;
|
||
}
|
||
.btn:disabled:hover,
|
||
.btn[disabled]:hover {
|
||
background: transparent;
|
||
text-shadow: none;
|
||
box-shadow: none;
|
||
}
|
||
.btn-approve { color: var(--green); border-color: var(--green); }
|
||
.btn-deny { color: var(--red); border-color: var(--red); }
|
||
.btn-destroy { color: var(--red); border-color: var(--red); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||
.btn-rebuild { color: var(--amber); border-color: var(--amber); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||
.btn-restart { color: var(--cyan); border-color: var(--cyan); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||
.btn-stop { color: var(--pink); border-color: var(--pink); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||
.btn-start { color: var(--green); border-color: var(--green); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||
/* M0V3 affordance (selection bar) — mauve reads as "structural
|
||
change" rather than the destructive red / amber chrome of
|
||
destroy / rebuild. See docs/web-ui.md::Selection bar. */
|
||
.btn-move { color: var(--purple); border-color: var(--purple); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||
.btn-talk { color: var(--cyan); border-color: var(--cyan); }
|
||
.btn-spawn { color: var(--amber); border-color: var(--amber); }
|
||
.btn-fire-now { color: var(--purple); border-color: var(--purple); }
|
||
/* Post-fire flash: report renders directly on the button for ~1.5s
|
||
so the operator sees ok/failed/missing/consumed counts inline
|
||
without a modal. Green when at least one ok; muted otherwise. */
|
||
.btn-fire-now-flashed {
|
||
color: var(--green);
|
||
border-color: var(--green);
|
||
text-shadow: 0 0 6px currentColor;
|
||
box-shadow: 0 0 8px -2px currentColor;
|
||
}
|
||
/* Inline edit button on each schedule row. Yellow reads as a
|
||
parallel destructive-adjacent action (edit changes state, but
|
||
isn't deletion). */
|
||
.btn-edit-schedule { color: var(--yellow); border-color: var(--yellow); }
|
||
|
||
/* ─── file-preview / diff panel (common.js Panel) ───────────────────
|
||
Side-panel content rendered by openFilePanel / buildTabbedPreview
|
||
in common.js; all three pages load the Panel singleton. */
|
||
.diff-panel { display: flex; flex-direction: column; gap: 0.6em; }
|
||
.diff-base-tabs { display: flex; flex-wrap: wrap; gap: 0.4em; }
|
||
.diff-base-tab {
|
||
background: transparent;
|
||
border: 1px solid var(--border);
|
||
color: var(--muted);
|
||
font: inherit;
|
||
font-size: 0.85em;
|
||
padding: 0.2em 0.7em;
|
||
cursor: pointer;
|
||
}
|
||
.diff-base-tab:hover { color: var(--fg); }
|
||
.diff-base-tab.active {
|
||
color: var(--purple);
|
||
border-color: var(--purple);
|
||
background: color-mix(in srgb, var(--purple) 8%, transparent);
|
||
}
|
||
/* Image / tabbed file preview */
|
||
.preview-host { margin-top: 0.5em; }
|
||
.img-preview {
|
||
display: block;
|
||
max-width: 100%;
|
||
height: auto;
|
||
margin: 0 auto;
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
/* checkerboard so transparent regions of the image read clearly */
|
||
background: repeating-conic-gradient(var(--border) 0% 25%, var(--bg) 0% 50%) 50% / 18px 18px;
|
||
}
|
||
|
||
/* Path linkification — agents drop pointer strings into messages;
|
||
clicking the anchor opens the file in the side panel. */
|
||
.path-link {
|
||
color: var(--blue);
|
||
text-decoration: underline dotted;
|
||
cursor: pointer;
|
||
}
|
||
.path-link:hover { color: var(--amber); }
|
||
/* File-preview body — rendered inside the side panel. */
|
||
.path-preview-body {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
padding: 0.5em 0.7em;
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
font-size: 0.85em;
|
||
color: var(--fg);
|
||
}
|
||
|
||
/* page chrome (.page-header / .page-back / .page-title — the standalone
|
||
pages' sticky back-link bar) moved to @hive/shared/chrome.css, imported
|
||
at the top of this file. */
|
||
|
||
/* ─── operator inbox + message rows ────────────────────────────────
|
||
.inbox and .msg-* are rendered by common.js (renderInbox) and
|
||
flow.js — both the dashboard side panel and the /flow.html page
|
||
display broker messages. */
|
||
.inbox {
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
padding: 0.5em 0.8em;
|
||
/* No max-height cap — let the inbox grow to fill the
|
||
side-panel-body which already scrolls. */
|
||
}
|
||
.inbox li {
|
||
padding: 0.25em 0;
|
||
border-bottom: 1px solid var(--border);
|
||
display: grid;
|
||
grid-template-columns: auto auto auto 1fr;
|
||
gap: 0.5em;
|
||
align-items: baseline;
|
||
}
|
||
.inbox li:last-child { border-bottom: 0; }
|
||
.inbox .msg-ts { color: var(--muted); font-size: 0.85em; }
|
||
.inbox .msg-from { color: var(--amber); }
|
||
.inbox .msg-sep { color: var(--muted); }
|
||
.inbox .msg-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; }
|
||
/* `#msgflow` is a shared `.live` pane inside `.terminal-wrap`. The
|
||
msgrow / msg-* rules below power both the dashboard CALL tab and
|
||
the /flow.html full-page terminal. */
|
||
.live .msgrow {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 0.5em;
|
||
padding: 0.1em 0;
|
||
text-indent: 0;
|
||
}
|
||
.live .msgrow .msg-body {
|
||
flex: 1 1 100%;
|
||
min-width: 0;
|
||
}
|
||
.live .msgrow.sent .msg-arrow { color: var(--cyan); }
|
||
.live .msgrow.delivered .msg-arrow { color: var(--green); }
|
||
/* Reply-thread rendering: indented border-left + muted reply tag. */
|
||
.live .msgrow.msg-reply {
|
||
padding-left: 1.2em;
|
||
border-left: 2px solid var(--border);
|
||
margin-left: 0.6em;
|
||
}
|
||
.msg-reply-tag {
|
||
color: var(--muted);
|
||
font-size: 0.8em;
|
||
white-space: nowrap;
|
||
order: -1;
|
||
}
|
||
.msg-reply-tag a {
|
||
color: var(--muted);
|
||
text-shadow: none;
|
||
font-weight: normal;
|
||
}
|
||
.msg-reply-tag a:hover { color: var(--fg); }
|
||
@keyframes msg-highlight-fade {
|
||
from { background: color-mix(in srgb, var(--purple) 18%, transparent); }
|
||
to { background: transparent; }
|
||
}
|
||
.msg-highlight { animation: msg-highlight-fade 1.5s ease-out forwards; }
|
||
.msg-ts { color: var(--muted); font-size: 0.85em; }
|
||
.msg-arrow { font-weight: bold; }
|
||
.msg-from { color: var(--amber); }
|
||
.msg-sep { color: var(--muted); }
|
||
.msg-to { color: var(--pink); }
|
||
.msg-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; }
|
||
|
||
/* ─── operator compose box ──────────────────────────────────────────
|
||
Sits inside `.terminal-wrap` on both /flow.html and the dashboard
|
||
CALL tab. Dashed top separator mirrors the agent terminal prompt. */
|
||
.op-compose {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.6em;
|
||
padding: 0.55em 0.8em;
|
||
border-top: 1px dashed var(--purple-dim);
|
||
}
|
||
.op-compose-prompt {
|
||
color: var(--purple);
|
||
text-shadow: 0 0 4px currentColor;
|
||
font-weight: bold;
|
||
white-space: nowrap;
|
||
user-select: none;
|
||
padding-top: 0.15em;
|
||
}
|
||
.op-compose-input {
|
||
flex: 1;
|
||
background: transparent;
|
||
color: var(--fg);
|
||
border: none;
|
||
font-family: inherit;
|
||
font-size: 1em;
|
||
line-height: 1.4;
|
||
resize: none;
|
||
overflow: hidden;
|
||
max-height: 8em;
|
||
padding: 0;
|
||
}
|
||
.op-compose-input:focus { outline: none; }
|
||
.op-compose-input::placeholder { color: var(--muted); }
|
||
.op-compose-suggest {
|
||
position: absolute;
|
||
left: 3em;
|
||
bottom: 100%;
|
||
z-index: 50;
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--purple-dim);
|
||
border-radius: 4px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0.3em 0;
|
||
min-width: 12em;
|
||
max-height: 14em;
|
||
overflow-y: auto;
|
||
}
|
||
.op-compose-suggest .item {
|
||
padding: 0.35em 0.9em;
|
||
cursor: pointer;
|
||
font-size: 0.9em;
|
||
color: var(--muted);
|
||
}
|
||
.op-compose-suggest .item.active,
|
||
.op-compose-suggest .item:hover {
|
||
background: var(--border);
|
||
color: var(--purple);
|
||
}
|
||
|
||
/* ─── side panel ─────────────────────────────────────────────────
|
||
Long content (file previews, diffs, journald, applied config) opens in
|
||
a drawer that swipes in from the right — used by both /flow.html and
|
||
the dashboard. The drawer chrome itself (backdrop, drawer, resize
|
||
handle, header, title, close button) is now the shared
|
||
`<hive-side-panel>` element (@hive/shared/side-panel.js); what's left
|
||
here is the drag cursor-override escape hatch (has to be a global,
|
||
light-DOM rule — it reaches every element on the page during a drag,
|
||
not just the shared element's own shadow tree) and the styling for
|
||
the panel's own content types, which the shared element's shadow tree
|
||
can never reach (see @hive/shared/side-panel/hive-side-panel.css's
|
||
header comment). `.side-panel-body` below is the compatibility class
|
||
common.js's `Panel` wrapper puts directly on its `<hive-side-panel>`
|
||
instance so these rules keep reaching the slotted content by ordinary
|
||
light-DOM descendant matching. */
|
||
body.side-panel-resizing {
|
||
user-select: none;
|
||
}
|
||
body.side-panel-resizing * { cursor: ew-resize !important; }
|
||
.side-panel-body .md { color: var(--fg); line-height: 1.5; }
|
||
.side-panel-body .md > :first-child { margin-top: 0; }
|
||
.side-panel-body .md > :last-child { margin-bottom: 0; }
|
||
.side-panel-body .md p { margin: 0.5em 0; }
|
||
.side-panel-body .md h1,
|
||
.side-panel-body .md h2,
|
||
.side-panel-body .md h3,
|
||
.side-panel-body .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
|
||
.side-panel-body .md code {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 2px;
|
||
padding: 0.1em 0.3em;
|
||
font-size: 0.9em;
|
||
}
|
||
.side-panel-body .md pre {
|
||
background: var(--crust);
|
||
border: 1px solid var(--border);
|
||
padding: 0.5em 0.7em;
|
||
overflow-x: auto;
|
||
margin: 0.5em 0;
|
||
}
|
||
.side-panel-body .md pre code { background: none; border: none; padding: 0; }
|
||
.side-panel-body .md a { color: var(--cyan); }
|
||
.side-panel-body .md ul,
|
||
.side-panel-body .md ol { margin: 0.4em 0; padding-left: 1.5em; }
|
||
.side-panel-body .md blockquote {
|
||
border-left: 3px solid var(--purple-dim);
|
||
padding-left: 0.8em;
|
||
margin: 0.4em 0;
|
||
color: var(--muted);
|
||
}
|
||
.side-panel-body .md table { border-collapse: collapse; margin: 0.5em 0; }
|
||
.side-panel-body .md th,
|
||
.side-panel-body .md td {
|
||
border: 1px solid var(--border);
|
||
padding: 0.2em 0.5em;
|
||
}
|
||
|
||
/* ─── page-content gutter ─────────────────────────────────────────────
|
||
Inverted-gutter layout: the dashboard + H0M3 keep `<body>` full-bleed
|
||
and wrap their padded content in a single `.page-content` div that
|
||
carries the horizontal gutter. Full-width chrome (the tab strip, the
|
||
server-warnings banner, the footer) then spans edge-to-edge for free,
|
||
with no negative-margin breakout hacks. Only matches on pages that opt
|
||
in by adding the wrapper (dashboard.html, index.html); FL0W / L0GS /
|
||
ST4TS / S3TT1NGS are full-bleed with their own `.<page>-main` padding. */
|
||
.page-content { padding: 0 1.5em; }
|
||
|
||
/* ─── sticky top region ───────────────────────────────────────────────
|
||
One sticky container per page holding the warning banner above the
|
||
page chrome (tab bar / page header). common.js (ensureStickyTop)
|
||
wraps the page's chrome in this on first render and injects the banner
|
||
as its first child, so the banner + chrome stack in a single sticky
|
||
context — pinned together — instead of two separate `top:0` stickies
|
||
colliding (the banner used to overlay the tab bar). */
|
||
.sticky-top {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 50;
|
||
}
|
||
|
||
/* ─── server warnings banner ──────────────────────────────────────────
|
||
Generic top-of-page strip, injected as the first child of `.sticky-top`
|
||
by common.js (renderServerWarnings) on every page. One row per warning;
|
||
colour comes from the per-warning `level` (warn = amber, crit = red).
|
||
Stickiness lives on the `.sticky-top` wrapper, not here. The bar spans
|
||
the full width edge-to-edge; the dashboard + H0M3 are full-bleed at the
|
||
body level (their 1.5em gutter lives on an inner `.page-content`
|
||
wrapper), so the banner is full-width for free. */
|
||
.server-warnings[hidden] { display: none; }
|
||
.server-warn {
|
||
text-align: center;
|
||
padding: 0.4em 1em;
|
||
font-size: 0.85em;
|
||
font-weight: bold;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
/* Tint composited over the opaque elevated-surface grey (not transparent):
|
||
the bar is `position: sticky`, so a see-through background would let
|
||
page content scroll through behind it. */
|
||
.server-warn-warn {
|
||
color: var(--amber);
|
||
background: color-mix(in srgb, var(--amber) 16%, var(--bg-elev));
|
||
border-bottom: 1px solid var(--amber);
|
||
}
|
||
.server-warn-crit {
|
||
color: var(--red);
|
||
background: color-mix(in srgb, var(--red) 18%, var(--bg-elev));
|
||
border-bottom: 1px solid var(--red);
|
||
}
|
||
|
||
/* ─── stats table ─────────────────────────────────────────────────
|
||
Shared right-aligned numeric table. Used by /stats.html (busiest
|
||
agents) and the dashboard SYST3M › C0NT41N3R L04D table — moved here
|
||
from dashboard.css when ST4TS became its own page. */
|
||
.hive-stats-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.85rem;
|
||
margin-bottom: 8px;
|
||
}
|
||
.hive-stats-table th,
|
||
.hive-stats-table td {
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 5px 8px;
|
||
text-align: right;
|
||
}
|
||
.hive-stats-table th:first-child,
|
||
.hive-stats-table td:first-child {
|
||
text-align: left;
|
||
}
|
||
.hive-stats-table th {
|
||
color: var(--muted);
|
||
font-weight: normal;
|
||
}
|
||
.hive-stats-table td.num {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|