Files
rustguac/static/rustguac.css
T
Jad Seifeddine 6fbf590c1d pages: one navigation on every page, with the caller's role baked into the served page
pages: bake the caller's role into the served page, version the logo

The header flashed on every navigation: gated links (Sessions, Reports,
Tokens, Admin) were hidden in the markup and shown after /api/me
answered, the logo was revalidated on each page load, and the nav
differed between pages, so switching tabs reflowed the header.

Serve each page in five variants, one per role level, that differ only
in a data-role-level attribute on <html>; optional auth now runs on the
page routes so a logged-in request gets its variant with the navigation
already right on first paint. The ETag is per variant. The logo joins
scripts and stylesheets in content-addressed, immutable caching.

Part of #254

pages: one navigation for every page, gated by role in CSS

Every page carries the same eight links in the same order, including
the Credentials entry (which opens the dialog on the connections page
and deep-links to it from elsewhere), with data-min-role on the gated
ones instead of inline display:none. rustguac.css shows them from the
data-role-level the server writes on <html>; the pages set that
attribute themselves from /api/me for API-key users, whose page GET
carries no credential. Admin-only controls follow the same gate, so
nothing pops in after the first paint. Recordings shows "host (Tenant)"
entry names as the host over a quieter tenant line.

Part of #254

tests: one navigation on every page, gated by data-role-level
(cherry picked from commit 5eed92c6ec)
2026-10-17 08:23:16 +11:00

871 lines
26 KiB
CSS

