shadow.enabled in housecarl.yaml loads Housecarl's ruleset so that it blocks, rejects and translates nothing: drops and rejects count and accept (an accept only ends Housecarl's chain), policies accept and log what they would drop, NAT only counts, MSS clamping and the flowtable are left out, and raw rules that do more than accept, drop or reject are left out with a warning. Comments mark what would happen ([would drop]). Filter chains run after the other firewall's (+10) or before (-10); NAT chains always first. Every change is checked by nftables in both forms. No probation; takeovers and closing connections are refused. The boot fallback filters nothing. internal/tablewatch puts the table back after a flush ruleset. Mode in status, healthz, whoami, the web UI, the log and the tracer; hc ruleset --enforcing. docs/shadow.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
567 lines
32 KiB
CSS
567 lines
32 KiB
CSS
/* Housecarl web UI. Plain CSS, light and dark themes (the same as Jarl's and
|
|
Steward's). */
|
|
:root {
|
|
--bg: #f6f7f9;
|
|
--panel: #ffffff;
|
|
--text: #1d2330;
|
|
--muted: #5f6b7d;
|
|
--hatch: rgba(95, 107, 125, 0.13);
|
|
--border: #d9dee6;
|
|
--accent: #2f5fb3;
|
|
--accent-text: #ffffff;
|
|
--accent-soft: #e5edfa;
|
|
--ok: #1e7a46;
|
|
--ok-soft: #e3f4ea;
|
|
--warn: #8a5a00;
|
|
--warn-soft: #fdf2d9;
|
|
--err: #b3261e;
|
|
--err-soft: #fbe7e5;
|
|
--info-soft: #eef1f6;
|
|
--code-bg: #f0f2f5;
|
|
--add: #e6f6ea;
|
|
--add-text: #135c2c;
|
|
--del: #fde8e7;
|
|
--del-text: #8c1d18;
|
|
--hunk: #e8eefb;
|
|
--shadow: 0 1px 2px rgba(20, 30, 50, 0.08);
|
|
--radius: 6px;
|
|
--vc-green: #3d8160; --vc-teal: #2f7c80; --vc-purple: #7a5ba8; --vc-pink: #a8557f;
|
|
--vc-orange: #a8652f; --vc-yellow: #8a7424; --vc-red: #b0534c; --vc-grey: #6b7280;
|
|
--mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
|
color-scheme: light;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--bg: #14171c;
|
|
--panel: #1c2027;
|
|
--text: #e3e7ee;
|
|
--muted: #9aa5b5;
|
|
--hatch: rgba(154, 165, 181, 0.12);
|
|
--border: #2f3540;
|
|
--accent: #6d9be8;
|
|
--accent-text: #0f1520;
|
|
--accent-soft: #23314a;
|
|
--ok: #59c287;
|
|
--ok-soft: #173326;
|
|
--warn: #e5b450;
|
|
--warn-soft: #3a2f15;
|
|
--err: #f08077;
|
|
--err-soft: #3c1f1d;
|
|
--info-soft: #232833;
|
|
--code-bg: #242933;
|
|
--add: #183a25;
|
|
--add-text: #9fe3b5;
|
|
--del: #43201e;
|
|
--del-text: #f7b2ac;
|
|
--hunk: #23314a;
|
|
--shadow: none;
|
|
--vc-green: #8fcfa9; --vc-teal: #87cdd0; --vc-purple: #bca6e6; --vc-pink: #e6a6c6;
|
|
--vc-orange: #ebbb8f; --vc-yellow: #e3d08e; --vc-red: #eba29c; --vc-grey: #aab2bf;
|
|
color-scheme: dark;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--bg: #14171c; --panel: #1c2027; --text: #e3e7ee; --muted: #9aa5b5; --border: #2f3540; --hatch: rgba(154, 165, 181, 0.12);
|
|
--accent: #6d9be8; --accent-text: #0f1520; --accent-soft: #23314a; --ok: #59c287; --ok-soft: #173326;
|
|
--warn: #e5b450; --warn-soft: #3a2f15; --err: #f08077; --err-soft: #3c1f1d; --info-soft: #232833;
|
|
--code-bg: #242933; --add: #183a25; --add-text: #9fe3b5; --del: #43201e; --del-text: #f7b2ac; --hunk: #23314a;
|
|
--shadow: none; color-scheme: dark;
|
|
--vc-green: #8fcfa9; --vc-teal: #87cdd0; --vc-purple: #bca6e6; --vc-pink: #e6a6c6;
|
|
--vc-orange: #ebbb8f; --vc-yellow: #e3d08e; --vc-red: #eba29c; --vc-grey: #aab2bf;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; }
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font: 14px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, sans-serif;
|
|
}
|
|
a { color: var(--accent); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
code, pre, .mono, textarea.mono, input.mono { font-family: var(--mono); font-size: 13px; }
|
|
pre { background: var(--code-bg); padding: 10px 12px; border-radius: var(--radius); overflow: auto; margin: 0; white-space: pre; }
|
|
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
h2 { font-size: 16px; margin: 18px 0 8px; }
|
|
h3 { font-size: 14px; margin: 14px 0 6px; }
|
|
.muted { color: var(--muted); }
|
|
.small { font-size: 12px; }
|
|
.nowrap { white-space: nowrap; }
|
|
.warn-text { color: var(--warn); }
|
|
.right { text-align: right; }
|
|
.hidden { display: none !important; }
|
|
|
|
/* Layout */
|
|
header.top {
|
|
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
|
background: var(--panel); border-bottom: 1px solid var(--border);
|
|
padding: 8px 16px; position: sticky; top: 0; z-index: 10;
|
|
}
|
|
header.top .brand { display: inline-flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; }
|
|
header.top .brand .logo { display: block; transition: transform 0.15s; }
|
|
header.top .brand .brand-name { display: block; color: var(--text); }
|
|
header.top .brand:hover { text-decoration: none; }
|
|
header.top .brand:hover .brand-name { color: var(--accent); }
|
|
header.top .brand:hover .logo { transform: scale(1.06); }
|
|
nav.main { display: flex; gap: 2px; flex-wrap: wrap; }
|
|
nav.main a { padding: 6px 10px; border-radius: var(--radius); color: var(--text); }
|
|
nav.main a:hover { background: var(--info-soft); text-decoration: none; }
|
|
nav.main a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
header.top .spacer { flex: 1; }
|
|
header.top form.search input { width: 220px; }
|
|
header.top .user { color: var(--muted); }
|
|
main { padding: 16px; max-width: 1400px; margin: 0 auto; }
|
|
.page-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
.page-head > .actions { padding: 4px 0; }
|
|
.filter-row + .panel, .actions + .panel { margin-top: 4px; }
|
|
.page-head .grow { flex: 1; min-width: 200px; }
|
|
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
|
|
/* Panels and cards */
|
|
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow); margin-bottom: 14px; }
|
|
.panel > h2:first-child, .panel > h3:first-child { margin-top: 0; }
|
|
/* Tiles share a row and grow to fill it, so a wrapped tile never sits
|
|
alone beside an empty space. */
|
|
.cards { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; }
|
|
.cards > * { flex: 1 1 170px; min-width: 0; }
|
|
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; box-shadow: var(--shadow); }
|
|
.card .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; }
|
|
.card .value { font-size: 18px; font-weight: 600; margin-top: 2px; }
|
|
.card .sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
|
|
/* Tables */
|
|
.table-wrap { overflow-x: auto; }
|
|
table { border-collapse: collapse; width: 100%; background: var(--panel); }
|
|
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
th { font-size: 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; white-space: nowrap; }
|
|
tr:hover td { background: var(--info-soft); }
|
|
td.data { font-family: var(--mono); font-size: 13px; word-break: break-all; }
|
|
td.name { font-family: var(--mono); font-size: 13px; white-space: nowrap; }
|
|
td.actions-cell { white-space: nowrap; text-align: right; width: 1%; }
|
|
tr.editing td { background: var(--accent-soft); }
|
|
tr.disabled td:not(.actions-cell), tr.disabled td strong { color: var(--muted); }
|
|
/* Disabled records: hatched, so they stand out from live and pending rows. */
|
|
tr.disabled td { background-image: repeating-linear-gradient(135deg, transparent 0 7px, var(--hatch) 7px 9px); }
|
|
.badge.off { background: var(--muted); color: var(--panel); }
|
|
button.toggle { min-width: 5.2em; }
|
|
.empty { color: var(--muted); padding: 18px; text-align: center; }
|
|
|
|
/* Forms */
|
|
input, select, textarea, button {
|
|
font: inherit; color: var(--text);
|
|
}
|
|
input[type=text], input[type=password], input[type=number], input[type=search], input:not([type]), select, textarea {
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 5px 8px; min-width: 0;
|
|
}
|
|
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
|
|
textarea { width: 100%; min-height: 80px; resize: vertical; }
|
|
.form-grid { display: grid; grid-template-columns: minmax(140px, 200px) 1fr; gap: 8px 14px; align-items: start; }
|
|
.form-grid > label { padding-top: 6px; color: var(--muted); }
|
|
.form-grid .help { grid-column: 2; margin-top: -4px; color: var(--muted); font-size: 12px; }
|
|
.form-grid input, .form-grid select { width: 100%; max-width: 560px; }
|
|
.form-grid input[type=checkbox] { width: auto; }
|
|
.inline-form { display: flex; gap: 6px; flex-wrap: wrap; align-items: flex-end; }
|
|
.inline-form .f { display: flex; flex-direction: column; gap: 2px; }
|
|
.inline-form .f label:not(.check) { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.3px; }
|
|
.inline-form .grow { flex: 1; min-width: 220px; }
|
|
.inline-form .grow input { width: 100%; }
|
|
.hint { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
.check { display: inline-flex; gap: 6px; align-items: center; cursor: pointer; }
|
|
|
|
button, .button {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
padding: 5px 12px; cursor: pointer; white-space: nowrap; color: var(--text);
|
|
}
|
|
button:hover, .button:hover { background: var(--info-soft); text-decoration: none; }
|
|
button.primary, .button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); font-weight: 600; }
|
|
button.primary:hover { filter: brightness(1.08); background: var(--accent); }
|
|
button.danger { color: var(--err); border-color: var(--err); }
|
|
button.danger:hover { background: var(--err-soft); }
|
|
button.small, .button.small { padding: 2px 8px; font-size: 12px; }
|
|
button.link { border: none; background: none; color: var(--accent); padding: 0 4px; }
|
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
/* Badges and messages */
|
|
.badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; background: var(--info-soft); color: var(--muted); white-space: nowrap; }
|
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
.badge.err { background: var(--err-soft); color: var(--err); }
|
|
.badge.accent { background: var(--accent-soft); color: var(--accent); }
|
|
.msg { border-radius: var(--radius); padding: 8px 12px; margin: 8px 0; border: 1px solid transparent; }
|
|
.msg.err { background: var(--err-soft); color: var(--err); border-color: var(--err); }
|
|
.msg.warn { background: var(--warn-soft); color: var(--warn); }
|
|
.msg.ok { background: var(--ok-soft); color: var(--ok); }
|
|
.msg.info { background: var(--info-soft); }
|
|
.msg ul { margin: 4px 0 0; padding-left: 18px; }
|
|
.problems { list-style: none; padding: 0; margin: 6px 0; }
|
|
.problems li { padding: 4px 8px; border-left: 3px solid var(--border); margin-bottom: 3px; background: var(--info-soft); border-radius: 0 var(--radius) var(--radius) 0; }
|
|
.problems li.error { border-color: var(--err); background: var(--err-soft); }
|
|
.problems li.warning { border-color: var(--warn); background: var(--warn-soft); }
|
|
.problems li .where { color: var(--muted); font-size: 12px; margin-left: 6px; }
|
|
|
|
/* Diff */
|
|
|
|
/* Tabs */
|
|
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 12px; flex-wrap: wrap; }
|
|
.tabs a { padding: 7px 12px; border: 1px solid transparent; border-bottom: none; border-radius: var(--radius) var(--radius) 0 0; color: var(--muted); }
|
|
.tabs a.active { background: var(--panel); border-color: var(--border); color: var(--text); font-weight: 600; margin-bottom: -1px; }
|
|
|
|
/* Modal */
|
|
.modal-back { position: fixed; inset: 0; background: rgba(10, 15, 25, 0.45); display: flex; align-items: flex-start; justify-content: center; padding: 60px 16px; z-index: 50; overflow: auto; }
|
|
.modal { background: var(--panel); border-radius: 8px; border: 1px solid var(--border); width: min(760px, 100%); padding: 18px 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.25); }
|
|
.modal.wide { width: min(1100px, 100%); }
|
|
.modal h2 { margin-top: 0; }
|
|
.modal .buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
|
|
/* Toasts */
|
|
#toasts { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 100; max-width: min(480px, calc(100% - 32px)); }
|
|
.toast { background: var(--panel); border: 1px solid var(--border); border-left: 4px solid var(--accent); padding: 10px 14px; border-radius: var(--radius); box-shadow: 0 4px 16px rgba(0,0,0,0.18); }
|
|
.toast.ok { border-left-color: var(--ok); }
|
|
.toast.err { border-left-color: var(--err); }
|
|
.toast.warn { border-left-color: var(--warn); }
|
|
|
|
/* Login */
|
|
.login { max-width: 360px; margin: 10vh auto; }
|
|
.login-logo { display: block; margin: 8px auto 10px; }
|
|
.login h1 { text-align: center; margin: 0 0 8px; }
|
|
.wordmark { display: block; width: 220px; height: auto; margin: 0 auto; color: var(--text); }
|
|
.wordmark-rule .fill { fill: var(--accent); }
|
|
.wordmark-rule .line { fill: none; stroke: var(--accent); stroke-width: 2.6; }
|
|
.login .tag { text-align: center; color: var(--muted); margin-bottom: 18px; }
|
|
.login form { display: flex; flex-direction: column; gap: 10px; }
|
|
.login input { width: 100%; padding: 8px 10px; }
|
|
.login button { justify-content: center; padding: 8px; }
|
|
|
|
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; }
|
|
.kv dt { color: var(--muted); }
|
|
.kv dd { margin: 0; }
|
|
.grants { font-family: var(--mono); font-size: 12px; }
|
|
.copy-box { display: flex; gap: 6px; align-items: center; }
|
|
.login.wide { max-width: 520px; }
|
|
.copy-box input { flex: 1; font-family: var(--mono); }
|
|
.copy-box input { min-width: 0; }
|
|
td.break { word-break: break-all; }
|
|
@media (max-width: 700px) {
|
|
.form-grid { grid-template-columns: 1fr; }
|
|
.form-grid .help { grid-column: 1; }
|
|
header.top form.search input { width: 140px; }
|
|
}
|
|
|
|
|
|
.refreshing { opacity: 0.6; transition: opacity 0.2s; }
|
|
/* Secondary row actions show when the row is hovered or focused; touch
|
|
screens, which can't hover, always show them. Opacity keeps them in the
|
|
layout (no shifting) and reachable with Tab. */
|
|
tr .reveal { opacity: 0; transition: opacity 0.1s; }
|
|
tr:hover .reveal, tr:focus-within .reveal { opacity: 1; }
|
|
@media (hover: none) { tr .reveal { opacity: 1; } }
|
|
.filter-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--panel); }
|
|
.segmented a { padding: 4px 12px; color: var(--text); border-right: 1px solid var(--border); text-decoration: none; font-variant-numeric: tabular-nums; }
|
|
.segmented a:last-child { border-right: none; }
|
|
.segmented a:hover { background: var(--info-soft); }
|
|
.segmented a.active { background: var(--accent); color: var(--accent-text); font-weight: 600; }
|
|
/* the "← Zones" link above a page's heading */
|
|
.back-link { font-size: 13px; margin-bottom: 2px; }
|
|
|
|
tr.selected td { background: var(--accent-soft); }
|
|
/* the page footer with the Housecarl build */
|
|
.shell { min-height: 100vh; display: flex; flex-direction: column; }
|
|
.shell > main { flex: 1; width: 100%; }
|
|
footer.site { padding: 12px 16px 16px; text-align: center; font-size: 12px; color: var(--muted); }
|
|
footer.site a { color: var(--muted); text-decoration: underline; text-decoration-color: var(--border); text-underline-offset: 2px; }
|
|
footer.site a:hover { color: var(--accent); text-decoration-color: currentColor; }
|
|
|
|
/* a request in progress */
|
|
.spinner { display: inline-block; width: 11px; height: 11px; margin-right: 6px; vertical-align: -1px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; border-top-color: var(--border); background: var(--accent-soft); } }
|
|
|
|
.err-text { color: var(--err); }
|
|
ul.plain { list-style: none; margin: 8px 0; padding: 0; }
|
|
ul.plain li { padding: 3px 0; }
|
|
/* Drop-down menus in the top bar (Server, the user). */
|
|
details.menu { position: relative; }
|
|
details.menu > summary { list-style: none; cursor: pointer; padding: 6px 10px; border-radius: var(--radius); color: var(--text); white-space: nowrap; }
|
|
details.menu > summary::-webkit-details-marker { display: none; }
|
|
details.menu > summary:hover, details.menu[open] > summary { background: var(--info-soft); }
|
|
details.menu > summary.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
details.menu > summary .caret { margin-left: 4px; font-size: 0.8em; opacity: 0.7; }
|
|
details.menu > summary .user { color: inherit; }
|
|
details.menu .menu-list {
|
|
position: absolute; top: calc(100% + 4px); left: 0; z-index: 20; min-width: 190px;
|
|
display: flex; flex-direction: column; padding: 4px;
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
|
}
|
|
details.menu.right .menu-list { left: auto; right: 0; }
|
|
details.menu .menu-list a, details.menu .menu-list .menu-item {
|
|
display: block; padding: 7px 10px; border-radius: var(--radius); color: var(--text); text-align: left;
|
|
background: none; border: 0; font: inherit; cursor: pointer; white-space: nowrap;
|
|
}
|
|
details.menu .menu-list a:hover, details.menu .menu-list .menu-item:hover { background: var(--info-soft); text-decoration: none; }
|
|
details.menu .menu-list a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
nav.main { align-items: center; }
|
|
header.top .header-right { display: flex; align-items: center; gap: 12px; margin-left: auto; }
|
|
@media (max-width: 1200px) {
|
|
header.top form.search input { width: 160px; }
|
|
header.top { gap: 10px; }
|
|
}
|
|
|
|
button.chip { padding: 1px 8px; font-size: 12px; font-family: var(--mono); border-radius: 999px; background: var(--info-soft); border: 1px solid var(--border); color: var(--text); }
|
|
button.chip:hover { background: var(--accent-soft); border-color: var(--accent); }
|
|
|
|
/* ---- Housecarl */
|
|
.wordmark-rule .tag { fill: var(--accent); font: 700 17px system-ui, sans-serif; letter-spacing: 0.5px; }
|
|
.login .wordmark { width: 290px; }
|
|
.subnet-usage { width: 260px; }
|
|
/* How full a subnet's pools are. */
|
|
.usage { min-width: 120px; }
|
|
.usage-bar { display: flex; height: 8px; border-radius: 4px; background: var(--info-soft); overflow: hidden; border: 1px solid var(--border); margin-top: 3px; }
|
|
.usage-bar .seg.leased { background: var(--accent); }
|
|
.usage-bar .seg.declined { background: var(--warn); }
|
|
.usage-bar.full .seg.leased { background: var(--err); }
|
|
.usage-text { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
/* Interfaces: serving or not. */
|
|
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: 1px; background: var(--muted); }
|
|
.dot.ok { background: var(--ok); }
|
|
.dot.err { background: var(--err); }
|
|
.iface-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.iface-list .iface { display: flex; gap: 10px; flex-wrap: wrap; align-items: baseline; }
|
|
.iface-list .iface .name { font-family: var(--mono); font-weight: 600; min-width: 8ch; }
|
|
/* The options editor: a row per option. */
|
|
.opt-rows { display: flex; flex-direction: column; gap: 6px; }
|
|
.opt-row { display: flex; gap: 6px; align-items: flex-start; flex-wrap: wrap; }
|
|
.opt-row select { width: 15em; }
|
|
.opt-row input.value { flex: 1; min-width: 14em; font-family: var(--mono); }
|
|
.opt-row select.type { width: 8em; }
|
|
.opt-row .help { flex-basis: 100%; color: var(--muted); font-size: 12px; margin: -2px 0 2px; }
|
|
.opt-inherited { color: var(--muted); font-size: 12px; margin-top: 6px; }
|
|
.opt-inherited code { font-family: var(--mono); }
|
|
/* Pools editor. */
|
|
.pool-rows { display: flex; flex-direction: column; gap: 6px; }
|
|
.pool-row { display: flex; gap: 6px; align-items: center; }
|
|
.pool-row input { font-family: var(--mono); width: 18em; }
|
|
.pool-row select { width: auto; }
|
|
/* Activity: one line per message. */
|
|
table.activity td { white-space: nowrap; }
|
|
table.activity td.detail { white-space: normal; color: var(--muted); }
|
|
table.activity tr.event-details > td { white-space: normal; background: var(--bg); }
|
|
.event-detail table.kv { border-collapse: collapse; margin: 4px 0; }
|
|
.event-detail table.kv th { text-align: left; font-weight: normal; color: var(--muted); padding: 2px 16px 2px 0; vertical-align: top; white-space: nowrap;
|
|
text-transform: none; letter-spacing: normal; font-size: inherit; background: none; border: 0; }
|
|
.event-detail table.kv td { padding: 2px 0; white-space: normal; word-break: break-word; background: none; border: 0; }
|
|
.type-OFFER, .type-ACK { color: var(--ok); }
|
|
.type-NAK, .type-DECLINE, .type-CONFLICT, .type-NO { color: var(--err); }
|
|
.type-RELEASE { color: var(--muted); }
|
|
td.mono, span.mono { font-family: var(--mono); font-size: 13px; }
|
|
.mt { margin-top: 10px; }
|
|
input:disabled, select:disabled, textarea:disabled { background: var(--info-soft); color: var(--muted); cursor: not-allowed; opacity: 1; }
|
|
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
|
.panel-head h2 { margin: 0; }
|
|
pre.wrap { white-space: pre-wrap; word-break: break-all; font-size: 12px; }
|
|
|
|
.dimmed { opacity: 0.55; }
|
|
|
|
/* The logo's own blue (artwork/logo.svg), lighter on dark backgrounds. */
|
|
:root { --logo: #1d5287; }
|
|
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --logo: #6d9be8; } }
|
|
:root[data-theme="dark"] { --logo: #6d9be8; }
|
|
.logo-mark { fill: var(--logo); }
|
|
|
|
/* The probation banner: changes roll back unless confirmed. */
|
|
.probation-banner { background: var(--warn-soft); color: var(--text); border-bottom: 1px solid var(--warn); padding: 10px 24px; font-size: 14px; }
|
|
.probation-banner .banner-actions { margin-left: 12px; white-space: nowrap; }
|
|
.shadow-banner { background: var(--panel); border-bottom-color: var(--border); }
|
|
/* List editors (zones, addresses, services). */
|
|
.list-rows { display: flex; flex-direction: column; gap: 6px; margin-bottom: 6px; }
|
|
.list-row { display: flex; gap: 6px; align-items: center; }
|
|
.list-row input { width: 26em; max-width: 100%; }
|
|
input.short { width: 7em; }
|
|
.builtins { margin-bottom: 10px; line-height: 1.6; }
|
|
.policy { margin-top: 10px; }
|
|
.checks { display: flex; flex-direction: column; gap: 4px; }
|
|
.form-actions { margin-top: 14px; }
|
|
tr.disabled td:not(.actions-cell) { opacity: 0.55; }
|
|
|
|
/* Pending changes (as in Jarl): the bar, the Review/Live switch, rows. */
|
|
.pending-bar {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
padding: 8px 24px; background: var(--warn-soft); border-bottom: 1px solid var(--border);
|
|
}
|
|
.pending-bar strong { color: var(--warn); }
|
|
.pending-bar .apply-now { margin-left: auto; }
|
|
.mode-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; margin-right: 10px; border-radius: 999px; font-size: 12px; font-weight: 500; background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
.mode-toggle:hover { color: var(--text); border-color: var(--accent); }
|
|
.mode-toggle .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
|
|
.mode-toggle.live { color: var(--warn); border-color: var(--warn); background: var(--warn-soft); }
|
|
.mode-toggle.live .dot { background: var(--warn); }
|
|
tr.pending-new td { background: var(--add); }
|
|
tr.pending-changed td { background: var(--warn-soft); }
|
|
tr.pending-deleted td { background: var(--del); color: var(--muted); }
|
|
tr.pending-deleted td:not(.actions-cell) { text-decoration: line-through; }
|
|
tr.op-failed td { background: var(--err-soft); }
|
|
input.wide { width: 100%; }
|
|
.diff { background: var(--code-bg); border-radius: var(--radius); overflow: auto; font-family: var(--mono); font-size: 12.5px; line-height: 1.4; }
|
|
.diff div { white-space: pre; padding: 0 10px; }
|
|
.diff .add { background: var(--add); color: var(--add-text); }
|
|
.diff .del { background: var(--del); color: var(--del-text); }
|
|
.diff .hunk { background: var(--hunk); color: var(--accent); }
|
|
.diff .file { font-weight: 700; padding-top: 6px; }
|
|
select.inline { width: auto; display: inline-block; }
|
|
/* The rule dialog's optional parts: "+" buttons, and ✕ to close one. */
|
|
.adders { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
button.close-section { padding: 0 6px; margin-left: 4px; font-size: 11px; line-height: 1.5; }
|
|
/* Trace: one panel per packet, its steps below. */
|
|
.trace-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
.trace-time { margin-left: auto; }
|
|
table.trace-steps { width: 100%; margin-top: 6px; }
|
|
table.trace-steps td { padding: 3px 8px; border-top: 1px solid var(--border); }
|
|
tr.step-drop td { background: var(--del); }
|
|
tr.step-accept td { background: var(--add); }
|
|
/* The zone matrix: a cell per pair, coloured by what happens. */
|
|
table.matrix th, table.matrix td { text-align: center; }
|
|
table.matrix tbody th { text-align: left; }
|
|
table.matrix th.corner { font-weight: 400; color: var(--muted); text-transform: none; }
|
|
table.matrix td { padding: 4px; }
|
|
.matrix-cell { width: 100%; min-width: 7em; border: 0; background: transparent; color: inherit; font-weight: 600; padding: 6px 8px; cursor: pointer; }
|
|
td.matrix-allowed { background: var(--add); color: var(--add-text); }
|
|
td.matrix-blocked { background: var(--del); color: var(--del-text); }
|
|
.matrix-n { margin-left: 6px; font-weight: 400; font-size: 12px; color: var(--warn); }
|
|
td.matrix-none { color: var(--muted); }
|
|
tr.matrix-rest td { border-top: 2px solid var(--border); }
|
|
|
|
/* ---- Statistics charts. Colors from the validated reference palette
|
|
(categorical slots 1-3, sequential blue), checked against this UI's
|
|
panel surfaces in both themes. */
|
|
:root {
|
|
--series-1: #2a78d6;
|
|
--series-2: #eb6834;
|
|
--series-3: #1baf7a;
|
|
--chart-grid: #eceef2;
|
|
--chart-axis: #c9cfd8;
|
|
/* Sequential ramp, low to high (blue 100 -> 700). */
|
|
--heat-0: #cde2fb; --heat-1: #9ec5f4; --heat-2: #6da7ec; --heat-3: #3987e5;
|
|
--heat-4: #256abf; --heat-5: #184f95; --heat-6: #0d366b;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--series-1: #3987e5;
|
|
--series-2: #d95926;
|
|
--series-3: #199e70;
|
|
--chart-grid: #272c34;
|
|
--chart-axis: #3a414d;
|
|
/* In dark, low values recede into the surface: the ramp runs dark -> light. */
|
|
--heat-0: #0d366b; --heat-1: #184f95; --heat-2: #256abf; --heat-3: #3987e5;
|
|
--heat-4: #6da7ec; --heat-5: #9ec5f4; --heat-6: #cde2fb;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--series-1: #3987e5;
|
|
--series-2: #d95926;
|
|
--series-3: #199e70;
|
|
--chart-grid: #272c34;
|
|
--chart-axis: #3a414d;
|
|
--heat-0: #0d366b; --heat-1: #184f95; --heat-2: #256abf; --heat-3: #3987e5;
|
|
--heat-4: #6da7ec; --heat-5: #9ec5f4; --heat-6: #cde2fb;
|
|
}
|
|
|
|
.refreshing { opacity: 0.6; transition: opacity 0.2s; }
|
|
/* Secondary row actions show when the row is hovered or focused; touch
|
|
screens, which can't hover, always show them. Opacity keeps them in the
|
|
layout (no shifting) and reachable with Tab. */
|
|
tr .reveal { opacity: 0; transition: opacity 0.1s; }
|
|
tr:hover .reveal, tr:focus-within .reveal { opacity: 1; }
|
|
@media (hover: none) { tr .reveal { opacity: 1; } }
|
|
/* Blocking steps, in order, on the Blocking page. */
|
|
.steps-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin: 4px 0 10px; }
|
|
.step-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 12px; font-family: var(--mono); font-size: 13px; background: var(--panel); }
|
|
.step-chip.accent { border-color: var(--accent); }
|
|
.step-tools { display: inline-flex; gap: 2px; margin-left: 2px; }
|
|
.step-tools button { padding: 0 5px; min-width: 0; }
|
|
h2.section { margin: 24px 0 10px; font-size: 18px; }
|
|
.segmented button { border: none; border-right: 1px solid var(--border); border-radius: 0; background: transparent; padding: 4px 12px; color: var(--text); }
|
|
.segmented button:last-child { border-right: none; }
|
|
.segmented button:hover { background: var(--info-soft); }
|
|
.segmented button.active { background: var(--accent); color: var(--accent-text); font-weight: 600; }
|
|
.card.stat .value.big { font-size: 26px; font-weight: 600; line-height: 1.2; }
|
|
|
|
.chart-card { margin: 0 0 14px; }
|
|
.chart-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
.chart-head .grow { flex: 1; min-width: 200px; }
|
|
.chart-head h2 { margin: 0; font-size: 16px; }
|
|
/* Two or three columns; the last cards stretch to fill their row, so no
|
|
card is left alone beside an empty space. */
|
|
.chart-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
|
|
.chart-grid > :last-child:nth-child(odd) { grid-column: 1 / -1; }
|
|
@media (min-width: 1100px) {
|
|
.chart-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
.chart-grid > :last-child:nth-child(odd) { grid-column: auto; }
|
|
.chart-grid > :last-child:nth-child(3n+1) { grid-column: 1 / -1; }
|
|
.chart-grid > :last-child:nth-child(3n+2) { grid-column: span 2; }
|
|
}
|
|
.chart-grid > .panel h2 { margin: 0; font-size: 16px; }
|
|
.list-sub { margin: 2px 0 8px; }
|
|
|
|
.chart { position: relative; outline: none; }
|
|
.chart:focus-visible { box-shadow: 0 0 0 2px var(--accent); border-radius: var(--radius); }
|
|
.chart svg { display: block; overflow: visible; }
|
|
.chart .grid { stroke: var(--chart-grid); stroke-width: 1; }
|
|
.chart .axis { stroke: var(--chart-axis); stroke-width: 1; }
|
|
.chart .tick, .chart .end-label { fill: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
.chart .end-label { fill: var(--text); }
|
|
.chart .line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
.chart .area { stroke: none; fill-opacity: 0.1; }
|
|
.chart .dot { stroke: var(--panel); stroke-width: 2; }
|
|
.chart .crosshair { stroke: var(--muted); stroke-width: 1; }
|
|
.chart .hit { fill: transparent; cursor: crosshair; }
|
|
.chart .s1, .key.s1 { stroke: var(--series-1); }
|
|
.chart .s2, .key.s2 { stroke: var(--series-2); }
|
|
.chart .s3, .key.s3 { stroke: var(--series-3); }
|
|
.chart .area.s1, .chart .dot.s1 { fill: var(--series-1); }
|
|
.chart .area.s2, .chart .dot.s2 { fill: var(--series-2); }
|
|
.chart .area.s3, .chart .dot.s3 { fill: var(--series-3); }
|
|
.key.line { display: inline-block; width: 14px; height: 0; border-top: 2px solid; vertical-align: middle; margin-right: 6px; }
|
|
.key.line.s1 { border-color: var(--series-1); }
|
|
.key.line.s2 { border-color: var(--series-2); }
|
|
.key.line.s3 { border-color: var(--series-3); }
|
|
.chart-legend { list-style: none; display: flex; gap: 14px; margin: 2px 0 0; padding: 0; font-size: 13px; color: var(--text); flex-wrap: wrap; }
|
|
|
|
.chart-tip { position: absolute; z-index: 5; pointer-events: none; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16); padding: 6px 10px; font-size: 13px; white-space: nowrap; }
|
|
.chart-tip .tip-head { color: var(--muted); font-size: 12px; margin-bottom: 2px; }
|
|
.chart-tip .tip-row strong { font-variant-numeric: tabular-nums; }
|
|
.chart-table .scroll, .chart-table .table-wrap { max-height: 360px; overflow: auto; }
|
|
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
|
|
.heatmap-wrap { position: relative; }
|
|
.heatmap { display: grid; grid-template-columns: 36px repeat(24, minmax(10px, 1fr)); gap: 2px; outline: none; }
|
|
.heatmap:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
|
|
.hm-corner, .hm-col, .hm-row { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
.hm-row { display: flex; align-items: center; }
|
|
.hm-cell { height: 22px; border-radius: 3px; }
|
|
.hm-cell:hover, .hm-cell:focus { outline: 2px solid var(--text); outline-offset: -1px; }
|
|
.hm-empty { background: transparent; box-shadow: inset 0 0 0 1px var(--chart-grid); }
|
|
.heat-0 { background: var(--heat-0); } .heat-1 { background: var(--heat-1); } .heat-2 { background: var(--heat-2); }
|
|
.heat-3 { background: var(--heat-3); } .heat-4 { background: var(--heat-4); } .heat-5 { background: var(--heat-5); }
|
|
.heat-6 { background: var(--heat-6); }
|
|
.hm-scale { display: flex; align-items: center; gap: 2px; margin-top: 10px; flex-wrap: wrap; }
|
|
.hm-scale .muted { margin: 0 6px; }
|
|
.hm-swatch { display: inline-block; width: 18px; height: 10px; border-radius: 2px; }
|
|
|
|
.bar-list { list-style: none; margin: 0; padding: 0; }
|
|
.bar-list li { padding: 4px 0; }
|
|
.bar-text { display: flex; gap: 8px; justify-content: space-between; font-size: 13px; }
|
|
.bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 12.5px; min-width: 0; }
|
|
.bar-value { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
.bar-track { height: 6px; margin-top: 3px; }
|
|
.bar-fill { height: 6px; background: var(--series-1); border-radius: 0 3px 3px 0; min-width: 2px; }
|
|
@media (max-width: 700px) {
|
|
.heatmap { grid-template-columns: 30px repeat(24, minmax(6px, 1fr)); gap: 1px; }
|
|
.hm-cell { height: 16px; }
|
|
.chart-grid { grid-template-columns: 1fr; }
|
|
}
|
|
.plain-list { margin: 0; padding-left: 18px; }
|
|
.plain-list li { padding: 2px 0; }
|