diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs
index a79a347b..5f6d2c48 100644
--- a/frontend/packages/dashboard/build.mjs
+++ b/frontend/packages/dashboard/build.mjs
@@ -10,8 +10,12 @@
// dist/static/logs.js /logs.html entry — build/agent/system
// log viewer sub-tabs
// dist/static/{tabs,flow,logs}.js.map source map siblings
-// dist/static/dashboard.css served at /static/dashboard.css
-// (@import resolved from @hive/shared)
+// dist/static/common.css loaded by every page (@hive/shared
+// imports + shared typography/badges/
+// buttons/inbox/side-panel)
+// dist/static/dashboard.css /index.html only (dashboard-specific)
+// dist/static/flow.css /flow.html only (flow chrome + composer)
+// dist/static/logs.css /logs.html only (log viewer sub-tabs)
//
// Both JS entries inline `./common.js` (DOM helpers, Panel singleton,
// NOTIF, path linkification) — esbuild dedupes the shared module
@@ -71,15 +75,19 @@ await build({
logLevel: 'info',
});
-// Bundle the CSS — esbuild resolves @import including the package
-// re-exports from @hive/shared.
-await build({
- entryPoints: [src('dashboard.css')],
- outfile: staticDir('dashboard.css'),
- bundle: true,
- loader: { '.css': 'css' },
- logLevel: 'info',
-});
+// Bundle CSS — one entry per page. esbuild resolves @import including
+// the package re-exports from @hive/shared. Each page loads common.css
+// (shared typography, badges, buttons, inbox, side panel) plus its own
+// page-specific bundle.
+for (const entry of ['common.css', 'dashboard.css', 'flow.css', 'logs.css']) {
+ await build({
+ entryPoints: [src(entry)],
+ outfile: staticDir(entry),
+ bundle: true,
+ loader: { '.css': 'css' },
+ logLevel: 'info',
+ });
+}
for (const html of ['index.html', 'flow.html', 'logs.html']) {
copyFileSync(src(html), dist(html));
diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css
new file mode 100644
index 00000000..47bba74c
--- /dev/null
+++ b/frontend/packages/dashboard/src/common.css
@@ -0,0 +1,510 @@
+/* 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";
+
+/* ─── 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 rgba(203, 166, 247, 0.4);
+}
+.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 rgba(137, 220, 235, 0.5);
+}
+a:hover {
+ color: var(--fg);
+ text-shadow: 0 0 12px rgba(137, 220, 235, 0.9);
+}
+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 rgba(250, 179, 135, 0.5);
+}
+.badge-rate-limited {
+ color: var(--red); border-color: var(--red);
+ text-shadow: 0 0 6px rgba(243, 139, 168, 0.5);
+}
+.badge-muted {
+ color: var(--muted); border-color: var(--purple-dim);
+ background: rgba(127, 132, 156, 0.08);
+}
+.badge-reminder {
+ color: var(--cyan); border-color: var(--cyan);
+ text-shadow: 0 0 6px rgba(137, 220, 235, 0.4);
+}
+/* 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 rgba(250, 179, 135, 0.5);
+}
+.badge-ctx-warn {
+ color: var(--red); border-color: var(--red);
+ text-shadow: 0 0 6px rgba(243, 139, 168, 0.5);
+}
+.badge-ok { background: rgba(166,227,161,0.12); color: var(--green); border-color: var(--green); }
+.badge-fail { background: rgba(243,139,168,0.12); color: var(--red); border-color: var(--red); }
+.badge-running { background: rgba(250,179,135,0.12); 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;
+ 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: rgba(205, 214, 244, 0.06);
+ 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(--mauve, #cba6f7); border-color: var(--mauve, #cba6f7); }
+/* 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, #f9e2af); border-color: var(--yellow, #f9e2af); }
+
+/* ─── 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: rgba(203, 166, 247, 0.08);
+}
+/* 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(#313244 0% 25%, #1e1e2e 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, #89b4fa);
+ 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: header + back link (flow + logs pages) ───────────
+ .logs-header / .logs-back / .logs-title are used by both
+ /flow.html and /logs.html — the same frosted sticky bar pattern. */
+.logs-header {
+ position: sticky;
+ top: 0;
+ z-index: 25;
+ background: rgba(30, 30, 46, 0.92);
+ -webkit-backdrop-filter: blur(8px) saturate(120%);
+ backdrop-filter: blur(8px) saturate(120%);
+ border-bottom: 1px solid var(--purple-dim);
+ display: flex;
+ align-items: center;
+ gap: 1.5em;
+ padding: 0.5em 1.5em;
+}
+
+.logs-back {
+ color: var(--purple);
+ text-decoration: none;
+ font-size: 0.88em;
+ white-space: nowrap;
+ flex: none;
+}
+.logs-back:hover { text-decoration: underline; }
+
+.logs-title {
+ color: var(--subtext0);
+ font-size: 0.85em;
+ letter-spacing: 0.05em;
+ flex: none;
+}
+
+/* ─── 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: rgba(203, 166, 247, 0.18); }
+ 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. */
+.side-panel {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ pointer-events: none;
+}
+.side-panel-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(17, 17, 27, 0.55);
+ opacity: 0;
+ transition: opacity 200ms ease;
+}
+.side-panel-drawer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: min(42em, 95vw);
+ background: var(--bg-elev);
+ border-left: 1px solid var(--purple-dim);
+ display: flex;
+ flex-direction: column;
+ transform: translateX(100%);
+ transition: transform 220ms ease;
+ overflow: hidden;
+}
+.side-panel.open { pointer-events: auto; }
+.side-panel.open .side-panel-backdrop { opacity: 1; }
+.side-panel.open .side-panel-drawer { transform: translateX(0); }
+.side-panel-resize {
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 6px;
+ cursor: ew-resize;
+ opacity: 0;
+ transition: opacity 120ms;
+ background: var(--purple-dim);
+}
+.side-panel-resize:hover,
+body.side-panel-resizing .side-panel-resize {
+ opacity: 1;
+}
+body.side-panel-resizing .side-panel-resize {
+ background: var(--purple);
+}
+body.side-panel-resizing {
+ user-select: none;
+}
+body.side-panel-resizing * { cursor: ew-resize !important; }
+.side-panel-head {
+ display: flex;
+ align-items: center;
+ gap: 0.6em;
+ padding: 0.6em 1em;
+ border-bottom: 1px solid var(--border);
+ flex: none;
+}
+.side-panel-title {
+ flex: 1;
+ font-size: 0.88em;
+ color: var(--muted);
+ letter-spacing: 0.04em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.side-panel-close {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ color: var(--muted);
+ font-size: 1em;
+ line-height: 1;
+ padding: 0.15em 0.4em;
+ cursor: pointer;
+ transition: border-color 0.15s ease, color 0.15s ease;
+}
+.side-panel-close:hover { border-color: var(--red); color: var(--red); }
+.side-panel-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0.8em 1em;
+ font-size: 0.88em;
+}
+.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;
+}
diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css
index f6a9bd32..b5f43262 100644
--- a/frontend/packages/dashboard/src/dashboard.css
+++ b/frontend/packages/dashboard/src/dashboard.css
@@ -1,7 +1,6 @@
-/* Shared Catppuccin palette + body typography + terminal pane styles.
- Bundled in front of the dashboard-only rules below via esbuild. */
-@import "@hive/shared/base.css";
-@import "@hive/shared/terminal.css";
+/* Dashboard-specific styles. common.css must be loaded first —
+ it defines the CSS custom properties, typography, and shared
+ component rules that this file depends on. */
/* ─── tabbed dashboard chrome ──────────────────────────────────────
Top-of-page sticky header with banner + tab strip. SSE stays
@@ -204,8 +203,8 @@ body.dashboard-shell {
.tab-pane { display: none; }
.tab-pane.tab-pane-active { display: block; }
-/* FL0W is a separate page (`/flow.html`) — its full-viewport vibec0re
- styling lives further down under `body.flow-shell`. */
+/* FL0W is a separate page (`/flow.html`) — its full-viewport
+ styling lives in flow.css. */
.banner {
text-align: center;
margin: 0 0 1em 0;
@@ -231,32 +230,7 @@ body.dashboard-shell {
from { background-position: 200% 0; }
to { background-position: -100% 0; }
}
-h1, h2 {
- color: var(--purple);
- text-transform: uppercase;
- letter-spacing: 0.15em;
- margin-top: 2em;
- text-shadow: 0 0 8px rgba(203, 166, 247, 0.4);
-}
-.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 rgba(137, 220, 235, 0.5);
-}
-a:hover {
- color: var(--fg);
- text-shadow: 0 0 12px rgba(137, 220, 235, 0.9);
-}
+
.role {
display: inline-block;
margin-left: 0.4em;
@@ -269,6 +243,7 @@ a:hover {
}
.role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(245, 194, 231, 0.08); }
.role-ag3nt { color: var(--amber); border-color: var(--amber); background: rgba(250, 179, 135, 0.08); }
+
/* Container rows: a full-height square agent icon on the left, the
identity / actions / drill-in lines stacked in the card body on the
right. Pending rows dim everything except the pending indicator. */
@@ -290,13 +265,9 @@ a:hover {
.container-row[data-depth="4"] { margin-left: 7.2em; }
.container-row[data-depth="5"] { margin-left: 9em; }
.container-row[data-depth="6"] { margin-left: 10.8em; }
-/* Tree prefix lanes — DOM-painted, not text-glyph-painted. See
- docs/web-ui.md::Topology tree for the full-row-height vertical
- bar rationale + icon-midline joint alignment. */
+/* Tree prefix lanes — DOM-painted, not text-glyph-painted. */
.container-row .tree-prefix {
position: absolute;
- /* Extend into the `.containers { gap: 0.4em }` below so vertical
- bars connect to the next sibling's prefix without a visual gap. */
top: 0;
bottom: -0.4em;
display: flex;
@@ -305,10 +276,6 @@ a:hover {
user-select: none;
color: var(--purple-dim);
}
-/* Prefix-left-edge ladder. Each depth step is 1.8em (matches the
- row indent ladder above) so the prefix's right edge meets the
- icon and its leftmost lane lines up with top-level rows' icons
- at x = 0. */
.container-row[data-depth="1"] .tree-prefix { left: -1.8em; }
.container-row[data-depth="2"] .tree-prefix { left: -3.6em; }
.container-row[data-depth="3"] .tree-prefix { left: -5.4em; }
@@ -319,10 +286,6 @@ a:hover {
flex: 0 0 1.8em;
position: relative;
}
-/* Continuation bar — full row + gap below (so two adjacent
- ancestor-line lanes from sibling rows visually merge into one
- unbroken vertical line). Drawn at lane center (0.6em from left)
- to keep it visually centered in the 1.8em column. */
.tree-prefix .lane-line::before,
.tree-prefix .lane-joint::before {
content: '';
@@ -332,18 +295,10 @@ a:hover {
bottom: 0;
border-left: 1px solid currentColor;
}
-/* Last-child joint (└): vertical bar stops at the icon midline (no
- continuation below this row). 3.1em ≈ row padding-top 0.6em +
- icon half-height 2.5em, matching the horizontal stub's y below. */
.tree-prefix .lane-joint-last::before {
bottom: auto;
height: 3.1em;
}
-/* Horizontal stub from the joint's vertical bar across the lane
- and through the row's padding-left to the icon left edge. Lands
- at the icon midline so the ├/└ meets the icon cleanly.
- Width = lane-right (1.2em from lane center) + row padding-left
- (0.8em) = 2em. */
.tree-prefix .lane-joint::after {
content: '';
position: absolute;
@@ -352,12 +307,6 @@ a:hover {
width: 2em;
border-top: 1px solid currentColor;
}
-/* Live cards get the icon-left / body-right split; tombstone rows
- keep the plain stacked block layout. The icon's fixed 5em width
- + aspect-ratio-derived height avoid the align-self: stretch
- feedback loop that used to make different-content cards show
- different-sized icons. See docs/web-ui.md::Container row (Icon
- layout + load strategy) for the load-state reflow rationale. */
.container-row:not(.tombstone) {
display: flex;
align-items: flex-start;
@@ -371,10 +320,6 @@ a:hover {
aspect-ratio: 1;
border-radius: 6px;
background-color: rgba(17, 17, 27, 0.6);
- /* Icon doubles as the selection toggle — cursor + hover ring
- make that affordable without a chrome change. The
- :focus-visible ring covers keyboard activation (Enter / Space).
- See docs/web-ui.md::Selection bar for the toggle semantics. */
cursor: pointer;
transition: box-shadow 120ms ease, transform 120ms ease;
}
@@ -385,8 +330,6 @@ a:hover {
outline: 2px solid var(--purple);
outline-offset: 2px;
}
-/* Selected state — mauve ring on the icon + a matching outline on the
- whole row so a glance at the list shows what's in the active set. */
.container-row.selected {
border-color: var(--purple);
box-shadow: inset 0 0 0 1px var(--purple);
@@ -395,9 +338,6 @@ a:hover {
.container-row.selected > .container-icon {
box-shadow: 0 0 0 2px var(--purple), 0 0 12px -4px var(--purple);
}
-/* Icon image — absolutely positioned, fills the square wrapper.
- See docs/web-ui.md::Container row (Icon layout + load strategy)
- for the load-state-can't-reflow-row rationale. */
.container-row:not(.tombstone) > .container-icon > .container-icon-img {
position: absolute;
inset: 0;
@@ -405,9 +345,6 @@ a:hover {
height: 100%;
object-fit: contain;
}
-/* When the fails to load it falls back to the dimmed
- hyperhive mark — see docs/web-ui.md::Container row for the
- fire-and-forget load + favicon fallback chain. */
.container-row:not(.tombstone) > .container-icon.icon-unreachable {
filter: grayscale(1);
opacity: 0.4;
@@ -418,10 +355,7 @@ a:hover {
}
/* ── per-agent three-dot context menu ─────────────────────────────────────
- Positioned after .card-body in the flex row. The ⋮ button is invisible
- until the row is hovered (or the menu is open) so quiet rows stay clean.
- The dropdown is absolute-positioned relative to .agent-menu and opens
- below + right-aligned to the button. */
+ Positioned after .card-body in the flex row. */
.agent-menu {
flex: none;
position: relative;
@@ -441,30 +375,28 @@ a:hover {
opacity: 0;
transition: opacity 120ms, background 120ms, color 120ms;
}
-/* Show on row hover or when the menu is open. */
.container-row:hover .agent-menu-btn,
.agent-menu.open .agent-menu-btn {
opacity: 1;
}
.agent-menu-btn:hover,
.agent-menu-btn:focus-visible {
- background: var(--border);
- color: var(--fg);
- opacity: 1;
+ background: rgba(203, 166, 247, 0.1);
+ color: var(--purple);
+ outline: none;
}
.agent-menu-btn:focus-visible {
- outline: 2px solid var(--purple);
- outline-offset: 1px;
+ outline: 1px solid var(--purple);
}
.agent-menu-dropdown {
position: absolute;
right: 0;
top: calc(100% + 2px);
- z-index: 200;
+ z-index: 50;
background: var(--bg-elev);
border: 1px solid var(--purple-dim);
border-radius: 6px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
list-style: none;
margin: 0;
padding: 0.3em 0;
@@ -478,41 +410,27 @@ a:hover {
border: none;
color: var(--fg);
font-family: inherit;
- font-size: 0.82em;
- letter-spacing: 0.01em;
+ font-size: 0.88em;
+ letter-spacing: 0.04em;
text-align: left;
padding: 0.4em 0.9em;
cursor: pointer;
}
.agent-menu-item:hover {
background: var(--border);
+ color: var(--purple);
}
.agent-menu-sep {
height: 1px;
background: var(--purple-dim);
margin: 0.3em 0;
}
-/* Pending state splits queued vs running — queued ops show only
- the pending-state badge (no row tint), running ops keep the
- amber row tint AND get a rotating amber ring on the icon. See
- docs/web-ui.md::Container row (Pending-state derivation) for
- the priority order. */
+/* Pending state splits queued vs running. */
.container-row.pending .actions { opacity: 0.4; pointer-events: none; }
.container-row.pending-running {
border-color: var(--amber);
background: rgba(250, 179, 135, 0.05);
}
-/* The spinner is a circular arc — a transparent ring with only the
- top side coloured amber (border-top-color). As it rotates, the
- visible arc sweeps around the icon like a classic loading indicator.
- border-radius: 50% is required for the arc to travel a circle;
- an earlier shape used border-radius: 11px which made the ring a
- rounded rectangle, so the arc swept a rectangular path and read as
- a spinning border side rather than a spinner.
-
- The 3px stroke and overflow:visible override let the ring sit just
- outside the square icon; it composes naturally with the mauve
- selected-ring (selected + running shows both). */
@keyframes container-icon-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
@@ -542,13 +460,6 @@ a:hover {
font-weight: bold;
}
.container-row .head .meta { margin-left: auto; }
-/* Icon-only nav strip in the head row — backend-supplied per-
- container link list. Inline-flex + gap so a longer list (e.g.
- with `dashboardLinks` extras) doesn't cram. Each link gets a
- comfortable hit target with a subtle hover so the icons read
- as interactive rather than decorative. See
- docs/web-ui.md::Container row Line 1 for the link-list source
- of truth. */
.container-row .head .nav-strip {
display: inline-flex;
align-items: center;
@@ -575,47 +486,7 @@ a:hover {
gap: 0.4em;
}
.container-row .actions form.inline { display: inline-block; margin: 0; }
-.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 rgba(250, 179, 135, 0.5);
-}
-.badge-rate-limited {
- color: var(--red); border-color: var(--red);
- text-shadow: 0 0 6px rgba(243, 139, 168, 0.5);
-}
-.badge-muted {
- color: var(--muted); border-color: var(--purple-dim);
- background: rgba(127, 132, 156, 0.08);
-}
-.badge-reminder {
- color: var(--cyan); border-color: var(--cyan);
- text-shadow: 0 0 6px rgba(137, 220, 235, 0.4);
-}
-/* Context-window usage badges on dashboard container rows. See
- docs/web-ui.md::Container row Line 1 for the per-container
- threshold derivation (yellow ≥ 50% / red ≥ 75% of the model's
- context window) and fallback values. */
-.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 rgba(250, 179, 135, 0.5);
-}
-.badge-ctx-warn {
- color: var(--red); border-color: var(--red);
- text-shadow: 0 0 6px rgba(243, 139, 168, 0.5);
-}
+
.agent-status {
font-size: 0.82em;
color: var(--subtext0, #a6adc8);
@@ -633,166 +504,18 @@ a:hover {
opacity: 0.85;
}
.container-row.tombstone .name { color: var(--muted); }
-/* Per-container journald viewer + applied-config viewer. Both open
- in the side panel and lazy-fetch on open; output is monospace
- inside a bordered
, controls (unit select + refresh) above. - The panel-body wrapper is a column flex container that fills the - side-panel-body so thecan flex-grow into a single tall - scrollable surface instead of a short box at the top with the - rest of the panel empty. */ + +/* Per-container journald viewer + applied-config viewer in the side + panel. The panel-body wrapper fills the height with a column flex + so thecan grow into a single scrollable surface. */ .journal-body { display: flex; flex-direction: column; height: 100%; gap: 0.4em; } -.journal-controls { - flex: 0 0 auto; - display: flex; - gap: 0.5em; - align-items: center; -} -.journal-unit { - font-family: inherit; - font-size: 0.9em; - background: var(--bg-elev); - color: var(--fg); - border: 1px solid var(--border); - padding: 0.2em 0.4em; -} -.journal-refresh { font-size: 0.75em; padding: 0.15em 0.5em; } -.journal-output { - margin: 0; - background: var(--crust); - color: var(--fg); - border: 1px solid var(--purple-dim); - padding: 0.5em 0.7em; - /* Take all leftover panel height + scroll inside the pre so long - log fetches don't push the controls off-screen. `min-height: 0` - is the canonical "let me actually flex-shrink for overflow" - escape hatch on flex children. */ - flex: 1 1 0; - min-height: 0; - overflow: auto; - font-size: 0.85em; - line-height: 1.4; - white-space: pre; - word-break: normal; -} -/* Build-log side-panel viewer. Same general shape as the journald - viewer: a toolbar above a scrollable list of log header rows; - clicking a row expands full stdout+stderr inline. */ -.build-logs-panel { - display: flex; - flex-direction: column; - height: 100%; - gap: 0.4em; -} -.build-logs-toolbar { - flex: 0 0 auto; - display: flex; - gap: 0.5em; - align-items: center; -} -.build-logs-refresh { font-size: 0.75em; padding: 0.15em 0.5em; } -.build-logs-list { - list-style: none; - margin: 0; - padding: 0; - flex: 1 1 0; - min-height: 0; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 0.25em; -} -.build-logs-loading, -.build-logs-empty, -.build-logs-error { - padding: 0.4em 0.6em; - font-size: 0.85em; - color: var(--subtext0); -} -.build-logs-error { color: var(--red); } -.build-logs-item { - border: 1px solid var(--border); - border-radius: 2px; -} -.build-logs-row-btn { - width: 100%; - background: none; - border: none; - color: var(--fg); - font-family: inherit; - font-size: 0.85em; - cursor: pointer; - padding: 0.35em 0.6em; - text-align: left; - display: flex; - gap: 0.5em; - align-items: center; - flex-wrap: wrap; -} -.build-logs-row-btn:hover { background: var(--bg-elev); } -.build-logs-kind { font-weight: 600; } -.build-logs-age { font-size: 0.88em; } -.build-logs-cmdline { - font-family: "JetBrains Mono", "Fira Code", monospace; - font-size: 0.8em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 28ch; -} -.build-logs-detail { padding: 0 0.4em 0.4em; } -.build-logs-output { - margin: 0; - background: var(--crust); - color: var(--fg); - border: 1px solid var(--purple-dim); - padding: 0.5em 0.7em; - overflow: auto; - font-size: 0.8em; - line-height: 1.4; - white-space: pre; - word-break: normal; - max-height: 60vh; -} -/* Status badge colours reused for build status chips. */ -.badge-ok { background: rgba(166,227,161,0.12); color: var(--green); border-color: var(--green); } -.badge-fail { background: rgba(243,139,168,0.12); color: var(--red); border-color: var(--red); } -.badge-running { background: rgba(250,179,135,0.12); color: var(--amber); border-color: var(--amber); } -/* Download link sits inline after the row button. Shown only while - the detail is expanded (toggled by JS). */ -.build-logs-dl { - display: none; - font-size: 0.72em; - padding: 0.15em 0.45em; - margin-left: 0.3em; - color: var(--muted); - text-decoration: none; - border: 1px solid var(--border); - border-radius: 3px; -} -.build-logs-dl:not([hidden]) { display: inline-block; } -.build-logs-dl:hover { color: var(--fg); border-color: var(--purple-dim); } -/* Live-streaming indicator badge inside the detail pane header. */ -.build-logs-live-badge { margin-bottom: 0.4em; } -/* Pulse animation on the "live" badge text while streaming. */ -.build-logs-live-badge.badge-running { animation: live-pulse 1.4s ease-in-out infinite; } -@keyframes live-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.45; } -} - -/* Notification controls — sit between the banner and the - containers section. Hidden by JS when notifications are - unsupported, denied, or already in the right state. */ -/* Port-collision banner: appears above the containers list when - two sub-agents hash to the same web UI port. Critical — without - resolution, one of the harnesses will restart-loop on - AddrInUse. */ +/* Notification controls — sit between the banner and the containers. */ .port-conflict { background: rgba(243, 139, 168, 0.08); border: 1px solid var(--red); @@ -840,18 +563,11 @@ a:hover { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } -.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; } -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; +@keyframes questions-pulse { + 0%, 100% { box-shadow: 0 0 12px -4px rgba(250, 179, 135, 0.55); } + 50% { box-shadow: 0 0 22px -2px rgba(250, 179, 135, 0.95); } } + /* Pending approval: a card with three stacked sections — identity header, what-changed body, decision actions. */ .approvals { list-style: none; padding: 0; margin: 0.4em 0 0; } @@ -869,8 +585,6 @@ code { flex-wrap: wrap; gap: 0.3em; } -/* Approval requested-at chip — right-aligned, goes amber after - 1h. See docs/web-ui.md::Approval card. */ .approval-ts { margin-left: auto; color: var(--muted); @@ -905,37 +619,7 @@ code { margin-top: 0.4em; } .drill-ins .panel-trigger { margin-top: 0; } -/* Diff side-panel: base-toggle tabs above the diff host. */ -.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: rgba(203, 166, 247, 0.08); -} -/* Image / tabbed file preview — see docs/web-ui.md::Side panel - for the type-aware preview shapes. */ -.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(#313244 0% 25%, #1e1e2e 0% 50%) 50% / 18px 18px; -} + .approval-tabs { display: flex; gap: 0.4em; @@ -968,6 +652,7 @@ code { .glyph-denied { color: var(--red); } .glyph-failed { color: var(--amber); } .glyph-cancelled { color: var(--muted); } + .meta-inputs { list-style: none; padding: 0; @@ -998,7 +683,6 @@ code { text-overflow: ellipsis; white-space: nowrap; } -/* Bulk select-all / -none control above the meta-inputs tree. */ .meta-inputs-bulk { margin: 0 0 0.5em; font-size: 0.8em; @@ -1018,7 +702,6 @@ code { border-color: var(--cyan); text-shadow: 0 0 6px currentColor; } -/* Tree twig glyph prefixing a nested (sub-)input row. */ .meta-input-twig { color: var(--purple-dim); margin-right: 0.1em; @@ -1043,8 +726,6 @@ code { opacity: 0.35; cursor: not-allowed; } -/* In-progress banner for the META INPUTS panel: shown while a - dashboard-triggered meta-update runs in the background. */ .meta-update-running { margin: 0 0 0.7em; padding: 0.4em 0.7em; @@ -1054,6 +735,7 @@ code { font-size: 0.85em; animation: badge-pulse 1.6s ease-in-out infinite; } + /* ─── rebuild queue panel ──────────────────────────────────────────────── */ .rebuild-queue { list-style: none; @@ -1101,10 +783,6 @@ code { .rqe-source-approval { color: var(--green); border-color: var(--green); } .rqe-when { color: var(--muted); font-size: 0.85em; } .rqe-reason { color: var(--muted); font-size: 0.85em; flex: 1 1 auto; } -/* In-flight step indicator on running queue entries — cyan sub- - line below the main row, wraps to its own line via flex-basis: - 100%. See docs/web-ui.md::R3BU1LD QU3U3 for the step-annotation - semantics. */ .rqe-step { flex-basis: 100%; margin: 0.1em 0 0 1.8em; @@ -1121,12 +799,6 @@ code { font-size: 0.8em; white-space: pre-wrap; } - -/* Cancel-X for queued rebuild_queue entries — sits at far right - via margin-left: auto, lights red on hover. Mirrors the side- - panel-close glyph shape (✗) so it stays unobtrusive in a list - row. See docs/web-ui.md::R3BU1LD QU3U3 for the queued-only - gating. */ .rqe-cancel { margin-left: auto; } @@ -1167,64 +839,7 @@ code { word-break: break-word; } ul form.inline { display: inline-block; } -.btn { - font-family: inherit; - font-weight: bold; - text-transform: uppercase; - letter-spacing: 0.1em; - background: transparent; - 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: rgba(205, 214, 244, 0.06); - 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(--mauve, #cba6f7); border-color: var(--mauve, #cba6f7); } -/* 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, #f9e2af); border-color: var(--yellow, #f9e2af); } + .spawnform { display: flex; gap: 0.6em; align-items: stretch; margin: 0.5em 0; } .spawnform input { font-family: inherit; @@ -1314,13 +929,6 @@ summary:hover { color: var(--purple); } padding: 0.6em 0.9em; animation: questions-pulse 2.4s ease-in-out infinite; } -@keyframes questions-pulse { - 0%, 100% { box-shadow: 0 0 12px -4px rgba(250, 179, 135, 0.55); } - 50% { box-shadow: 0 0 22px -2px rgba(250, 179, 135, 0.95); } -} -/* Reminders list — rendered from /api/reminders, separate from the - main /api/state snapshot. Each row stacks identity, head meta, - body, and a small cancel form. */ .reminders { list-style: none; padding: 0; @@ -1358,29 +966,6 @@ summary:hover { color: var(--purple); } margin-top: 0.3em; } -/* Path linkification — agents drop pointer strings into messages - constantly; clicking the anchor opens the file in the side panel, - lazy-loaded from /api/state-file. */ -.path-link { - color: var(--blue, #89b4fa); - 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); -} - -/* Filter chip row above the questions list. The active chip lights - up amber to match the rest of the dashboard's selection accents. */ .questions-filters { display: flex; flex-wrap: wrap; @@ -1402,16 +987,11 @@ summary:hover { color: var(--purple); } color: var(--amber); border-color: var(--amber); } -/* Peer (agent-to-agent) question rows get a left rule + dim - target-name styling so they read distinctly from operator-bound - threads at a glance. */ .questions li.question-peer { border-left: 2px solid var(--mauve, #cba6f7); padding-left: 0.6em; } .questions .msg-to-peer { color: var(--mauve, #cba6f7); } -/* The override button on peer threads picks up a non-default colour - so the operator notices they're answering on someone's behalf. */ .btn-override { background: var(--mauve, #cba6f7) !important; color: var(--bg) !important; } .questions li.question { padding: 0.4em 0; @@ -1482,141 +1062,7 @@ summary:hover { color: var(--purple); } .question-answered .q-body { color: var(--muted); margin-bottom: 0.15em; } .q-answer { font-size: 0.9em; color: var(--green, #a6e3a1); padding: 0.1em 0 0.4em 0; } .q-answer-text { font-style: italic; } -.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` (see - hive-fr0nt::TERMINAL_CSS). The msgrow / msg-* rules below are - dashboard-specific: each broker event becomes a grid of timestamp + - arrow + from/sep/to + body inside the `.row` shell. */ -/* Flex (not grid): the row carries the header chips (ts / arrow / - from / → / to / body) inline. Flex collapses whitespace-only text - nodes between items and gives `body` the remaining width via - `flex: 1`. Path references inside `body` are inline anchors that - open the side panel — no full-width sibling rows. */ -.live .msgrow { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 0.5em; - padding: 0.1em 0; - /* Override the per-agent-terminal's hanging-indent metrics from - TERMINAL_CSS — the dashboard's broker rows are flex grids, not - glyph-prefixed text, and don't want the prefix column. */ - text-indent: 0; -} -.live .msgrow .msg-body { - /* Body takes a full flex line of its own beneath the metadata - chips. See docs/web-ui.md::FL0W page (MESS4GE FL0W) for the - flex-basis: 100% + min-width: 0 rationale. */ - 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; /* prepend before other flex items */ -} -.msg-reply-tag a { - color: var(--muted); - text-shadow: none; - font-weight: normal; -} -.msg-reply-tag a:hover { color: var(--fg); } -/* Flash highlight when scrolled to from a reply link. */ -@keyframes msg-highlight-fade { - from { background: rgba(203, 166, 247, 0.18); } - 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; } -/* Compose box sits inside `.terminal-wrap`, below the `.live` log. The - dashed separator mirrors the agent terminal's prompt divider. */ -.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; - border: none; - outline: none; - color: var(--fg); - font: inherit; - font-size: 0.85em; - line-height: 1.5; - resize: none; - overflow: hidden; - min-height: 1.5em; - caret-color: var(--purple); -} -.op-compose-input::placeholder { color: var(--muted); } -.op-compose-suggest { - position: absolute; - bottom: 100%; - left: 0.8em; - margin-bottom: 0.2em; - background: rgba(24, 24, 37, 0.95); - border: 1px solid var(--border); - font-size: 0.85em; - min-width: 12em; - max-height: 12em; - overflow-y: auto; - z-index: 10; -} -.op-compose-suggest .item { - padding: 0.2em 0.8em; - cursor: pointer; - color: var(--fg); -} -.op-compose-suggest .item.active, -.op-compose-suggest .item:hover { - background: rgba(203, 166, 247, 0.18); - color: var(--purple); -} + footer { margin-top: 4em; text-align: center; @@ -1624,442 +1070,16 @@ footer { font-size: 0.9em; } footer a { color: var(--purple); } -/* Slug banner lives at the page footer — slim top margin so it - doesn't crash into the prior content, bottom margin separating - from the divider/link line. */ footer .banner-thin { margin-bottom: 0.8em; } -/* ─── side panel ───────────────────────────────────────────────── - Long content (file previews, diffs, journald, applied config) - opens in a drawer that swipes in from the right instead of - expanding inline. `.panel-trigger` is the inline affordance that - opens it. */ -.panel-trigger { - background: none; - border: none; - color: var(--muted); - font-family: inherit; - font-size: 0.85em; - letter-spacing: 0.05em; - cursor: pointer; - padding: 0; - margin-top: 0.5em; - display: inline-block; - text-align: left; - text-decoration: none; -} -.panel-trigger:hover { color: var(--cyan); } - -.side-panel { - position: fixed; - inset: 0; - z-index: 50; - /* Closed: the wrapper ignores pointer events so the dashboard - underneath stays interactive; `.open` flips it back on. */ - pointer-events: none; -} -.side-panel-backdrop { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.55); - opacity: 0; - transition: opacity 0.2s ease; -} -.side-panel-drawer { - position: absolute; - top: 0; - right: 0; - bottom: 0; - /* Width is a CSS variable so the drag handle can update it live - (JS sets `--side-panel-w` on the drawer) and localStorage- - persisted widths apply on first paint. Default min(760px, 94vw) - covers operators who never drag. See docs/web-ui.md::Side panel - for the drag-to-resize mechanism + localStorage key. */ - width: var(--side-panel-w, min(760px, 94vw)); - /* Clamp so a stored width can never push the drawer off-screen - or shrink it past readability. min content width matches the - panel-head's natural width with the close button visible. */ - min-width: 320px; - max-width: 96vw; - display: flex; - flex-direction: column; - background: var(--bg-elev); - border-left: 2px solid var(--purple); - box-shadow: -10px 0 30px rgba(0, 0, 0, 0.45); - transform: translateX(100%); - transition: transform 0.25s ease; -} -.side-panel.open { pointer-events: auto; } -.side-panel.open .side-panel-backdrop { opacity: 1; } -.side-panel.open .side-panel-drawer { transform: translateX(0); } -/* Drag-to-resize handle on the drawer's left edge. Invisible until - hover/drag so it doesn't compete with the 2px mauve border-left - for the visual boundary. Pointer-cursor tells the operator the - edge is grabbable; the brighter glow during drag - (body.side-panel-resizing) is the affordance the eye tracks. */ -.side-panel-resize { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 6px; - margin-left: -3px; - cursor: ew-resize; - z-index: 1; - background: transparent; - transition: background 120ms ease; -} -.side-panel-resize:hover, -body.side-panel-resizing .side-panel-resize { - background: rgba(203, 166, 247, 0.55); -} -/* While dragging: kill text selection + force the resize cursor - page-wide so the cursor doesn't flip back to default the moment it - leaves the 6px handle band. */ -body.side-panel-resizing { - cursor: ew-resize !important; - user-select: none; -} -body.side-panel-resizing * { cursor: ew-resize !important; } -.side-panel-head { - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1em; - padding: 0.7em 1em; - border-bottom: 1px solid var(--border); -} -.side-panel-title { - color: var(--purple); - font-weight: bold; - letter-spacing: 0.05em; - word-break: break-all; -} -.side-panel-close { - flex: 0 0 auto; - background: var(--bg); - color: var(--fg); - border: 1px solid var(--border); - font-family: inherit; - font-size: 1em; - line-height: 1; - padding: 0.25em 0.55em; - cursor: pointer; -} -.side-panel-close:hover { border-color: var(--red); color: var(--red); } -.side-panel-body { - flex: 1 1 auto; - overflow: auto; - padding: 1em; -} -/* Markdown file previews rendered by `marked`. TERMINAL_CSS scopes - its own `.md` rules to `.live .row`, so the panel needs its own. */ -.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: 3px; - padding: 0.05em 0.3em; - font-size: 0.9em; -} -.side-panel-body .md pre { - background: var(--bg); - border: 1px solid var(--border); - border-radius: 3px; - padding: 0.6em 0.8em; - overflow-x: auto; -} -.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 { - margin: 0.5em 0; - padding-left: 0.8em; - border-left: 2px solid var(--border); - 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; -} - -/* ─── /flow.html — full-page chat ───────────────────────────────── - See docs/web-ui.md::FL0W page for the page-vs-pane rationale - and the inbox-flyout-as-pill ergonomics. Shape mirrors the - per-agent live page (frosted-glass header + composer, full- - viewport terminal). */ - -:root { - /* Approximate height of the flow chrome (tabbar + dashboard-chrome - padding). The slug banner lives at the page footer on /, omitted - on /flow.html since the full-viewport terminal has no normal-flow - footer position. */ - --flow-header-h: 3.6em; - --flow-composer-h: 3.6em; - --flow-frost-bg: rgba(30, 30, 46, 0.74); - --flow-frost-blur: blur(12px) saturate(140%); -} - -body.flow-shell { - /* Override the dashboard's centred-max-width layout — flow owns - the whole viewport. */ - max-width: none; - margin: 0; - padding: 0; - height: 100vh; - overflow: hidden; - background: - radial-gradient(ellipse 80% 60% at 50% 0%, - rgba(203, 166, 247, 0.06) 0%, - transparent 60%), - var(--bg); -} - -/* Flow chrome reuses the dashboard's `.dashboard-chrome` + tabbar - so the operator can switch tabs from the flow page without - navigating back first. The chrome must be fixed-position (vs - sticky on the dashboard) since flow-shell has `overflow: hidden` - on body and the main area absolute-positions the terminal. */ -body.flow-shell .dashboard-chrome.flow-chrome { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 30; - margin: 0; - padding: 0.4em 0 0; - background: var(--flow-frost-bg); - -webkit-backdrop-filter: var(--flow-frost-blur); - backdrop-filter: var(--flow-frost-blur); - border-bottom: 1px solid var(--purple-dim); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35); -} -/* Active tab on the flow page is FL0W. Its `.tab-link` styling (the - tab strip's right-most entry on the dashboard, where it represents - the *link out* to /flow.html) needs to read as the active tab here. - `aria-current="page"` flags it semantically. */ -body.flow-shell .tabbar .tab.active.tab-link { - color: var(--purple); - background: var(--bg); - border-color: var(--purple-dim); - box-shadow: 0 -2px 12px -4px rgba(203, 166, 247, 0.4); -} -/* Inbox pill — operator inbox flyout trigger. Sits right under the - header so it stays in the operator's gaze without crowding the - chat. Same shape as the agent page's header pills. */ -.flow-pill { - position: fixed; - top: calc(var(--flow-header-h) + 0.8em); - right: 1em; - z-index: 25; - background: var(--bg-elev); - border: 1px solid var(--purple-dim); - color: var(--fg); - font-family: inherit; - font-size: 0.85em; - letter-spacing: 0.04em; - border-radius: 999px; - padding: 0.3em 0.8em; - display: inline-flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); - transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease; -} -.flow-pill:hover { - border-color: var(--purple); - color: var(--purple); - box-shadow: 0 0 12px -2px var(--purple); -} -.flow-pill-icon { font-size: 1.05em; line-height: 1; } -.flow-pill-label { color: var(--muted); } -.flow-pill-count { - background: rgba(250, 179, 135, 0.18); - color: var(--amber); - border-radius: 999px; - padding: 0 0.5em; - min-width: 1.6em; - text-align: center; - font-weight: bold; - font-variant-numeric: tabular-nums; -} - -.flow-main { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - overflow: hidden; -} -/* flow.html now has a slim header (back link only) instead of the full - tabbar — reduce the top padding to match the smaller header height. */ -body.flow-shell .flow-main-slim { - padding-top: calc(var(--flow-header-h) + 0.4em); -} -.flow-main .terminal-wrap { - position: absolute; - inset: 0; - border: 0; - background: transparent; - box-shadow: none; - border-radius: 0; - margin: 0; - padding: 0; -} -.flow-main .live { - position: absolute; - inset: 0; - height: auto; - max-height: none; - padding-top: calc(var(--flow-header-h) + 0.8em); - padding-bottom: calc(var(--flow-composer-h) + 0.8em); - scroll-padding-top: calc(var(--flow-header-h) + 0.8em); - scroll-padding-bottom: calc(var(--flow-composer-h) + 0.8em); - overflow: auto; -} -/* Tail pill (↓ N new): bottom offset clears the floating composer. - Pill is anchored on .flow-main (not .terminal-wrap) so the - backdrop-filter stacking context doesn't trap its z-index — see - docs/web-ui.md::Per-agent page (Terminal-wrap) for the same - gotcha on the agent page. */ -.flow-main .tail-pill { - bottom: calc(var(--flow-composer-h) + 0.6em); - z-index: 35; -} - -.flow-composer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 30; - min-height: var(--flow-composer-h); - background: var(--flow-frost-bg); - -webkit-backdrop-filter: var(--flow-frost-blur); - backdrop-filter: var(--flow-frost-blur); - border-top: 1px solid var(--purple-dim); - box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.35); - padding: 0.4em 1em; -} - -/* Hidden inbox section on the flow page — the renderInbox path - wants `#inbox-section` in the DOM (legacy contract), but we - surface the messages via the pill/flyout instead. */ -.flow-inbox-headless { display: none !important; } - -/* ─── /logs.html — log viewer ────────────────────────────────────── - Same minimal-chrome pattern as /flow.html but with a sub-tab strip - instead of a full dashboard tabbar. */ - -body.logs-shell { - margin: 0; - padding: 0; -} - -.logs-header { - position: sticky; - top: 0; - z-index: 25; - background: rgba(30, 30, 46, 0.92); - -webkit-backdrop-filter: blur(8px) saturate(120%); - backdrop-filter: blur(8px) saturate(120%); - border-bottom: 1px solid var(--purple-dim); - display: flex; - align-items: center; - gap: 1.5em; - padding: 0.5em 1.5em; -} - -.logs-back { - color: var(--purple); - text-decoration: none; - font-size: 0.88em; - white-space: nowrap; - flex: none; -} -.logs-back:hover { text-decoration: underline; } - -.logs-title { - color: var(--subtext0); - font-size: 0.85em; - letter-spacing: 0.05em; - flex: none; -} - -.logs-tabbar { - display: flex; - gap: 0.2em; - flex: 1; -} - -.logs-tab { - display: inline-flex; - align-items: center; - padding: 0.35em 0.9em; - border-radius: 4px; - color: var(--subtext0); - text-decoration: none; - font-size: 0.85em; - letter-spacing: 0.05em; - transition: background 100ms, color 100ms; -} -.logs-tab:hover { background: var(--border); color: var(--fg); } -.logs-tab.logs-tab-active { - background: var(--border); - color: var(--purple); -} - -.logs-main { - padding: 1.2em 1.5em 2em; -} - -.logs-pane[hidden] { display: none; } - -.logs-toolbar { - display: flex; - gap: 0.6em; - align-items: center; - margin-bottom: 0.9em; - flex-wrap: wrap; -} - -.logs-toolbar .journal-unit { - background: var(--border); - color: var(--fg); - border: 1px solid var(--purple-dim); - border-radius: 4px; - padding: 0.25em 0.5em; - font-size: 0.85em; -} - /* ─── capabilities (permissions) table ───────────────────────────── Agents × capabilities matrix in the P3RM1SS10NS tab. Same layout as the tool-groups table below. Horizontally scrollable on narrow viewports. */ -.cap-table-wrap { - overflow-x: auto; - margin-top: 0.5em; -} -.cap-table { - border-collapse: collapse; - font-size: 0.82em; - min-width: 100%; -} +.cap-table-wrap { overflow-x: auto; margin-top: 0.5em; } +.cap-table { border-collapse: collapse; font-size: 0.82em; min-width: 100%; } .cap-table th, .cap-table td { padding: 0.35em 0.6em; @@ -2103,62 +1123,24 @@ body.logs-shell { } /* ─── tool-groups (permissions) table ────────────────────────────── - Agents × tool-groups matrix in the P3RM1SS10NS tab. Horizontally - scrollable on narrow viewports. */ -.tg-table-wrap { - overflow-x: auto; - margin-top: 0.5em; -} -.tg-table { - border-collapse: collapse; - font-size: 0.82em; - min-width: 100%; -} + Agents × tool-groups matrix in the P3RM1SS10NS tab. Same layout + as the capabilities table above. Horizontally scrollable on narrow + viewports. */ +.tg-table-wrap { overflow-x: auto; margin-top: 0.5em; } +.tg-table { border-collapse: collapse; font-size: 0.82em; min-width: 100%; } .tg-table th, -.tg-table td { - padding: 0.35em 0.6em; - border: 1px solid var(--border); - text-align: center; - vertical-align: middle; -} -.tg-table thead th { - background: var(--bg-elev); - color: var(--muted); - letter-spacing: 0.05em; - white-space: nowrap; -} -.tg-agent-col { - text-align: left !important; - min-width: 8em; -} -.tg-agent-name { - color: var(--fg); - font-weight: 600; -} -.tg-default-label { - margin-left: 0.4em; - font-size: 0.85em; -} -.tg-cb { - cursor: pointer; - width: 1em; - height: 1em; - accent-color: var(--purple); -} -.tg-save-btn { - font-size: 0.78em; - padding: 0.2em 0.6em; - white-space: nowrap; -} -.tg-row:hover td { - background: var(--bg-elev); -} +.tg-table td { padding: 0.35em 0.6em; border: 1px solid var(--border); text-align: center; vertical-align: middle; } +.tg-table thead th { background: var(--bg-elev); color: var(--muted); letter-spacing: 0.05em; white-space: nowrap; } +.tg-agent-col { text-align: left !important; min-width: 8em; } +.tg-group-col { min-width: 5em; } +.tg-save-col { min-width: 4em; } +.tg-agent-name { color: var(--fg); font-weight: 600; } +.tg-cb { cursor: pointer; width: 1em; height: 1em; accent-color: var(--purple); } +.tg-save-btn { font-size: 0.78em; padding: 0.2em 0.6em; white-space: nowrap; } +.tg-row:hover td { background: var(--bg-elev); } /* ─── scheduled prompts tab ──────────────────────────────────────── - Creation form at the top, list of queued schedule cards below. - Cards show: id + source + due-in + cancel-all in the header, - the prompt body, then a targets table with per-row cancel. See - docs/web-ui.md::SCH3DUL3S tab. */ + Creation form at the top, list of queued schedule cards below. */ .schedule-edit-form { display: flex; @@ -2170,9 +1152,6 @@ body.logs-shell { padding: 0.8em 1em; margin-bottom: 0.5em; } -/* Inline edit form opens directly under the schedule row's - actions strip, indented slightly so it visually nests under - the row it edits. */ .schedule-edit-form-wrapper { margin-top: 0.5em; padding-left: 0.5em; @@ -2235,8 +1214,6 @@ body.logs-shell { justify-content: flex-end; } -/* Interval composer — preset chip row + d/h/m/s inputs + live - preview, so the operator never has to multiply seconds by hand. */ .schedule-interval-presets { display: flex; flex-wrap: wrap; @@ -2279,9 +1256,6 @@ body.logs-shell { } .schedule-interval-preview-oneshot { color: var(--muted); font-style: italic; } -/* Schedules-as-table — one row per schedule, attribute columns - on the left, one ✓/✕ column per agent in the middle, actions - column on the right. See docs/web-ui.md::SCH3DUL3S tab. */ .schedules-table { width: 100%; border-collapse: collapse; @@ -2301,11 +1275,6 @@ body.logs-shell { .schedules-table-id { width: 3em; } .schedules-table-body-th { min-width: 12em; } .schedules-table-actions-th { width: 7em; } -/* Tilted agent column headers — narrow (~28px) and tall (~90px); - the innerrotates -45° about its bottom-left corner, with - a translate to slide the text up alongside the cell border. The - inner carries the actual baseline so the underline - (border-bottom on the cell) aligns with the rotated text. */ .schedules-table-agent-th { width: 28px; min-width: 28px; @@ -2376,24 +1345,10 @@ body.logs-shell { .schedules-table-actions .btn-inline-small { margin-left: 0.3em; } -/* Edit form expands inline into a colspan'd row underneath the - schedule it edits. Wrapper drops the form's normal background so it - reads as "an extension of the row above" instead of a free-floating - card. */ .schedules-table-edit-row > td { - padding: 0.5em 0.8em 0.8em !important; - background: rgba(24, 24, 37, 0.4); + background: rgba(38, 32, 60, 0.35); + padding: 0.5em 0.6em; } -.btn-inline-small { - font-size: 0.7em; - padding: 0.1em 0.45em; -} - -/* Inline create row at the bottom of the schedules table. Cells - host inputs directly so the operator can fill + click + to - queue a new schedule without leaving the table view. Slightly - different background tone so it reads as "this isn't a schedule - yet, it's the create form". */ .schedules-table-create-row td { background: rgba(38, 32, 60, 0.25); border-top: 2px solid var(--border); @@ -2425,9 +1380,6 @@ body.logs-shell { width: 100%; min-height: 1.6em; resize: vertical; - /* Single-line by default; bumps to 4em on focus so a multi-line - prompt has room without the row taking permanent vertical space. - :focus-within so resizing-up sticks while focus stays in the row. */ } .schedules-table-create-body:focus-within .schedules-table-inline-textarea { min-height: 4em; @@ -2451,9 +1403,6 @@ body.logs-shell { width: 2.4em; text-align: right; font-variant-numeric: tabular-nums; - /* hide native number spinners — they steal too much width at the - 2.4em column size and the d/h/m/s buttons aren't useful with no - min/max range hint anyway. */ -moz-appearance: textfield; appearance: textfield; } @@ -2477,7 +1426,6 @@ body.logs-shell { display: inline-flex; cursor: pointer; padding: 0.4em 0.6em; - /* Pad out the label so the entire cell is a click target. */ } .schedules-table-create-submit { font-size: 1em; @@ -2491,12 +1439,12 @@ body.logs-shell { color: var(--muted); border-color: var(--border); } +.btn-inline-small { + font-size: 0.7em; + padding: 0.1em 0.45em; +} -/* Selection bar — sticky-bottom strip that surfaces bulk actions - when ≥1 agent is selected (click the icon). Visually echoes the - flow composer's frosted-mauve treatment so the chrome reads as - part of the same vibecore family. Hidden when empty. See - docs/web-ui.md::Selection bar for the bulk-action semantics. */ +/* Selection bar — sticky-bottom strip for bulk actions. */ .selection-bar { position: fixed; bottom: 0; @@ -2508,9 +1456,9 @@ body.logs-shell { flex-wrap: wrap; gap: 0.6em; padding: 0.55em 1em; - background: var(--flow-frost-bg); - -webkit-backdrop-filter: var(--flow-frost-blur); - backdrop-filter: var(--flow-frost-blur); + background: var(--flow-frost-bg, rgba(30, 30, 46, 0.74)); + -webkit-backdrop-filter: blur(12px) saturate(140%); + backdrop-filter: blur(12px) saturate(140%); border-top: 1px solid var(--purple); box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.4); } @@ -2546,17 +1494,9 @@ body.logs-shell { padding: 0.2em 0.7em; } /* Pad the dashboard body so the sticky bar doesn't cover the - bottom of the agent list. ~3.4em covers the bar's vertical - footprint with breathing room; only applies when the bar is - visible (`body.has-selection`, toggled by tabs.js when the - selection set is non-empty). */ + bottom of the agent list. Only applies when bar is visible. */ body.dashboard-shell.has-selection { padding-bottom: 4.5em; } -/* M0V3 →picker — inline `