Files
rustguac/static/rustguac.css
T
Jad Seifeddine b8c5c0f9ee search: one global connection search, as a floating overlay on every page
The '/' search box leaves the connections folder header for a bigger,
centred box in the brand row, inline with the logo. Results open in a dim,
blurred overlay over an untouched folder view: a centred panel with the
summary, the Type and Folder facets and the results table (fixed columns,
one line per cell, folder shown by display name over its key, Connect, new
window and open-folder gathered into one actions cell). Focusing the box or
pressing / opens it and lists every connection the user can see; typing
narrows it; Escape from anywhere, a mousedown outside the box and panel, or
opening a folder closes it (an open facet menu takes the first Escape).

static/search.js is the single implementation, used on every page
including connections, which dropped ~16k of duplicated markup, CSS and JS;
the styles live once in rustguac.css so the results look identical
everywhere. The index is cached in sessionStorage and only re-fetched when
the address book's version changes, so moving between pages is instant; the
connections page keeps the folder-specific hooks it needs (rgSelectFolder
selects a folder in place, rgSearchIndex feeds the credentials banner) and
opening a folder from another page navigates to connections with
#open=scope/path. The session type is escaped before it becomes a badge
class, and the module stands down cleanly on a page with no brand row.

Connections also gets a more generous content gutter, the folder action
buttons move down to the filter row beside Type, the folder title is inset
to align with the table, and the Connect button keeps its size and spins
in place instead of growing to 'Connecting...'.

Part of #254

tests: the global search overlay on every page

search: keep page modals above the brand-row search box

The global search lifts the brand row to z-index 300 so the box is not
dimmed by its own results overlay (200), but the page modals sat at 100, so
the search input showed through the ad-hoc session dialog and could still
be focused. Modal overlays now sit at 1000, above the search chrome and
below the themed dialogs (4000) and the embedded session (5000). A spec
pins it on the ad-hoc and folder modals.

search: never on the login page, nor for anyone the server does not recognise

The global search was included on index.html, so the login page carried
the search box, and because the index was cached in sessionStorage it
kept answering with the whole address book after logout, or to the next
login in the same tab.

- index.html no longer loads search.js at all.
- search.js only renders for an operator or above: the role the server
  bakes on <html data-role-level> decides on first paint, and an API-key
  user (served the anonymous variant) is confirmed through /api/me first.
  Anonymous or viewer: nothing injected, cache swept.
- The cached index is keyed to the identity /api/me returns; a cache left
  by a different login is dropped rather than shown.
- A 401/403 from the index or version endpoints removes the box, the
  overlay and the cache; the logout link clears the cache before the
  login page shows.

e2e: login page has no box and never loads search.js; anonymous and
viewer callers get nothing and a stale cache is wiped; a different
login's cache is dropped; the same login keeps its cache across pages;
logout and expiry forget it.
2026-10-16 16:51:36 +07:00

