hyperhive/frontend/packages/dashboard/src/dashboard.css
iris 0deca50352 dashboard: fold schedule creation into the table view (closes #564)
The N3W SCH3DUL3 vertical form is gone. The schedules table now
carries an always-visible inline create row at the bottom — fill the
cells, click + to POST /api/schedules, the new schedule appears
above on the next refresh.

Per-column inputs match the display semantics:
- `next` → `<input type="datetime-local">` (defaults to now+5min)
- `every` → 4 mini d/h/m/s number inputs (blank/all-zero = one-shot)
- `body` → `<textarea rows=1>` expanding to 4em on focus-within
  so multi-line prompts still fit (closes mara's "prompt may still be
  multi-line" requirement)
- per-agent columns → checkbox (whole cell clickable via padded label)
- actions → + submit + ⌫ reset (clears the carry without POSTing)

`newScheduleCarry` module-scope object survives the paintAtomic
re-render so mid-typed values stick across schedule-list refreshes
(same pattern as `scheduleEditCarry` from #474).

H2 collapses from "N3W SCH3DUL3" + "QU3U3D SCH3DUL3S" down to a
single "SCH3DUL3S" since the section now does both. Empty-state
branch on `renderSchedulesList` is gone — the table always renders
because the create row is always there.

`#schedule-new-section` div + `renderScheduleNewForm` /
`readScheduleFormCarry` / `submitNewSchedule` and the
`.schedule-new-form` CSS selector all removed. The edit-form path
(#474 `renderScheduleEditForm` colspan'd row on `✎` toggle) is
unchanged — "edit mode is inline in the table" already shipped
there, this PR just adds the same mental model for create.

Validations match the old form: empty targets / empty body /
invalid datetime / non-integer interval each surface an `alert()`.
The + button shows a spinner while the POST is in flight, then
clears the carry + triggers `refreshSchedules()`.
2026-05-29 19:57:14 +02:00

2087 lines
62 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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";
/* ─── tabbed dashboard chrome (#369) ────────────────────────────────
Top-of-page sticky header with banner + tab strip. Tab routing is
hash-based; tab panes are show/hide via the `.tab-pane-active`
class. SSE stays alive across tab switches so count pills update
live on inactive tabs without losing pulse on what's happening
elsewhere. */
body.dashboard-shell {
/* Full-width layout (#416 — mara: drop the 90em cap so wide screens
don't waste real estate on empty side margins). `padding: 0 1.5em
1.5em` keeps a small gutter on the left/right so cards don't kiss
the viewport edge; `.dashboard-chrome { margin: 0 -1.5em ... }`
still pulls the chrome bar edge-to-edge through that gutter. */
margin: 0;
padding: 0 1.5em 1.5em;
}
.dashboard-chrome {
position: sticky;
top: 0;
z-index: 25;
background: rgba(30, 30, 46, 0.86);
-webkit-backdrop-filter: blur(8px) saturate(120%);
backdrop-filter: blur(8px) saturate(120%);
border-bottom: 1px solid var(--purple-dim);
padding: 0.4em 0 0;
margin: 0 -1.5em 1em;
}
.banner-thin {
text-align: center;
margin: 0;
padding: 0 1em;
font-size: 0.78em;
color: var(--purple-dim);
letter-spacing: 0.15em;
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
.tabbar {
display: flex;
align-items: center;
gap: 0.2em;
padding: 0.5em 1em 0;
border-bottom: 1px solid var(--purple-dim);
}
.tabbar .tab {
display: inline-flex;
align-items: center;
gap: 0.5em;
padding: 0.55em 1em 0.45em;
margin-bottom: -1px; /* overlap the tabbar bottom border */
color: var(--muted);
font-family: inherit;
font-size: 0.92em;
letter-spacing: 0.08em;
text-decoration: none;
border: 1px solid transparent;
border-bottom: 0;
border-radius: 4px 4px 0 0;
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.tabbar .tab:hover {
color: var(--purple);
background: rgba(203, 166, 247, 0.06);
}
.tabbar .tab.active {
color: var(--purple);
border-color: var(--purple-dim);
background: var(--bg);
/* Lift the active tab visually — the bottom border of the tabbar
yields under it via the -1px margin above. */
box-shadow: 0 -2px 12px -4px rgba(203, 166, 247, 0.4);
}
.tab-label { font-weight: bold; }
.tab-count {
display: inline-block;
background: var(--purple-dim);
color: var(--purple);
border-radius: 999px;
padding: 0 0.55em;
min-width: 1.6em;
text-align: center;
font-size: 0.82em;
font-variant-numeric: tabular-nums;
font-weight: bold;
}
.tab-count-attn {
/* Y3R C4LL — operator-action-required pill colour, harder to miss. */
background: rgba(243, 139, 168, 0.22);
color: var(--red);
}
/* Notification controls cohabit with the tabs (always-on chrome). */
.tabbar #notif-row {
margin-left: auto;
display: flex;
gap: 0.5em;
align-items: center;
padding-right: 0.5em;
}
/* Tab pane visibility — show only the active one. The .tab-pane-active
class is set by tabs.js based on the URL hash; default (no hash)
resolves to SW4RM. */
.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`. */
.banner {
text-align: center;
margin: 0 0 1em 0;
font-size: 0.95em;
overflow-x: auto;
background: linear-gradient(
90deg,
var(--purple-dim) 0%,
var(--purple) 50%,
var(--purple-dim) 100%
);
background-size: 200% 100%;
background-position: 50% 0;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 0 6px rgba(203, 166, 247, 0.45));
}
.banner.active {
animation: banner-shimmer 1.8s linear infinite;
}
@keyframes banner-shimmer {
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;
padding: 0.05em 0.5em;
border: 1px solid;
border-radius: 2px;
font-size: 0.8em;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.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. */
.containers { display: flex; flex-direction: column; gap: 0.4em; }
.container-row {
padding: 0.6em 0.8em;
border: 1px solid var(--border);
border-radius: 4px;
background: rgba(24, 24, 37, 0.55);
transition: opacity 200ms ease, border-color 200ms ease;
}
/* Topology indent (#363). Each depth level shifts the row right by one
step; the .tree-prefix span (drawn by tabs.js::treePrefix) carries
the ├─ / └─ glyph and any continuation lines that thread through
ancestor columns. When every container has parent=null (pre-#361
state) `[data-depth]` is absent on every row and these rules are
no-ops — the layout reads exactly like the legacy flat list.
Per-depth indent: hardcoded steps for 6 levels (sufficient for any
plausible hive topology) — the typed `attr()` function from CSS
Values 5 would collapse this to one rule, but browser support is
still partial (Chromium-only as of 2026). */
.container-row[data-depth] { position: relative; }
.container-row[data-depth="1"] { margin-left: 1.8em; }
.container-row[data-depth="2"] { margin-left: 3.6em; }
.container-row[data-depth="3"] { margin-left: 5.4em; }
.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 sits in the left margin and paints the connecting
├ / └ / │ lanes as CSS rules rather than text glyphs (#388). Each
ancestor depth gets its own `.tree-lane` so we can paint a
full-row-height vertical bar that extends through the
.containers row gap into the next sibling — text box-drawing
glyphs only fill one text line, which left visible breaks
between rows once cards grew taller than one line of text (5em
square icons + multi-line body). The horizontal stub at the row's
own joint lands at the icon midline so the L/T meets the icon
edge cleanly. */
.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;
flex-direction: row;
pointer-events: none;
user-select: none;
color: var(--purple-dim);
}
/* Each depth step is one 1.8em lane wide — same step as the row's
own margin-left ladder above, so the rightmost lane (the joint)
sits flush against the row content (the icon). The prefix's left
edge is depth*1.8em LEFT of the row's left edge, so its right
edge meets the icon, and the leftmost lane lines up with the
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; }
.container-row[data-depth="4"] .tree-prefix { left: -7.2em; }
.container-row[data-depth="5"] .tree-prefix { left: -9em; }
.container-row[data-depth="6"] .tree-prefix { left: -10.8em; }
.tree-prefix .tree-lane {
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: '';
position: absolute;
left: 0.6em;
top: 0;
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;
left: 0.6em;
top: 3.1em;
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 is a background-image div
with no intrinsic size, so its load state can never reflow the row
(issue #177). It used to `align-self: stretch` to fill the body
height, but with state badges / rate-limit pills / etc. wrapping the
head row, the body grew taller and the square icon grew with it —
so two cards with different content showed different-sized icons
(issue #344). Fixed at 5em now; height follows from aspect-ratio. */
.container-row:not(.tombstone) {
display: flex;
align-items: flex-start;
gap: 0.7em;
}
.container-row:not(.tombstone) > .container-icon {
position: relative;
overflow: hidden;
flex: none;
width: 5em;
aspect-ratio: 1;
border-radius: 6px;
background-color: rgba(17, 17, 27, 0.6);
/* #443 — icon is the selection toggle. Cursor + hover ring make
that affordable without a chrome change. The :focus-visible ring
covers keyboard activation (Enter / Space). */
cursor: pointer;
transition: box-shadow 120ms ease, transform 120ms ease;
}
.container-row:not(.tombstone) > .container-icon:hover {
box-shadow: 0 0 0 2px var(--purple);
}
.container-row:not(.tombstone) > .container-icon:focus-visible {
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);
background: rgba(203, 166, 247, 0.06);
}
.container-row.selected > .container-icon {
box-shadow: 0 0 0 2px var(--purple), 0 0 12px -4px var(--purple);
}
/* The icon image fills the square wrapper and is taken out of flow
(absolute) so its load state — pending, loaded, broken — can never
contribute intrinsic size or reflow the row. (issue #177) */
.container-row:not(.tombstone) > .container-icon > .container-icon-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
/* When the <img> fails to load it falls back to the dimmed hyperhive
mark, standing in for the unreachable agent icon (issues #195, #202). */
.container-row:not(.tombstone) > .container-icon.icon-unreachable {
filter: grayscale(1);
opacity: 0.4;
}
.container-row .card-body {
flex: 1;
min-width: 0;
}
.container-row.pending {
border-color: var(--amber);
background: rgba(250, 179, 135, 0.05);
}
.container-row.pending .actions { opacity: 0.4; pointer-events: none; }
.container-row .head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.4em;
}
.container-row .head .name {
font-size: 1.05em;
font-weight: bold;
}
.container-row .head .meta { margin-left: auto; }
/* Icon-only nav strip in the head row — the per-container backend-
supplied link list (issue #262). Inline-flex + gap so a longer list
(e.g. with `dashboardLinks` extras) doesn't cram (issue #333). Each
link gets a comfortable hit target with a subtle hover so the
icons read as interactive rather than decorative. */
.container-row .head .nav-strip {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 0.35em;
}
.nav-link {
color: var(--muted);
font-size: 0.95em;
line-height: 1;
padding: 0.15em 0.35em;
border-radius: 3px;
text-decoration: none;
transition: background 0.12s ease, color 0.12s ease;
}
.nav-link:hover {
background: rgba(203, 166, 247, 0.12);
color: var(--cyan);
text-shadow: 0 0 6px rgba(137, 220, 235, 0.5);
}
.container-row .actions {
display: flex;
flex-wrap: wrap;
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. Thresholds
are derived per-container: yellow ≥ 50% and red ≥ 75% of the model's
context window (`ContainerView.context_window_tokens`), mirroring the
harness compaction watermarks. Falls back to fixed 100k / 150k when
the window is unknown. (issue #66) */
.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);
padding: 0.1em 0.3em 0.25em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-status .status-icon { opacity: 0.65; }
.agent-status .status-age { opacity: 0.5; font-size: 0.9em; margin-left: 0.2em; }
.container-row.tombstone {
border-style: dashed;
background: rgba(24, 24, 37, 0.35);
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 <pre>, controls (unit select + refresh) above.
#541: the panel-body wrapper is a column flex container that fills
the side-panel-body so the <pre> can flex-grow into a single tall
scrollable surface instead of a short box at the top with the rest
of the panel empty. */
.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: #11111b;
color: var(--fg);
border: 1px solid var(--purple-dim);
padding: 0.5em 0.7em;
/* #541: 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;
}
/* 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. */
.port-conflict {
background: rgba(243, 139, 168, 0.08);
border: 1px solid var(--red);
color: var(--red);
padding: 0.5em 0.8em;
margin-bottom: 0.6em;
border-radius: 4px;
text-shadow: 0 0 6px rgba(243, 139, 168, 0.4);
animation: questions-pulse 2.4s ease-in-out infinite;
}
.port-conflict strong { color: var(--red); }
.notif-row {
display: flex;
gap: 0.5em;
align-items: center;
margin: 0.5em 0;
font-size: 0.85em;
}
.btn-notif {
font-family: inherit;
font-size: 0.85em;
background: transparent;
color: var(--cyan);
border: 1px solid var(--cyan);
padding: 0.2em 0.7em;
border-radius: 999px;
cursor: pointer;
text-shadow: 0 0 4px currentColor;
}
.btn-notif:hover {
background: rgba(137, 220, 235, 0.1);
box-shadow: 0 0 10px -2px currentColor;
}
.pending-state {
color: var(--amber);
font-size: 0.85em;
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 0 0 6px rgba(250, 179, 135, 0.55);
animation: badge-pulse 1.6s ease-in-out infinite;
}
@keyframes badge-pulse {
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;
}
/* 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; }
.approval-card {
background: var(--bg-elev);
border: 1px solid var(--border);
border-left: 3px solid var(--purple);
border-radius: 4px;
padding: 0.6em 0.8em;
margin-bottom: 0.6em;
}
.approval-head {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.3em;
}
/* When the approval was requested — right-aligned in the head row;
goes amber once it has been pending ≥ 1h so a stale request stands
out at a glance (issue #272). */
.approval-ts {
margin-left: auto;
color: var(--muted);
font-size: 0.85em;
}
.approval-ts.stale {
color: var(--amber);
text-shadow: 0 0 6px rgba(250, 179, 135, 0.5);
}
.approval-body {
margin: 0.45em 0;
padding-left: 1.3em;
}
.approval-description {
font-size: 0.9em;
color: var(--fg);
white-space: pre-wrap;
margin-bottom: 0.35em;
}
.approval-actions {
display: flex;
gap: 0.5em;
padding-top: 0.45em;
border-top: 1px solid var(--border);
}
.approval-actions form.inline { display: inline; }
/* Inline drill-in triggers (logs / config repo / view diff). */
.drill-ins {
display: flex;
flex-wrap: wrap;
gap: 0.15em 1.1em;
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 (issues #188, #192) */
.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;
margin: 0.6em 0 0.4em;
}
.approval-tab {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
font: inherit;
font-size: 0.85em;
letter-spacing: 0.08em;
padding: 0.25em 0.9em;
cursor: pointer;
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.approval-tab:hover { color: var(--fg); }
.approval-tab.active {
color: var(--purple);
border-color: var(--purple);
background: rgba(203, 166, 247, 0.08);
text-shadow: 0 0 4px currentColor;
}
.approvals-history .status { font-size: 0.85em; padding: 0 0.5em; }
.status-approved { color: var(--green); }
.status-denied { color: var(--red); }
.status-failed { color: var(--amber); }
.status-cancelled { color: var(--muted); }
.glyph-approved { color: var(--green); }
.glyph-denied { color: var(--red); }
.glyph-failed { color: var(--amber); }
.glyph-cancelled { color: var(--muted); }
.meta-inputs {
list-style: none;
padding: 0;
margin: 0 0 0.8em;
display: grid;
gap: 0.2em;
}
.meta-inputs li {
padding: 0.25em 0.6em;
border: 1px solid var(--border);
background: rgba(24, 24, 37, 0.6);
}
.meta-inputs label {
display: flex;
align-items: baseline;
gap: 0.5em;
cursor: pointer;
font-size: 0.9em;
}
.meta-input-name { color: var(--amber); font-weight: bold; }
.meta-input-rev { color: var(--muted); }
.meta-input-ts { color: var(--muted); font-size: 0.85em; }
.meta-input-url {
color: var(--muted);
font-size: 0.85em;
margin-left: auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Bulk select-all / -none control above the meta-inputs tree (#275). */
.meta-inputs-bulk {
margin: 0 0 0.5em;
font-size: 0.8em;
color: var(--muted);
}
.meta-bulk-btn {
font: inherit;
font-size: 1em;
background: transparent;
border: 1px solid var(--purple-dim);
color: var(--cyan);
padding: 0.1em 0.6em;
margin-right: 0.2em;
cursor: pointer;
}
.meta-bulk-btn:hover {
border-color: var(--cyan);
text-shadow: 0 0 6px currentColor;
}
/* Tree twig glyph prefixing a nested (sub-)input row (#275). */
.meta-input-twig {
color: var(--purple-dim);
margin-right: 0.1em;
}
.btn-meta-update {
background: rgba(203, 166, 247, 0.12);
border: 1px solid var(--purple);
color: var(--purple);
text-shadow: 0 0 4px currentColor;
padding: 0.3em 1em;
font: inherit;
font-size: 0.85em;
letter-spacing: 0.08em;
cursor: pointer;
transition: box-shadow 0.15s ease, background 0.15s ease;
}
.btn-meta-update:hover:not([disabled]) {
background: rgba(203, 166, 247, 0.22);
box-shadow: 0 0 10px -2px currentColor;
}
.btn-meta-update[disabled] {
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 (issue #259). */
.meta-update-running {
margin: 0 0 0.7em;
padding: 0.4em 0.7em;
border: 1px solid var(--purple);
background: rgba(203, 166, 247, 0.12);
color: var(--purple);
font-size: 0.85em;
animation: badge-pulse 1.6s ease-in-out infinite;
}
/* ─── rebuild queue panel ──────────────────────────────────────────────── */
.rebuild-queue {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.2em;
}
.rebuild-queue-entry {
padding: 0.3em 0.6em;
border: 1px solid var(--border);
background: rgba(24, 24, 37, 0.6);
font-size: 0.9em;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.4em;
}
.rebuild-queue-entry.rqe-child { margin-left: 1.6em; border-color: var(--purple-dim); }
.rebuild-queue-entry.rqe-running {
border-color: var(--purple);
background: rgba(203, 166, 247, 0.12);
animation: badge-pulse 1.6s ease-in-out infinite;
}
.rebuild-queue-entry.rqe-failed { border-color: var(--red); color: var(--red); }
.rebuild-queue-entry.rqe-cancelled { opacity: 0.6; }
.rebuild-queue-entry.rqe-done { opacity: 0.7; color: var(--green); }
.rqe-state { font-weight: bold; min-width: 1.2em; text-align: center; }
.rqe-kind { color: var(--cyan); }
.rqe-agent { color: var(--amber); font-weight: bold; }
.rqe-source {
font-size: 0.75em;
padding: 0.05em 0.45em;
border-radius: 0.7em;
border: 1px solid var(--border);
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.rqe-source-manual { color: var(--cyan); border-color: var(--cyan); }
.rqe-source-meta_update { color: var(--purple); border-color: var(--purple); }
.rqe-source-auto_update { color: var(--muted); }
.rqe-source-crash_recover { color: var(--amber); border-color: var(--amber); }
.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; }
/* #437: in-flight step indicator on running queue entries — sub-line
below the main row, indented under the state glyph + kind. Cyan
keeps it visually grouped with the running spinner instead of
blending into the muted reason/timing chips. flex-basis: 100% so
it always wraps to its own line. */
.rqe-step {
flex-basis: 100%;
margin: 0.1em 0 0 1.8em;
color: var(--cyan);
font-size: 0.85em;
}
.rqe-error {
flex-basis: 100%;
margin: 0.3em 0 0;
padding: 0.3em 0.5em;
background: rgba(243, 139, 168, 0.1);
border-left: 2px solid var(--red);
color: var(--red);
font-size: 0.8em;
white-space: pre-wrap;
}
/* #575: cancel-X for queued rebuild_queue entries. Quiet by default
(sits at the far right via margin-left: auto), lights red on hover.
Renders only when `state === 'queued'` per renderQueueEntry's
guard — Running / terminal rows don't get the affordance. Mirrors
the side-panel-close glyph shape (✗) without the full B6N-style
`btn-deny` width so it stays unobtrusive in a list row. */
.rqe-cancel {
margin-left: auto;
}
.rqe-cancel-btn {
background: transparent;
border: 1px solid var(--purple-dim);
color: var(--muted);
border-radius: 999px;
width: 1.6em;
height: 1.6em;
font-size: 0.85em;
line-height: 1;
padding: 0;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.rqe-cancel-btn:hover:not(:disabled),
.rqe-cancel-btn:focus-visible {
color: var(--red);
border-color: var(--red);
box-shadow: 0 0 8px -2px var(--red);
outline: none;
}
.rqe-cancel-btn:disabled {
opacity: 0.5;
cursor: default;
}
.history-note {
margin-left: 1.8em;
margin-top: 0.2em;
color: var(--muted);
font-size: 0.85em;
white-space: pre-wrap;
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; }
.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;
}
/* #474: 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;
font-size: 1em;
background: var(--bg-elev);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.4em 0.6em;
flex: 1;
}
.spawnform input::placeholder { color: var(--muted); }
.spawnform input:focus { outline: 1px solid var(--purple); }
.role-pending { color: var(--amber); border-color: var(--amber); }
.btn-inline {
font-family: inherit;
background: transparent;
cursor: pointer;
margin-left: 0.4em;
}
.btn-inline:hover { background: rgba(255, 184, 77, 0.1); }
.kind {
display: inline-block;
margin-left: 0.4em;
padding: 0.05em 0.5em;
border: 1px solid var(--purple-dim);
color: var(--purple-dim);
border-radius: 2px;
font-size: 0.75em;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.kind-spawn { color: var(--amber); border-color: var(--amber); }
.spinner {
display: inline-block;
animation: spin 1s linear infinite;
color: var(--amber);
}
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.talkform {
display: flex;
gap: 0.6em;
align-items: stretch;
margin-top: 0.5em;
}
.talkform select, .talkform input {
font-family: inherit;
font-size: 1em;
background: var(--bg-elev);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.4em 0.6em;
}
.talkform select { color: var(--amber); }
.talkform input { flex: 1; }
.talkform input::placeholder { color: var(--muted); }
.talkform input:focus, .talkform select:focus { outline: 1px solid var(--purple); }
details { margin-top: 0.5em; }
summary {
cursor: pointer;
color: var(--muted);
font-size: 0.85em;
text-transform: uppercase;
letter-spacing: 0.1em;
}
summary:hover { color: var(--purple); }
.diff {
background: var(--bg-elev);
border: 1px solid var(--border);
padding: 0.8em;
margin-top: 0.4em;
overflow-x: auto;
font-size: 0.85em;
line-height: 1.4;
color: var(--muted);
white-space: pre;
}
.diff span { display: block; }
.diff .diff-add { color: var(--green); }
.diff .diff-del { color: var(--red); }
.diff .diff-hunk { color: var(--cyan); }
.diff .diff-file { color: var(--purple); font-weight: bold; }
.diff .diff-ctx { color: var(--fg); }
.questions {
background: var(--bg-elev);
border: 1px solid var(--amber);
box-shadow: 0 0 12px -4px var(--amber);
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;
margin: 0;
}
.reminder-row {
padding: 0.4em 0;
border-bottom: 1px solid var(--border);
}
.reminder-row:last-child { border-bottom: 0; }
.reminder-head { font-size: 0.9em; }
.reminder-body {
color: var(--fg);
white-space: pre-wrap;
word-break: break-word;
margin: 0.3em 0;
}
.reminder-row.reminder-failed {
border-left: 2px solid var(--red, #f38ba8);
padding-left: 0.5em;
}
.reminder-error {
color: var(--red, #f38ba8);
background: rgba(243, 139, 168, 0.06);
border: 1px solid rgba(243, 139, 168, 0.25);
padding: 0.3em 0.5em;
font-size: 0.85em;
white-space: pre-wrap;
word-break: break-word;
margin: 0.2em 0;
}
.reminder-actions {
display: flex;
gap: 0.4em;
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;
gap: 0.3em;
margin-bottom: 0.5em;
}
.q-filter-chip {
background: var(--bg);
color: var(--muted);
border: 1px solid var(--border);
border-radius: 999px;
padding: 0.15em 0.7em;
font: inherit;
font-size: 0.85em;
cursor: pointer;
}
.q-filter-chip:hover { color: var(--fg); }
.q-filter-chip.active {
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;
border-bottom: 1px solid var(--border);
}
.questions li.question:last-child { border-bottom: 0; }
.questions .q-head { font-size: 0.9em; }
.questions .q-ttl {
color: var(--amber);
margin-left: 0.4em;
font-size: 0.95em;
letter-spacing: 0.05em;
}
.questions .q-body {
color: var(--fg);
margin: 0.3em 0;
white-space: pre-wrap;
word-break: break-word;
}
.qform {
display: flex;
flex-direction: column;
gap: 0.5em;
margin-top: 0.4em;
}
.qform .q-options {
display: flex;
flex-direction: column;
gap: 0.25em;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.4em 0.6em;
}
.qform .q-option label { cursor: pointer; user-select: none; }
.qform .q-option input { margin-right: 0.4em; accent-color: var(--amber); }
.qform .q-free { display: flex; }
.qform .q-free textarea {
flex: 1;
font-family: inherit;
font-size: 1em;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.4em 0.6em;
resize: vertical;
line-height: 1.4;
}
.qform .q-free textarea::placeholder { color: var(--muted); }
.qform .q-free textarea:focus { outline: 1px solid var(--amber); }
.qform button { align-self: flex-start; }
.qform-cancel { margin-top: 0.3em; }
.q-history {
margin-top: 0.8em;
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.4em 0.7em;
}
.q-history summary { cursor: pointer; color: var(--muted); font-size: 0.9em; user-select: none; }
.questions-answered {
border: none;
box-shadow: none;
animation: none;
padding: 0;
margin-top: 0.5em;
}
.question-answered { opacity: 0.7; }
.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;
/* #450: no max-height cap — let the inbox grow to fill the
side-panel-body which already scrolls (`overflow: auto`). The
pre-#450 24em cap clamped the list well short of the available
panel height even on tall viewports. */
}
.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 {
/* #485: body takes a full flex line of its own beneath the
metadata chips (ts / arrow / from / sep / to). Previously the
body sat inline with `flex: 1 1 0`, eating whatever the chips
left — which on a long timestamp + agent names + arrows meant
the body started ~30ch in and wrapped awkwardly. Pushing
`flex-basis: 100%` forces the body to wrap to its own line in
the existing `flex-wrap: wrap` row, where it can use the full
width down to the row's content edge.
`min-width: 0` still applies so `word-break: break-word`
actually kicks in instead of forcing the row wider than its
container. */
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;
color: var(--muted);
font-size: 0.9em;
}
footer a { color: var(--purple); }
/* Slug banner now lives at the page footer (#389 follow-up) — give
it a slim top margin so it doesn't crash into the prior content,
and 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;
/* #451: width is a CSS variable so the drag handle (added by
Panel.bind) can update it live, and so localStorage-persisted
widths apply on first paint. Default min(760px, 94vw) preserves
the pre-#451 behaviour for operators who never drag. JS sets
`--side-panel-w` via inline style on the drawer; persistence
lives in localStorage (key `hyperhive:side-panel-width`),
replayed onto the var by `Panel.applyStoredWidth` at bind. */
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); }
/* #451: drag-to-resize handle on the drawer's left edge. The handle
itself is 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 (#369) ───────────────────────────
The all-agents chat surface lives on its own page so it can claim
full-viewport vibec0re styling (operator @ #369#issuecomment-3437).
Same shape as the per-agent live page (#362): frosted-glass header
at top, frosted composer docked at bottom, terminal scrolls
behind both. Operator inbox lives behind a header pill that opens
the side-panel flyout — preserves the "inbox + chat in one view"
ergonomics without stealing terminal real estate. */
:root {
/* Approximate height of the flow chrome (tabbar + dashboard-chrome
padding). The banner-thin slug used to live in the chrome and
padded this up to 4.7em; with the slug moved out (#389 follow-up:
slug lives at the page footer on /, simply omitted on /flow.html
since there's no normal-flow footer position in the full-viewport
terminal), the chrome is just tabs now and shrinks accordingly. */
--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 (#383): 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);
}
/* Legacy `.flow-title` / `.flow-hint` / `.flow-back` rules were
removed in #383 — the flow page now uses the shared chrome with
the dashboard tab strip, no need for FL0W-specific title/hint
elements. The `.notif-row` styling lives under the shared
`.tabbar #notif-row` selector earlier in the file. */
/* 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 pills (#362). */
.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-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.
z-index escapes the stacking context the .terminal-wrap's
backdrop-filter creates (issue #375) — tabs.js anchors the pill
on .flow-main now (not .terminal-wrap), so this z-index reaches
the root stacking context and properly floats above the
composer at z-index 30. */
.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; }
/* ─── scheduled prompts tab (#459) ─────────────────────────────────────
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. */
.schedule-edit-form {
display: flex;
flex-direction: column;
gap: 0.6em;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.8em 1em;
margin-bottom: 0.5em;
}
/* #474 — 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;
border-left: 2px solid var(--yellow, #f9e2af);
}
.schedule-edit-targets-note {
color: var(--muted);
font-style: italic;
font-size: 0.85em;
margin: 0.2em 0 0 0;
}
.schedule-field {
display: flex;
flex-direction: column;
gap: 0.2em;
}
.schedule-field-label {
color: var(--muted);
font-size: 0.85em;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.schedule-field input,
.schedule-field textarea {
font-family: inherit;
font-size: inherit;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.35em 0.5em;
}
.schedule-field textarea { resize: vertical; min-height: 4em; }
.schedule-targets {
display: flex;
flex-wrap: wrap;
gap: 0.4em;
}
.schedule-target-chip {
display: inline-flex;
align-items: center;
gap: 0.4em;
padding: 0.2em 0.6em;
border: 1px solid var(--border);
border-radius: 999px;
font-size: 0.85em;
cursor: pointer;
background: rgba(24, 24, 37, 0.4);
}
.schedule-target-chip:hover { border-color: var(--purple-dim); }
.schedule-target-chip:has(input:checked) {
border-color: var(--purple);
color: var(--purple);
background: rgba(203, 166, 247, 0.08);
}
.schedule-target-chip input { margin: 0; }
.schedule-actions {
display: flex;
gap: 0.5em;
justify-content: flex-end;
}
/* Interval composer (#466). 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;
gap: 0.3em;
margin-top: 0.1em;
}
.btn-interval-preset {
color: var(--cyan);
border-color: var(--cyan);
font-size: 0.75em;
padding: 0.15em 0.55em;
letter-spacing: 0.05em;
}
.btn-interval-oneshot {
color: var(--muted);
border-color: var(--muted);
}
.schedule-interval-parts {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35em;
margin-top: 0.4em;
}
.schedule-interval-num {
width: 4em;
text-align: right;
font-variant-numeric: tabular-nums;
}
.schedule-interval-unit {
color: var(--muted);
font-size: 0.85em;
margin-right: 0.2em;
}
.schedule-interval-preview {
margin-top: 0.4em;
color: var(--cyan);
font-size: 0.9em;
font-variant-numeric: tabular-nums;
}
.schedule-interval-preview-oneshot { color: var(--muted); font-style: italic; }
/* #535 — schedules-as-table. One row per schedule, attribute columns
on the left, one ✓/✕ column per agent in the middle, actions
column on the right. Agent column headers tilt -45° so a stack of
short agent names fits in ~28px each. */
.schedules-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
}
.schedules-table thead th {
vertical-align: bottom;
padding: 0.3em 0.5em;
text-align: left;
color: var(--muted);
font-weight: normal;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.8em;
border-bottom: 1px solid var(--border);
}
.schedules-table-id { width: 3em; }
.schedules-table-body-th { min-width: 12em; }
.schedules-table-actions-th { width: 7em; }
/* Tilted agent column headers (#535). Header cell is narrow (~28px)
and tall (~90px); the inner <div> rotates -45° about its bottom-left
corner, with a translate to slide the text up alongside the cell
border. The inner <span> 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;
height: 95px;
padding: 0 !important;
vertical-align: bottom;
white-space: nowrap;
text-transform: none !important;
letter-spacing: 0 !important;
position: relative;
}
.schedules-table-agent-th > div {
position: absolute;
bottom: 4px;
left: 14px;
transform-origin: left bottom;
transform: rotate(-45deg);
font-family: ui-monospace, 'JetBrains Mono', monospace;
font-size: 0.9em;
color: var(--fg);
}
.schedules-table-agent-th > div > span {
display: inline-block;
padding: 0 4px 1px;
}
.schedules-table tbody td {
padding: 0.3em 0.5em;
border-top: 1px solid var(--border);
vertical-align: middle;
}
.schedules-table-row-cancelled td { opacity: 0.55; }
.schedules-table-body-cell {
max-width: 30em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: ui-monospace, 'JetBrains Mono', monospace;
font-size: 0.9em;
}
.schedules-table-check {
width: 28px;
text-align: center;
padding: 0.3em 0 !important;
}
.schedules-table-check-btn {
background: transparent;
border: 0;
color: var(--green);
cursor: pointer;
padding: 0.1em 0.3em;
font-family: inherit;
font-size: 1em;
}
.schedules-table-check-btn:hover:not(:disabled) {
color: var(--red);
}
.schedules-table-check-btn:disabled {
cursor: default;
}
.schedules-table-check-cancelled {
color: var(--muted);
font-size: 0.9em;
}
.schedules-table-actions {
text-align: right;
white-space: nowrap;
}
.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);
}
.btn-inline-small {
font-size: 0.7em;
padding: 0.1em 0.45em;
}
/* #564 — 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);
vertical-align: top;
padding: 0.4em 0.5em;
}
.schedules-table-create-cell {
padding: 0.35em 0.4em;
}
.schedules-table-inline-input,
.schedules-table-inline-textarea,
.schedules-table-inline-num {
background: var(--bg);
border: 1px solid var(--border);
color: var(--fg);
font-family: ui-monospace, 'JetBrains Mono', monospace;
font-size: 0.85em;
padding: 0.2em 0.4em;
border-radius: 2px;
}
.schedules-table-inline-input:focus,
.schedules-table-inline-textarea:focus,
.schedules-table-inline-num:focus {
border-color: var(--purple);
outline: none;
}
.schedules-table-inline-textarea {
display: block;
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;
}
.schedules-table-inline-desc {
display: block;
width: 100%;
margin-top: 0.25em;
}
.schedules-table-inline-datetime {
width: 100%;
font-size: 0.78em;
}
.schedules-table-inline-interval {
display: flex;
align-items: center;
gap: 0.1em;
white-space: nowrap;
}
.schedules-table-inline-num {
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;
}
.schedules-table-inline-num::-webkit-outer-spin-button,
.schedules-table-inline-num::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.schedules-table-inline-unit {
color: var(--muted);
font-size: 0.7em;
margin-right: 0.15em;
}
.schedules-table-inline-check {
width: 16px;
height: 16px;
cursor: pointer;
margin: 0;
}
.schedules-table-inline-check-lbl {
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;
padding: 0.15em 0.55em;
color: var(--green);
border-color: var(--green);
}
.schedules-table-create-reset {
font-size: 0.85em;
padding: 0.15em 0.45em;
color: var(--muted);
border-color: var(--border);
}
/* Selection bar (#443). 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 —
leaves the page footer's normal-flow position untouched. */
.selection-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 40;
display: flex;
align-items: center;
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);
border-top: 1px solid var(--purple);
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.4);
}
.selection-bar[hidden] { display: none; }
.selection-count {
color: var(--purple);
font-weight: bold;
letter-spacing: 0.05em;
}
.selection-names {
color: var(--muted);
font-size: 0.85em;
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selection-actions {
display: inline-flex;
flex-wrap: wrap;
gap: 0.4em;
align-items: center;
}
.selection-actions .btn {
font-size: 0.75em;
padding: 0.2em 0.7em;
}
.selection-clear {
color: var(--muted);
border-color: var(--purple-dim);
font-size: 0.75em;
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). */
body.dashboard-shell.has-selection { padding-bottom: 4.5em; }