/* ============================================================
rustguac — shared operator-console stylesheet
- 8px baseline grid for chrome controls (px units).
- Body copy 18px, control heights 38/44/54px.
- CSS variable theming is driven by the theme picker; only
the palette tokens here are intended to be overridden.
============================================================ */
:root {
/* theme palette */
--primary: #e94560; --primary-hover: #c73652;
--accent: #5bc0be; --accent-hover: #4aa3a1;
--bg: #1a1a2e; --surface: #16213e; --input: #0f3460;
--text: #e0e0e0; --text-muted: #aaa; --border: #333;
--text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555;
--status-pending: #f0c040; --status-active: #5bc0be;
--status-completed: #888; --status-error: #e94560; --status-expired: #666;
--type-ssh-bg: #1b4332; --type-ssh-fg: #52b788;
--type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050;
--type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0;
--type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0;
--type-vdi-bg: #0e2a2a; --type-vdi-fg: #2dd4bf;
--hop-bg: #1b4332; --hop-fg: #52b788;
--bg-pattern: none;
/* spacing (4px unit) */
--s-1: 4px; --s-2: 8px; --s-3: 14px;
--s-4: 20px; --s-5: 28px; --s-6: 40px;
/* geometry */
--radius: 2px;
/* control heights */
--ctl-sm: 38px;
--ctl-md: 44px;
--ctl-lg: 54px;
/* type scale — fz-md == body */
--fz-xs: 14px;
--fz-sm: 16px;
--fz-md: 18px;
--fz-lg: 20px;
--fz-xl: 24px;
}
/* ── Reset / base ─────────────────────────────── */
body {
font-family: monospace;
background-color: var(--bg);
background-image: var(--bg-pattern);
background-attachment: fixed;
min-height: 100vh;
color: var(--text);
padding: var(--s-5) var(--s-6);
font-size: 18px;
line-height: 1.55;
margin: 0;
}
/* Narrow desktops (a 1710px MacBook display, 1600px monitors): a tighter
page gutter. Shared here so every page shifts together and switching
tabs does not snap the content sideways. */
@media (max-width: 1800px) {
body { padding: var(--s-4) var(--s-4); }
}
h1 { color: var(--primary); margin: 0; font-size: 30px; letter-spacing: 0.02em; font-weight: bold; }
h2 { color: var(--primary); font-size: var(--fz-lg); margin-top: var(--s-5); letter-spacing: 0.02em; }
h3 { color: var(--text); font-size: var(--fz-md); margin-top: var(--s-4); letter-spacing: 0.02em; }
a { color: var(--accent); }
code { background: var(--surface); padding: 2px var(--s-1); border-radius: var(--radius); font-size: 0.9em; }
.brand-row { display: flex; align-items: center; gap: var(--s-3); }
.brand-row #site-logo { height: 36px; width: auto; display: block; }
/* ── Navigation ─────────────────────────────── */
nav {
margin-top: var(--s-4);
margin-bottom: var(--s-5);
padding-bottom: var(--s-3);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: var(--s-5);
font-size: var(--fz-md);
}
nav a {
text-decoration: none;
color: var(--text-muted);
padding: var(--s-1) 0;
margin: 0;
position: relative;
line-height: 1.4;
}
nav a:hover { color: var(--text); text-decoration: none; }
nav a.active { color: var(--primary); font-weight: bold; }
nav a.active::after {
content: '';
position: absolute;
left: 0; right: 0;
bottom: calc(var(--s-3) * -1 - 1px);
height: 2px;
background: var(--primary);
}
#user-menu-wrapper {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: var(--s-3);
position: relative;
float: none;
}
#logout-item {
color: var(--text-muted);
font-size: var(--fz-md);
text-decoration: none;
padding: var(--s-1) 0;
line-height: 1.4;
margin: 0;
}
#logout-item:hover { color: var(--primary); }
#user-menu-btn {
cursor: pointer;
color: var(--text-muted);
font-size: var(--fz-md);
display: inline-flex;
align-items: center;
gap: var(--s-1);
padding: 0 var(--s-2);
height: var(--ctl-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
line-height: 1;
box-sizing: border-box;
transition: color .12s, border-color .12s;
}
#user-menu-btn:hover { color: var(--text); border-color: var(--text-muted); }
#user-menu {
display: none;
position: absolute;
right: 0;
top: calc(var(--ctl-sm) + var(--s-2));
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
min-width: 260px;
z-index: 50;
padding: var(--s-1) 0;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.um-section-label {
padding: var(--s-2) var(--s-3) var(--s-1);
font-size: var(--fz-xs);
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.um-item {
padding: var(--s-2) var(--s-3);
cursor: pointer;
display: flex;
align-items: center;
gap: var(--s-2);
font-size: var(--fz-sm);
color: var(--text);
}
.um-item:hover { background: var(--input); }
.um-item.active { color: var(--accent); }
.um-theme-info { display: flex; flex-direction: column; }
.um-theme-name { font-weight: bold; }
.um-theme-desc { font-size: var(--fz-xs); color: var(--text-dim); }
.um-swatch {
display: inline-block;
width: 14px; height: 14px;
border-radius: 50%;
border: 1px solid var(--border);
flex-shrink: 0;
}
.um-divider { height: 1px; background: var(--border); margin: var(--s-1) 0; }
.um-logout { color: var(--text-muted); }
.um-logout:hover { color: var(--primary); background: var(--input); }
/* ── Buttons (unified ladder) ─────────────────────────────── */
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--s-1);
height: var(--ctl-md);
padding: 0 var(--s-3);
background: var(--primary);
color: var(--text-on-primary);
border: 1px solid var(--primary);
border-radius: var(--radius);
font-family: monospace;
font-size: var(--fz-md);
font-weight: bold;
line-height: 1;
cursor: pointer;
white-space: nowrap;
transition: background .12s, border-color .12s, color .12s;
box-sizing: border-box;
}
button:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
button:disabled {
background: var(--btn-disabled);
border-color: var(--btn-disabled);
color: var(--text-dim);
cursor: default;
}
button.btn-primary {
background: var(--primary);
border-color: var(--primary);
color: var(--text-on-primary);
}
.btn-connect {
background: var(--accent);
border-color: var(--accent);
color: var(--bg);
height: var(--ctl-sm);
padding: 0 var(--s-4);
font-size: var(--fz-sm);
min-width: 130px;
}
.btn-connect:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-add {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
font-weight: normal;
height: var(--ctl-sm);
padding: 0 var(--s-3);
font-size: var(--fz-sm);
}
.btn-add:hover {
background: var(--surface);
border-color: var(--accent);
color: var(--accent);
}
.btn-small {
background: transparent;
border: 1px solid transparent;
color: var(--text-muted);
font-weight: normal;
height: var(--ctl-sm);
padding: 0 var(--s-2);
font-size: var(--fz-sm);
font-family: monospace;
cursor: pointer;
border-radius: var(--radius);
}
.btn-small:hover {
color: var(--primary);
background: transparent;
border-color: var(--border);
text-decoration: none;
}
.btn-action { color: var(--accent); }
.btn-action:hover { color: var(--accent-hover); }
/* ── Form controls (base) ─────────────────────────────── */
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="date"],
input[type="search"],
select,
textarea {
background: var(--input);
border: 1px solid var(--border);
color: var(--text);
font-family: monospace;
font-size: var(--fz-md);
padding: 0 var(--s-2);
height: var(--ctl-md);
border-radius: var(--radius);
box-sizing: border-box;
line-height: 1;
}
textarea { height: auto; padding: var(--s-2); line-height: 1.5; }
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.18);
}
label {
display: block;
margin-top: var(--s-3);
color: var(--text-muted);
font-size: var(--fz-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ── Generic tables ─────────────────────────────── */
table {
border-collapse: collapse;
width: 100%;
margin-top: var(--s-3);
border: 1px solid var(--border);
}
th, td {
text-align: left;
padding: var(--s-3) var(--s-4);
border-bottom: 1px solid var(--border);
font-size: var(--fz-md);
vertical-align: middle;
}
th {
color: var(--text-muted);
font-size: var(--fz-sm);
font-weight: normal;
text-transform: uppercase;
letter-spacing: 0.12em;
background: var(--surface);
line-height: 1.6;
}
tbody tr:nth-child(even) { background: rgba(255,255,255,0.02); }
tbody tr:hover { background: var(--surface); }
tbody tr:last-child td { border-bottom: none; }
/* ── Status colors (session/token lifecycle) ─────────────── */
.status-pending { color: var(--status-pending); }
.status-active { color: var(--status-active); }
.status-completed { color: var(--status-completed); }
.status-error { color: var(--status-error); }
.status-expired { color: var(--status-expired); }
.status-disabled { color: var(--primary); }
/* ── Session type badges ─────────────────────────────── */
.type-badge {
display: inline-block;
padding: 3px var(--s-2);
border-radius: var(--radius);
font-size: var(--fz-sm);
font-weight: bold;
letter-spacing: 0.12em;
text-transform: uppercase;
min-width: 52px;
text-align: center;
line-height: 1.5;
}
.type-ssh { background: var(--type-ssh-bg); color: var(--type-ssh-fg); }
.type-rdp { background: var(--type-rdp-bg); color: var(--type-rdp-fg); }
.type-vnc { background: var(--type-vnc-bg); color: var(--type-vnc-fg); }
.type-web { background: var(--type-web-bg); color: var(--type-web-fg); }
.type-vdi { background: var(--type-vdi-bg); color: var(--type-vdi-fg); }
/* ── Section heading strip (used above grids/lists) ──────── */
.section-head {
display: flex;
align-items: baseline;
gap: var(--s-2);
margin-bottom: var(--s-3);
padding-bottom: var(--s-2);
border-bottom: 1px solid var(--border);
}
.section-head strong {
color: var(--accent);
font-size: var(--fz-sm);
text-transform: uppercase;
letter-spacing: 0.14em;
font-weight: bold;
}
.section-count {
color: var(--text-muted);
font-size: var(--fz-sm);
font-weight: normal;
letter-spacing: 0.04em;
}
/* ── Empty / error messages ─────────────────────────────── */
.empty { color: var(--text-dim); margin: var(--s-5) 0; font-size: var(--fz-md); }
#global-error, #error { color: var(--primary); margin-top: var(--s-2); white-space: pre-wrap; font-size: var(--fz-md); }
.no-vault { color: var(--text-muted); margin: var(--s-5) 0; }
.empty-state {
border: 1px dashed var(--border);
border-radius: var(--radius);
padding: var(--s-6) var(--s-5);
text-align: center;
margin: var(--s-5) 0;
}
.empty-state h3 {
color: var(--accent);
margin: 0 0 var(--s-2);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: var(--fz-lg);
}
.empty-state p { color: var(--text-muted); margin: 0 0 var(--s-4); }
.empty-state button { height: var(--ctl-lg); padding: 0 var(--s-4); font-size: var(--fz-md); }
/* ── Modal ─────────────────────────────── */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.75);
z-index: 100;
justify-content: center;
align-items: center;
backdrop-filter: blur(2px);
}
.modal-overlay.active { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--s-5);
min-width: 440px;
max-width: 600px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 16px 48px rgba(0,0,0,0.55);
}
.modal h3 {
color: var(--primary);
margin: 0 0 var(--s-3);
padding-bottom: var(--s-2);
border-bottom: 1px solid var(--border);
font-size: var(--fz-lg);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.modal label {
display: block;
margin-top: var(--s-3);
color: var(--text-muted);
font-size: var(--fz-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.modal input, .modal select, .modal textarea {
display: block;
width: 100%;
margin-top: var(--s-1);
background: var(--input);
border: 1px solid var(--border);
color: var(--text);
font-family: monospace;
font-size: var(--fz-md);
padding: 0 var(--s-2);
height: var(--ctl-md);
border-radius: var(--radius);
box-sizing: border-box;
text-transform: none;
letter-spacing: 0;
}
.modal textarea { height: auto; padding: var(--s-2); }
/* Checkboxes / radios live inside labels and shouldn't take the full
44px input height — keep native size, inline, next to the label text. */
.modal input[type="checkbox"],
.modal input[type="radio"] {
display: inline-block;
width: auto;
height: auto;
padding: 0;
margin: 0 var(--s-1) 0 0;
vertical-align: middle;
box-shadow: none;
}
.modal label:has(> input[type="checkbox"]),
.modal label:has(> input[type="radio"]) {
text-transform: none;
letter-spacing: 0;
font-size: var(--fz-sm);
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
gap: var(--s-2);
}
.modal input:focus, .modal select:focus, .modal textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.18);
}
.modal-actions {
margin-top: var(--s-4);
padding-top: var(--s-3);
border-top: 1px solid var(--border);
display: flex;
gap: var(--s-2);
justify-content: flex-end;
}
.modal-actions .btn-cancel {
background: transparent;
color: var(--text-muted);
border-color: var(--border);
font-weight: normal;
}
.modal-actions .btn-cancel:hover {
background: var(--surface);
border-color: var(--text-muted);
color: var(--text);
}
/* My Credentials modal: pin the header (title/description/filter) and the
Save/Close footer, and scroll only the credential list, so the modal stays
usable no matter how many credential variables exist. Scoped by #id so the
generic `.modal { overflow-y:auto }` is overridden only here. */
#my-creds-modal .modal {
display: flex;
flex-direction: column;
overflow: hidden;
}
#my-creds-modal #my-creds-list {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
/* room so focus rings / scrollbar don't clip against the padding edge */
margin: 0 calc(var(--s-2) * -1);
padding: 0 var(--s-2);
}
#my-creds-search {
/* filter box lives in the pinned header */
margin: var(--s-1) 0 var(--s-2);
}
.cred-group {
border-top: 1px solid var(--border);
}
.cred-group-head {
display: flex;
align-items: center;
gap: 0.4em;
cursor: pointer;
user-select: none;
color: var(--accent);
font-weight: bold;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.55em 0 0.35em;
}
.cred-group-caret {
display: inline-block;
width: 1em;
font-size: 1.15em;
line-height: 1;
transition: transform 0.12s ease;
}
.cred-group.collapsed .cred-group-caret {
transform: rotate(-90deg);
}
.cred-group.collapsed .cred-group-body {
display: none;
}
.cred-group-count {
margin-left: auto;
color: var(--text-dim);
font-weight: normal;
font-size: 0.85em;
}
.field-hint {
font-size: var(--fz-xs);
color: var(--text-dim);
margin-top: var(--s-1);
text-transform: none;
letter-spacing: 0;
}
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: 2.4em; }
.pw-toggle {
position: absolute;
right: var(--s-1);
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: var(--fz-sm);
padding: var(--s-1);
line-height: 1;
width: auto;
height: auto;
font-weight: normal;
}
.pw-toggle:hover {
color: var(--text);
background: none;
border-color: transparent;
}
/* ── Card (generic surface block used for status panels, cards) ── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--s-4);
}
/* ── Status-card grid (used on admin dashboard) ──────────────── */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--s-3);
margin: var(--s-3) 0 var(--s-5);
}
.status-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--s-3) var(--s-4);
}
.status-card .label {
color: var(--text-muted);
font-size: var(--fz-xs);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.status-card .value {
color: var(--accent);
font-size: var(--fz-xl);
font-weight: bold;
margin-top: var(--s-1);
line-height: 1.2;
}
.status-card .detail {
color: var(--text-dim);
font-size: var(--fz-xs);
margin-top: var(--s-1);
}
/* ── Small chips for feature flags, tags ──────────────────────── */
.feat-tag {
display: inline-block;
padding: 2px var(--s-2);
border-radius: var(--radius);
font-size: var(--fz-xs);
margin-right: var(--s-1);
margin-bottom: var(--s-1);
letter-spacing: 0.06em;
}
.feat-on { background: var(--type-ssh-bg); color: var(--type-ssh-fg); }
.feat-off { background: var(--border); color: var(--text-dim); }
/* ── Form card (used by sessions form, onboarding) ──────────── */
form.card,
.add-form {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--s-4) var(--s-5);
margin-top: var(--s-3);
}
/* ── Hop cards (SSH jump host editor — used by addressbook + sessions) ── */
.hop-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: var(--s-2);
}
.hop-card-header {
display: flex; align-items: center; gap: var(--s-2);
padding: var(--s-2) var(--s-3);
cursor: pointer;
font-size: var(--fz-md);
}
.hop-card-header:hover { background: var(--surface); }
.hop-card-header .hop-num { color: var(--accent); font-weight: bold; min-width: 2em; }
.hop-card-header .hop-hostname { flex: 1; color: var(--text); }
.hop-card-header .hop-hostname input {
display: inline; width: 100%; height: var(--ctl-sm);
padding: 0 var(--s-2); font-size: var(--fz-md);
background: var(--input); border: 1px solid var(--border); color: var(--text);
font-family: monospace; border-radius: var(--radius); box-sizing: border-box;
}
.hop-card-header .hop-remove {
color: var(--primary); cursor: pointer; font-size: var(--fz-sm);
background: none; border: 1px solid transparent;
padding: 0 var(--s-2); font-family: monospace; font-weight: normal;
height: var(--ctl-sm);
}
.hop-card-header .hop-remove:hover {
text-decoration: underline;
background: none; border-color: transparent;
}
.hop-card-body { padding: var(--s-1) var(--s-3) var(--s-3) 44px; display: none; }
.hop-card-body.expanded { display: block; }
.hop-card-body label { margin-top: var(--s-2); }
.hop-card-body input, .hop-card-body textarea {
display: block; width: 100%;
padding: 0 var(--s-2); height: var(--ctl-md); margin-top: var(--s-1);
background: var(--input); border: 1px solid var(--border); color: var(--text);
font-family: monospace; font-size: var(--fz-md);
border-radius: var(--radius); box-sizing: border-box;
}
.hop-card-body textarea { height: auto; padding: var(--s-2); }
/* ── Token reveal (one-time token display, admin/tokens pages) ── */
.token-reveal {
background: var(--surface);
padding: var(--s-4);
border-radius: var(--radius);
margin-top: var(--s-3);
border: 1px solid var(--accent);
}
.token-reveal code {
background: var(--input);
padding: var(--s-2) var(--s-3);
border-radius: var(--radius);
font-size: var(--fz-sm);
word-break: break-all;
display: block;
margin: var(--s-2) 0;
user-select: all;
}
.token-reveal .warning {
color: var(--primary);
font-size: var(--fz-sm);
margin-top: var(--s-2);
}
/* ── Row states ─────────────────────────────── */
.disabled-row { opacity: 0.5; }
/* ── Status pill (used in reports / sessions grids) ──────────── */
.status-badge {
display: inline-block;
padding: 2px var(--s-2);
border-radius: var(--radius);
font-size: var(--fz-sm);
letter-spacing: 0.06em;
}
/* ── Pagination ─────────────────────────────── */
.pagination {
display: flex;
align-items: center;
gap: var(--s-3);
margin: var(--s-3) 0;
font-size: var(--fz-sm);
}
.pagination button {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
font-family: monospace;
font-size: var(--fz-sm);
height: var(--ctl-sm);
padding: 0 var(--s-3);
border-radius: var(--radius);
font-weight: normal;
}
.pagination button:hover { background: var(--surface); border-color: var(--accent); color: var(--accent); }
.pagination button:disabled {
color: var(--text-dim);
border-color: var(--border);
background: transparent;
}
.pagination .page-info { color: var(--text-muted); }
/* ── Summary cards (reports top-of-page KPI strip) ─────────── */
.summary-cards {
display: flex;
gap: var(--s-3);
margin: var(--s-4) 0;
flex-wrap: wrap;
}
.summary-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--s-3) var(--s-4);
flex: 1;
min-width: 180px;
}
.summary-card .card-label {
color: var(--text-muted);
font-size: var(--fz-xs);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.summary-card .card-value {
color: var(--accent);
font-size: var(--fz-xl);
font-weight: bold;
margin-top: var(--s-1);
line-height: 1.2;
}
/* ── Flow diagram (you → hop → target path) ─────────────────── */
.flow-diagram {
display: flex; align-items: center; gap: 0;
flex-wrap: wrap; margin-bottom: var(--s-3);
font-size: var(--fz-sm);
}
.flow-node {
display: inline-flex; align-items: center;
height: var(--ctl-sm); padding: 0 var(--s-2);
border-radius: var(--radius); white-space: nowrap;
}
.flow-node-you { background: var(--input); color: var(--text-muted); }
.flow-node-hop { background: var(--hop-bg); color: var(--hop-fg); }
.flow-node-target { background: var(--input); color: var(--accent); font-weight: bold; }
.flow-arrow { color: var(--text-muted); margin: 0 var(--s-1); }
/* ── Connections quick-find ─────────────────────────────────── */
.connections-search {
flex: 1 1 220px;
min-width: 180px;
max-width: 480px;
height: var(--ctl-md);
margin-left: var(--s-2);
margin-right: auto;
}
.connections-search:disabled {
opacity: 0.6;
cursor: progress;
}
.entries-table mark {
background: var(--accent);
color: var(--bg);
padding: 0 0.15em;
border-radius: 2px;
}
.search-folder-cell {
color: var(--text-muted);
font-size: var(--fz-sm);
}
.search-folder-cell .folder-scope {
color: var(--accent);
margin-right: 0.2em;
}
.search-open-folder {
color: var(--accent);
text-decoration: none;
font-size: var(--fz-sm);
white-space: nowrap;
}
.search-open-folder:hover { text-decoration: underline; }
.search-more {
margin-top: var(--s-2);
padding: var(--s-2) var(--s-3);
color: var(--text-muted);
font-size: var(--fz-sm);
font-style: italic;
text-align: center;
border-top: 1px dashed var(--border);
}
/* ── Role-gated navigation ──────────────────────
The server writes data-role-level (0 anonymous … 4 admin) on <html> for
the user it serves a page to, so gated links and admin-only controls are
right on the first paint: no fetch-then-show, no reflow, nothing popping
in after the header has drawn. A page reconciles the attribute from
/api/me for identities the page GET could not carry (API keys). */
nav a[data-min-role] { display: none; }
html[data-role-level="2"] nav a[data-min-role="operator"],
html[data-role-level="3"] nav a[data-min-role="operator"],
html[data-role-level="3"] nav a[data-min-role="poweruser"],
html[data-role-level="4"] nav a[data-min-role] { display: inline; }
html:not([data-role-level="4"]) .admin-only { display: none !important; }
/* ── Entry names ──
"host (Tenant)" display names read as the host over a quieter subline. */
.entry-name { display: block; color: var(--text); }
.entry-sub { display: block; font-size: var(--fz-xs); color: var(--text-dim); margin-top: 1px; letter-spacing: 0.02em; }