1153 lines
39 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; }
/* The key glyph before Credentials renders as a large colour emoji; keep it to text height. */
nav .nav-key { font-size: 0.75em; vertical-align: 0.05em; margin-right: 0.1em; }
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); }
/* Connecting state: keep the button's size, hide the label, spin in place. */
.btn-connect.loading { color: transparent !important; position: relative; pointer-events: none; }
.btn-connect.loading::after {
content: ""; position: absolute; top: 50%; left: 50%;
width: 15px; height: 15px; margin: -8px 0 0 -8px;
border: 2px solid color-mix(in srgb, var(--bg) 35%, transparent);
border-top-color: var(--bg);
border-radius: 50%;
animation: rg-btn-spin 0.7s linear infinite;
}
@keyframes rg-btn-spin { to { transform: rotate(360deg); } }
.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); }
.type-proxmox { background: #e57000; color: #000; }
/* ── Section heading strip (used above grids/lists) ──────── */
.section-head {
display: flex;
align-items: center;
gap: var(--s-2);
min-height: var(--ctl-sm);
margin-bottom: var(--s-3);
padding-bottom: var(--s-2);
border-bottom: 1px solid var(--border);
}
/* A section's action button sits on the right of its heading band. */
.section-head .section-action { margin-left: auto; align-self: center; }
.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);
/* Above the brand row (300), which the global search lifts over its own
results overlay (200): a dialog must cover the search box too. */
z-index: 1000;
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 ─────────────────────────────────── */
.entries-table mark {
background: var(--accent);
color: var(--bg);
padding: 0 0.15em;
border-radius: 2px;
}
.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; }
/* ── Facet filters (filters.js) ──────────────────────────── */
.rg-filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--s-2);
}
.rg-filters-label {
color: var(--text-muted);
font-size: var(--fz-sm);
text-transform: uppercase;
letter-spacing: 0.12em;
}
.rg-facet { position: relative; }
.rg-facet[hidden] { display: none; }
.rg-facet-btn {
display: inline-flex;
align-items: center;
gap: var(--s-1);
height: var(--ctl-sm);
padding: 0 var(--s-2);
background: var(--input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-muted);
font-family: monospace;
font-size: var(--fz-sm);
cursor: pointer;
white-space: nowrap;
}
/* The generic button:hover paints the primary colour; these stay on the input tone. */
.rg-facet-btn:hover, .rg-facet-btn:focus, .rg-facet.open .rg-facet-btn { background: var(--input); border-color: var(--accent); color: var(--text); }
.rg-facet-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.rg-facet.active .rg-facet-btn { border-color: var(--accent); color: var(--accent); }
.rg-facet.active .rg-facet-btn:hover, .rg-facet.active .rg-facet-btn:focus { color: var(--accent); }
.rg-facet-n {
background: var(--accent);
color: var(--bg);
border-radius: 999px;
padding: 0 0.55em;
font-size: var(--fz-xs);
font-weight: bold;
line-height: 1.6;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
}
.rg-facet-n.one { background: transparent; color: var(--accent); padding: 0; font-weight: normal; font-size: var(--fz-sm); }
.rg-facet-n.one::before { content: "\00b7\00a0"; color: var(--text-dim); }
.rg-facet-n[hidden] { display: none; }
.rg-facet-caret { font-size: 0.7em; opacity: 0.6; margin-left: 1px; }
.rg-facet.open .rg-facet-caret { transform: rotate(180deg); }
.rg-facet-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 600;
min-width: 260px;
max-width: min(440px, calc(100vw - 32px));
padding: var(--s-2);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
}
.rg-facet-menu.right { left: auto; right: 0; }
.rg-facet-menu[hidden] { display: none; }
.rg-facet-tools-row { display: flex; align-items: center; gap: var(--s-1); margin-bottom: var(--s-2); }
.rg-facet-tools { flex: 1; min-width: 0; display: flex; }
.rg-facet-tools input[type="search"] { flex: 1; min-width: 0; height: var(--ctl-sm); font-size: var(--fz-sm); }
.rg-facet-tool {
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius);
color: var(--text-muted);
font-family: monospace;
font-size: var(--fz-xs);
height: var(--ctl-sm);
padding: 0 var(--s-2);
cursor: pointer;
}
.rg-facet-tool:hover, .rg-facet-tool:focus { background: transparent; color: var(--accent); border-color: var(--border); }
.rg-facet-list { max-height: 340px; overflow-y: auto; overflow-x: hidden; padding-right: var(--s-2); scrollbar-gutter: stable; }
.rg-facet-opt {
display: flex;
align-items: center;
gap: var(--s-2);
margin: 0;
padding: 4px var(--s-1);
border-radius: var(--radius);
color: var(--text);
font-size: var(--fz-sm);
font-weight: normal;
text-transform: none;
letter-spacing: normal;
cursor: pointer;
}
.rg-facet-opt:hover { background: var(--input); }
.rg-facet-opt.zero { opacity: 0.55; }
.rg-facet-opt input[type="checkbox"] { margin: 0; flex: none; accent-color: var(--accent); width: 15px; height: 15px; }
.rg-facet-val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rg-facet-val .type-badge { font-size: var(--fz-xs); min-width: 44px; padding: 1px var(--s-2); }
.rg-facet-count { color: var(--text-dim); font-size: var(--fz-xs); flex: none; }
.rg-facet-empty { color: var(--text-dim); font-size: var(--fz-sm); padding: var(--s-2) var(--s-1); }
.rg-filters-clear {
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius);
color: var(--text-muted);
font-family: monospace;
font-size: var(--fz-sm);
height: var(--ctl-sm);
padding: 0 var(--s-2);
cursor: pointer;
}
.rg-filters-clear:hover, .rg-filters-clear:focus { background: transparent; color: var(--primary); border-color: var(--border); }
.rg-filters-clear[hidden] { display: none; }
/* ── List tool rows (search box + facet filters above a table) ── */
.list-tools {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--s-2);
margin-bottom: var(--s-3);
}
.list-tools:empty { display: none; }
.list-tools .list-search { flex: 1 1 260px; min-width: 200px; max-width: 520px; height: var(--ctl-sm); font-size: var(--fz-sm); }
/* A page-specific action (New ad-hoc session, Export CSV) pinned to the right
of the shared search+filters row. */
.list-tools .list-action { margin-left: auto; }
/* ── Modal extras ─────────────────────────────────────── */
.modal .modal-hint { color: var(--text-muted); font-size: var(--fz-sm); margin: 0 0 var(--s-3); }
.modal.adhoc-modal { min-width: 520px; max-width: 640px; }
/* ── Global connection search (search.js) and the results overlay ──
Shared by every page and by the connections folder view, so the markup
is generated in one place (search.js) and styled in one place here. */
.brand-row { position: relative; }
.brand-row:has(.global-search) { z-index: 300; } /* lift the search above the overlay */
.global-search {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: min(640px, 42vw); min-width: 320px;
display: flex; align-items: center;
}
.global-search input[type="search"] {
width: 100%; height: 48px;
padding: 0 52px 0 46px;
font-size: var(--fz-lg);
background: var(--input); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); box-sizing: border-box; font-family: monospace;
transition: border-color 0.15s, box-shadow 0.15s;
}
.global-search input[type="search"]:focus {
outline: none; border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.global-search input[type="search"]:disabled { opacity: 0.6; cursor: progress; }
.global-search-icon {
position: absolute; left: 16px; top: 50%; transform: translateY(-50%) rotate(-45deg);
color: var(--text-dim); font-size: 22px; line-height: 1; pointer-events: none;
}
.global-search:focus-within .global-search-icon { color: var(--accent); }
.global-search-key {
position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
font-family: monospace; font-size: var(--fz-xs); color: var(--text-dim);
border: 1px solid var(--border); border-radius: 4px; padding: 1px 7px; line-height: 1.5;
pointer-events: none;
}
.global-search:focus-within .global-search-key, .global-search.has-value .global-search-key { display: none; }
@media (max-width: 1180px) {
.global-search { position: static; transform: none; margin-left: auto; width: 360px; min-width: 0; }
}
#search-overlay {
display: none;
position: fixed; inset: 0;
z-index: 200;
background: color-mix(in srgb, var(--bg) 62%, transparent);
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
padding: 86px var(--s-5) var(--s-5);
justify-content: center; align-items: flex-start;
}
#search-overlay.open { display: flex; }
#search-overlay-panel {
width: min(1680px, 100%);
max-height: calc(100vh - 110px);
display: flex; flex-direction: column;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
overflow: hidden;
animation: rg-search-in 140ms ease-out both;
}
@keyframes rg-search-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
#search-overlay-head {
display: flex; flex-direction: column; gap: var(--s-2);
padding: var(--s-4) var(--s-4) var(--s-3);
border-bottom: 1px solid var(--border);
flex: none;
}
#search-overlay-head .list-tools { margin: 0; }
.search-summary { color: var(--text-muted); font-size: var(--fz-sm); margin: 0; }
.search-summary strong { color: var(--text); font-weight: bold; }
#search-results { overflow-y: auto; overflow-x: hidden; padding: 0 var(--s-4) var(--s-3); }
/* The results table styles itself fully here (not via any page's inline
.entries-table), so it looks identical on every page. */
#search-results .entries-table { border-collapse: collapse; width: 100%; margin-top: 0; border: 0; }
#search-results .entries-table th,
#search-results .entries-table td {
padding: var(--s-2) var(--s-3);
text-align: left;
border-bottom: 1px solid var(--border);
font-size: var(--fz-sm);
vertical-align: middle;
}
#search-results .entries-table th {
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: var(--fz-xs);
font-weight: normal;
position: sticky; top: 0;
background: var(--surface);
z-index: 1;
}
#search-results .entries-table tbody tr:hover { background: var(--input); }
#search-results .entries-table tbody tr:last-child td { border-bottom: none; }
/* Override any page's own table rules so the results look the same everywhere. */
#search-results .entries-table tbody tr:nth-child(even) { background: transparent; }
#search-results .folder-cell .folder-scope {
display: inline; width: auto; height: auto;
font-size: inherit; color: var(--text-dim); cursor: default;
margin-right: var(--s-1);
}
#search-results .empty-state { padding: 2.5em 1em; text-align: center; }
/* Results table: fixed columns, one line per cell, actions on the right. */
.search-results { table-layout: fixed; }
.search-results .c-name { width: 24%; }
.search-results .c-type { width: 7%; }
.search-results .c-host { width: 18%; }
.search-results .c-user { width: 9%; }
.search-results .c-folder { width: 17%; }
.search-results .c-actions { width: 25%; }
.search-results td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
.search-results .entry-name, .search-results .entry-sub { display: block; overflow: hidden; text-overflow: ellipsis; }
.search-results .entry-sub { color: var(--text-muted); font-size: var(--fz-xs); }
.search-results .folder-cell .fname { display: block; overflow: hidden; text-overflow: ellipsis; }
.search-results .folder-cell .fkey { display: block; overflow: hidden; text-overflow: ellipsis; color: var(--text-dim); font-size: var(--fz-xs); letter-spacing: 0.04em; }
.search-results .folder-cell .folder-scope { margin-right: var(--s-1); }
.search-results .host-cell { color: var(--text-muted); }
.search-results .actions-cell { text-align: right; overflow: visible; text-overflow: clip; white-space: nowrap; }
.search-results .actions-cell .btn-connect { vertical-align: middle; }
.search-results .btn-connect-win {
margin-left: var(--s-1); min-width: 0; width: auto; flex: none;
padding: 0 var(--s-1); font-size: var(--fz-md); line-height: 1; vertical-align: middle;
}
.search-results .search-open-folder { margin-left: var(--s-3); white-space: nowrap; color: var(--accent); text-decoration: none; font-size: var(--fz-sm); }
.search-results .search-open-folder:hover { text-decoration: underline; }
.search-more { color: var(--text-muted); font-size: var(--fz-sm); padding: var(--s-3) 0; }
/* ── Embedded fullscreen session (search.js rgLaunchFullscreen) ──
The launcher tab goes fullscreen on the Connect click (the only moment a
gesture is available) and the session loads in a full-viewport iframe here,
so there is no second click. */
#rg-session-overlay { position: fixed; inset: 0; z-index: 5000; background: #000; }
#rg-session-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
#rg-session-spinner {
position: absolute; top: 50%; left: 50%; width: 34px; height: 34px; margin: -17px 0 0 -17px;
border: 3px solid rgba(255,255,255,0.25); border-top-color: var(--accent, #5bc0be);
border-radius: 50%; animation: rg-btn-spin 0.8s linear infinite;
}
#rg-session-exit {
position: absolute; top: 10px; right: 14px; z-index: 5001;
height: 32px; padding: 0 var(--s-3, 14px);
background: rgba(20,24,38,0.72); color: var(--text, #e0e0e0);
border: 1px solid var(--border, #333); border-radius: var(--radius, 2px);
font-family: monospace; font-size: var(--fz-sm, 16px); cursor: pointer;
opacity: 0.35; transition: opacity 0.15s;
}
#rg-session-exit:hover { opacity: 1; background: var(--surface, #16213e); border-color: var(--primary, #e94560); color: var(--text, #e0e0e0); }