Files
gandalf/static/style.css
Jared Vititoe e8314b5ba3 Fix topology diagram: replace SVG fork with CSS, fix line alignment
- Remove SVG fork with preserveAspectRatio="none" (caused line width
  distortion and stretched 10G DAC label like a tube TV)
- Replace with pure CSS .topo-fork: stem + horizontal bar + left/right
  drops, all absolutely positioned at consistent 2px width
- Use .topo-sw-row with two 50% halves so switch centres land at
  exactly 25% and 75% — matching fork drop positions mathematically
- ISL rendered via ::before/::after on .topo-sw-row (switch boxes
  with solid bg cover the line at their edges, leaving only the gap)
- Add .topo-sw-drops: two vertical stubs from switch centres to bus rails
- All lines are now exactly 2px, no distortion, no misalignment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 22:35:02 -04:00

2203 lines
63 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ══════════════════════════════════════════════════════════════════════
GANDALF Terminal aesthetic (Pulse / TinkerTickets style)
══════════════════════════════════════════════════════════════════════ */
/* ── Variables ────────────────────────────────────────────────────── */
:root {
--bg: #0a0a0a;
--bg2: #1a1a1a;
--bg3: #2a2a2a;
--bg-hover: rgba(0,255,65,.07);
--green: #00ff41;
--green-dim: rgba(0,255,65,.15);
--green-dark: #00cc33;
--green-muted: #008822;
--amber: #ffb000;
--amber-dim: rgba(255,176,0,.15);
--cyan: #00ffff;
--cyan-dim: rgba(0,255,255,.12);
--red: #ff4444;
--red-dim: rgba(255,68,68,.15);
--orange: #ff8c00;
--orange-dim: rgba(255,140,0,.15);
--border: rgba(0,255,65,.35);
--border-hi: #00ff41;
--text: #00ff41;
--text-dim: #00cc33;
--text-muted: #00bb33;
--font: 'Courier New','Consolas','Monaco','Menlo',monospace;
--glow: 0 0 5px #00ff41, 0 0 10px #00ff41, 0 0 15px #00ff41;
--glow-xl: 0 0 8px #00ff41, 0 0 16px #00ff41, 0 0 24px #00ff41, 0 0 32px rgba(0,255,65,.5);
--glow-amber: 0 0 5px #ffb000, 0 0 10px #ffb000, 0 0 15px #ffb000;
--glow-red: 0 0 5px #ff4444, 0 0 10px rgba(255,68,68,.4);
--glow-cyan: 0 0 5px #00ffff, 0 0 10px rgba(0,255,255,.35);
/* Unified naming aliases — matches base.css variable names */
--bg-primary: var(--bg);
--bg-secondary: var(--bg2);
--bg-tertiary: var(--bg3);
--terminal-green: var(--green);
--terminal-green-dim: var(--green-dim);
--terminal-amber: var(--amber);
--terminal-amber-dim: var(--amber-dim);
--terminal-cyan: var(--cyan);
--terminal-red: var(--red);
--text-primary: var(--text);
--text-secondary: var(--text-dim);
--border-color: var(--border);
--glow-green: var(--glow);
--font-mono: var(--font);
}
/* ── Reset ────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scrollbar-color: var(--green-muted) var(--bg2); scrollbar-width: thin; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg2); }
::-webkit-scrollbar-thumb { background: var(--green-muted); }
::-webkit-scrollbar-thumb:hover { background: var(--green); }
/* ── Body / CRT ───────────────────────────────────────────────────── */
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
position: relative;
animation: flicker .25s ease-in-out 30s infinite;
}
/* CRT scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
rgba(0,0,0,0.15) 0px, rgba(0,0,0,0.15) 1px,
transparent 1px, transparent 2px
);
pointer-events: none;
z-index: 9999;
animation: scanline 8s linear infinite;
}
/* Binary data stream corner */
body::after {
content: '10101010';
position: fixed;
bottom: 10px; right: 14px;
font-family: var(--font);
font-size: .55rem;
color: var(--green);
opacity: .07;
pointer-events: none;
letter-spacing: 2px;
animation: data-stream 3s steps(1) infinite;
}
@keyframes scanline { to { transform: translateY(4px); } }
@keyframes flicker { 0%,100%{opacity:1} 10%{opacity:.96} 50%{opacity:.98} }
@keyframes data-stream {
0% { content:'10101010'; } 25% { content:'01010101'; }
50% { content:'11001100'; } 75% { content:'00110011'; }
}
@keyframes pulse-glow {
0%,100% { text-shadow: var(--glow); }
50% { text-shadow: var(--glow-xl); }
}
@keyframes pulse-red {
0%,100% { box-shadow: 0 0 0 0 rgba(255,68,68,.5); }
50% { box-shadow: 0 0 6px 3px rgba(255,68,68,.2); }
}
@keyframes blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
@keyframes slide-in {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
a { color: var(--amber); text-decoration: none; }
a:hover { text-decoration: underline; text-shadow: var(--glow-amber); }
/* ── Header ───────────────────────────────────────────────────────── */
.header {
background: var(--bg2);
border-bottom: 2px solid var(--green);
box-shadow: 0 2px 16px rgba(0,255,65,.12);
padding: 0 28px;
height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 100;
}
.header::before { content:'╔'; position:absolute; top:-1px; left:-1px; font-size:1.4rem; color:var(--green); text-shadow:var(--glow); line-height:1; }
.header::after { content:'╗'; position:absolute; top:-1px; right:-1px; font-size:1.4rem; color:var(--green); text-shadow:var(--glow); line-height:1; }
.header-left { display:flex; align-items:center; gap:24px; }
.header-brand { display:flex; flex-direction:column; }
.header-title {
font-size: 1.35em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
letter-spacing: .08em;
}
.header-title::before { content:'>> '; color:var(--green); text-shadow:var(--glow); }
.header-sub { font-size: .65em; color: var(--text-muted); letter-spacing: .12em; text-transform: uppercase; }
.header-nav { display:flex; gap:3px; }
.nav-link {
color: var(--text-muted);
padding: 5px 12px;
border: 1px solid transparent;
font-size: .8em;
letter-spacing: .06em;
text-transform: uppercase;
transition: all .15s;
}
.nav-link::before { content:'[ '; }
.nav-link::after { content:' ]'; }
.nav-link:hover {
color: var(--green);
border-color: var(--border);
background: var(--green-dim);
text-shadow: var(--glow);
text-decoration: none;
}
.nav-link.active {
color: var(--amber);
border-color: var(--amber);
background: var(--amber-dim);
text-shadow: var(--glow-amber);
text-decoration: none;
}
.header-right { display:flex; align-items:center; gap:10px; }
.header-user { font-size: .78em; color: var(--text-muted); }
.header-user::before { content:'[USER: '; }
.header-user::after { content:']'; }
/* ── Main ─────────────────────────────────────────────────────────── */
.main { max-width: 1500px; margin: 0 auto; padding: 22px 20px; }
/* ── Section ──────────────────────────────────────────────────────── */
.section { margin-bottom: 26px; }
.section-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
border-bottom: 1px solid var(--border);
padding-bottom: 5px;
}
.section-title {
font-size: .9em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
text-transform: uppercase;
letter-spacing: .1em;
}
.section-title::before { content:'╠═══ '; color:var(--green); text-shadow:var(--glow); }
.section-title::after { content:' ═══╣'; color:var(--green); text-shadow:var(--glow); }
.section-badge {
font-size: .72em;
font-weight: bold;
color: var(--red);
border: 1px solid var(--red);
padding: 0 5px;
text-shadow: var(--glow-red);
}
.section-badge::before { content:'['; }
.section-badge::after { content:']'; }
/* ── Page header ──────────────────────────────────────────────────── */
.page-header { margin-bottom: 18px; }
.page-title {
font-size: 1.05em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
letter-spacing: .06em;
}
.page-title::before { content:'>> '; color:var(--green); text-shadow:var(--glow); }
.page-sub { font-size: .75em; color: var(--text-muted); margin-top: 3px; }
/* ── Status bar ───────────────────────────────────────────────────── */
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg2);
border: 1px solid var(--border);
padding: 9px 16px;
margin-bottom: 18px;
gap: 12px;
flex-wrap: wrap;
position: relative;
}
.status-bar::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.status-bar::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.status-chips { display:flex; gap:7px; flex-wrap:wrap; align-items:center; }
.chip {
font-size: .78em;
font-weight: bold;
padding: 2px 9px;
border: 1px solid;
letter-spacing: .04em;
}
.chip::before { content:'['; }
.chip::after { content:']'; }
.chip-critical { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); animation:pulse-glow 2s infinite; }
.chip-warning { color:var(--orange); border-color:var(--orange); }
.chip-ok { color:var(--green); border-color:var(--border); text-shadow:var(--glow); }
.status-meta { display:flex; align-items:center; gap:10px; white-space:nowrap; }
.last-check { font-size: .72em; color: var(--text-muted); }
.btn-refresh {
background: transparent;
border: 1px solid var(--border);
color: var(--text-muted);
padding: 2px 10px;
font-family: var(--font);
font-size: .75em;
cursor: pointer;
transition: all .15s;
}
.btn-refresh:hover { color:var(--green); border-color:var(--green); background:var(--green-dim); text-shadow:var(--glow); }
/* ── Topology ─────────────────────────────────────────────────────── */
.topology {
background: var(--bg2);
border: 1px solid var(--border);
padding: 20px 16px 16px;
margin-bottom: 16px;
text-align: center;
overflow-x: auto;
position: relative;
}
.topology::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.topology::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.topo-row { display:flex; justify-content:center; gap:16px; flex-wrap:wrap; align-items:center; }
.topo-row-internet { margin-bottom:2px; }
.topo-hosts-row { flex-wrap:wrap; gap:10px; }
.topo-connectors { display:flex; justify-content:center; gap:80px; height:22px; margin:0; }
.topo-connectors.single { gap:0; }
.topo-connectors.wide { gap:44px; }
.topo-line { width:1px; height:100%; background:var(--green); opacity:.4; }
.topo-line-labeled { position:relative; }
.topo-line-labeled::after {
content: attr(data-link-label);
position: absolute;
left: 6px; top: 50%;
transform: translateY(-50%);
font-size: .62em;
color: var(--amber);
text-shadow: var(--glow-amber);
white-space: nowrap;
letter-spacing: .05em;
}
.topo-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 7px 12px;
border: 1px solid var(--border);
background: var(--bg3);
min-width: 94px;
font-size: .75em;
position: relative;
transition: border-color .2s;
}
.topo-node::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.8rem; line-height:1; }
.topo-node::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.8rem; line-height:1; }
.topo-internet { border-color:var(--cyan); color:var(--cyan); text-shadow:var(--glow-cyan); font-weight:bold; }
.topo-switch { border-color:var(--amber); color:var(--amber); text-shadow:var(--glow-amber); }
.topo-host { cursor:default; }
.topo-icon { font-size:1.1em; }
.topo-label { font-weight:bold; letter-spacing:.03em; }
.topo-badge { font-size:.68em; padding:1px 5px; border:1px solid; letter-spacing:.03em; }
.topo-badge-up { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.topo-badge-down { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); animation:pulse-glow 1.5s infinite; }
.topo-badge-degraded { color:var(--orange); border-color:var(--orange); }
.topo-status-up { border-color:var(--green); box-shadow:0 0 8px rgba(0,255,65,.2); }
.topo-status-down { border-color:var(--red); box-shadow:0 0 8px rgba(255,68,68,.3); }
.topo-status-degraded { border-color:var(--orange); box-shadow:0 0 8px rgba(255,140,0,.2); }
.topo-status-dot { width:7px; height:7px; border:1px solid var(--text-muted); background:transparent; position:absolute; top:5px; right:5px; }
/* Topology subtitle text */
.topo-node-sub {
font-size: .58em;
color: var(--text-muted);
letter-spacing: .02em;
font-weight: normal;
}
.topo-badge-unknown { color:var(--text-muted); border-color:var(--border); }
.topo-vlan-tag {
color: var(--cyan) !important;
opacity: .7;
font-size: .54em !important;
}
/* Switch tier: two switches with horizontal connector */
.topo-switch-tier {
display: flex;
align-items: center;
gap: 0;
}
.topo-h-link {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 8px;
}
.topo-h-link-line {
width: 70px;
height: 1px;
background: var(--amber);
opacity: .5;
}
.topo-h-link-label {
font-size: .52em;
color: var(--amber);
opacity: .7;
margin-top: 3px;
letter-spacing: .04em;
}
/* Host tier: two groups side by side */
.topo-host-tier {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 40px;
margin-top: 0;
}
.topo-host-group { flex-shrink: 0; }
/* PoE host group: offset right to sit below PoE switch */
.topo-poe-hosts {
padding-top: 0;
}
/* Off-rack node (dashed border) */
.topo-host-table {
border-style: dashed;
}
/* Dashed 10G line (for off-rack/table host) */
.topo-line-dashed {
background: none;
border-left: 1px dashed var(--green);
opacity: .4;
}
/* 1G management band — horizontal amber dashed line with label */
.topo-mgmt-band {
display: flex;
align-items: center;
gap: 6px;
padding: 0 8px;
height: 16px;
}
.topo-mgmt-label {
font-size: .52em;
color: var(--amber);
opacity: .65;
white-space: nowrap;
letter-spacing: .04em;
}
.topo-mgmt-line {
flex: 1;
height: 1px;
border-top: 1px dashed var(--amber);
opacity: .4;
}
/* ── Host cards ───────────────────────────────────────────────────── */
.host-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
gap: 12px;
}
.host-card {
background: var(--bg2);
border: 1px solid var(--border);
padding: 12px;
position: relative;
transition: border-color .2s, box-shadow .2s;
}
.host-card::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.host-card::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.host-card:hover { border-color:var(--green); box-shadow:0 0 12px rgba(0,255,65,.12); }
.host-card-up { border-left: 3px solid var(--green); }
.host-card-down { border-left: 3px solid var(--red); box-shadow: inset 3px 0 10px rgba(255,68,68,.08); }
.host-card-degraded { border-left: 3px solid var(--orange); }
.host-card-header { margin-bottom: 8px; }
.host-name-row { display:flex; align-items:center; gap:6px; margin-bottom:3px; }
.host-name {
font-weight: bold;
font-size: .88em;
color: var(--amber);
text-shadow: var(--glow-amber);
letter-spacing: .04em;
}
.host-meta { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
.host-ip { font-size:.72em; color:var(--text-muted); letter-spacing:.02em; }
.host-source {
font-size: .65em;
padding: 1px 5px;
border: 1px solid;
letter-spacing: .04em;
font-weight: bold;
}
.source-prometheus { color:#e8703a; border-color:rgba(232,112,58,.4); }
.source-ping { color:var(--cyan); border-color:var(--cyan-dim); }
.iface-list { border-top:1px solid var(--border); padding-top:6px; margin-bottom:8px; }
.iface-row { display:flex; align-items:center; gap:6px; padding:2px 0; }
.iface-name { font-size:.78em; flex:1; color:var(--text-dim); letter-spacing:.01em; }
.iface-state { font-size:.72em; font-weight:bold; letter-spacing:.04em; }
.state-up { color:var(--green); text-shadow:var(--glow); }
.state-down { color:var(--red); text-shadow:var(--glow-red); }
.state-initial_down { color:var(--text-muted); }
.host-ping-note { font-size:.72em; color:var(--text-muted); border-top:1px solid var(--border); padding-top:6px; margin-bottom:8px; }
.host-actions { border-top:1px solid var(--border); padding-top:7px; display:flex; gap:5px; flex-wrap:wrap; }
/* ── Status dots ──────────────────────────────────────────────────── */
.host-status-dot, .iface-dot,
.dot-up, .dot-down, .dot-degraded, .dot-unknown, .dot-initial_down {
display: inline-block;
width: 8px; height: 8px;
border: 1px solid;
flex-shrink: 0;
}
.dot-up, .host-status-dot.dot-up { border-color:var(--green); background:var(--green); box-shadow:0 0 4px var(--green); }
.dot-down, .host-status-dot.dot-down { border-color:var(--red); background:var(--red); animation:pulse-red 1.5s infinite; }
.dot-degraded { border-color:var(--orange); background:var(--orange); }
.dot-unknown, .dot-initial_down { border-color:var(--text-muted); background:transparent; }
/* ── Badges ───────────────────────────────────────────────────────── */
.badge {
display: inline-block;
font-size: .7em;
font-weight: bold;
padding: 1px 6px;
border: 1px solid;
letter-spacing: .05em;
text-transform: uppercase;
}
.badge::before { content:'['; }
.badge::after { content:']'; }
.badge-critical { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); }
.badge-warning { color:var(--orange); border-color:var(--orange); }
.badge-info { color:var(--cyan); border-color:var(--cyan-dim); }
.badge-ok { color:var(--green); border-color:var(--border); text-shadow:var(--glow); }
.badge-neutral { color:var(--text-muted); border-color:var(--text-muted); }
.badge-suppressed{ font-size:.9em; padding:0; border:none; color:var(--text-muted); }
/* ── Tables ───────────────────────────────────────────────────────── */
.table-wrap {
background: var(--bg2);
border: 1px solid var(--border);
overflow: hidden;
position: relative;
}
.table-wrap::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.table-wrap::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.data-table { width:100%; border-collapse:collapse; }
.data-table th {
background: var(--bg3);
padding: 8px 12px;
text-align: left;
font-size: .7em;
font-weight: bold;
color: var(--amber);
text-transform: uppercase;
letter-spacing: .08em;
border-bottom: 1px solid var(--border);
white-space: nowrap;
text-shadow: var(--glow-amber);
}
.data-table th::before { content:'> '; color:var(--green); }
.data-table td {
padding: 7px 12px;
border-bottom: 1px solid rgba(0,255,65,.08);
vertical-align: middle;
font-size: .83em;
}
.data-table tr:last-child td { border-bottom:none; }
.data-table tr:hover td { background:var(--bg-hover); }
.row-critical td { background:rgba(255,68,68,.03); }
.row-critical td:first-child { border-left:2px solid var(--red); }
.row-warning td { background:rgba(255,140,0,.03); }
.row-warning td:first-child { border-left:2px solid var(--orange); }
.row-resolved td { opacity:.5; }
.data-table-sm td, .data-table-sm th { padding:5px 10px; }
.ts-cell { color:var(--text-muted); font-size:.75em; white-space:nowrap; }
.desc-cell { max-width:280px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ticket-link{ color:var(--amber); text-shadow:var(--glow-amber); font-weight:bold; }
.empty-state { padding:28px; text-align:center; color:var(--text-muted); font-size:.82em; }
.empty-row td{ text-align:center; color:var(--text-muted); }
/* ── Buttons ──────────────────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
border: 2px solid;
cursor: pointer;
font-family: var(--font);
font-size: .8em;
font-weight: bold;
letter-spacing: .05em;
text-transform: uppercase;
background: transparent;
transition: all .15s;
}
.btn::before { content: '[ '; }
.btn::after { content: ' ]'; }
.btn:hover { transform: translateY(-2px); }
.btn-primary { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.btn-primary::before { content:'> '; color:var(--amber); }
.btn-primary::after { content:''; }
.btn-primary:hover { background:var(--green-dim); box-shadow:var(--glow); }
.btn-secondary { color:var(--text-dim); border-color:var(--border); }
.btn-secondary:hover { color:var(--green); border-color:var(--green); background:var(--bg-hover); }
.btn-danger { color:var(--red); border-color:rgba(255,68,68,.35); }
.btn-danger:hover { background:var(--red-dim); border-color:var(--red); text-shadow:var(--glow-red); }
.btn-lg { padding:8px 18px; font-size:.85em; }
.btn-sm {
padding: 2px 8px;
font-family: var(--font);
font-size: .7em;
font-weight: bold;
border: 2px solid;
cursor: pointer;
background: transparent;
letter-spacing: .04em;
transition: all .15s;
}
.btn-suppress { color:var(--text-muted); border-color:var(--text-muted); }
.btn-suppress:hover { color:var(--amber); border-color:var(--amber); }
.btn-danger.btn-sm { color:var(--red); border-color:rgba(255,68,68,.35); }
.btn-danger.btn-sm:hover{ color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); }
/* ── Modal ────────────────────────────────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.8);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: var(--bg2);
border: 3px double var(--green);
box-shadow: 0 0 30px rgba(0,255,65,.2), 0 8px 40px rgba(0,0,0,.8);
width: 480px;
max-width: 95vw;
padding: 20px;
position: relative;
}
.modal::before { content:'╔'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:.9rem; line-height:1; }
.modal::after { content:'╗'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:.9rem; line-height:1; }
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
}
.modal-header h3 { font-size:.88em; color:var(--amber); text-shadow:var(--glow-amber); text-transform:uppercase; letter-spacing:.08em; }
.modal-header h3::before { content:'>> '; color:var(--green); }
.modal-close {
background: none;
border: 1px solid var(--border);
cursor: pointer;
font-size: .82em;
color: var(--text-muted);
padding: 2px 8px;
font-family: var(--font);
transition: all .15s;
}
.modal-close:hover { color:var(--red); border-color:var(--red); }
.modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
/* ── Forms ────────────────────────────────────────────────────────── */
.form-card {
background: var(--bg2);
border: 1px solid var(--border);
padding: 16px;
position: relative;
}
.form-card::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.form-card::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.form-row { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:10px; }
.form-row-align { align-items:flex-end; }
.form-group { display:flex; flex-direction:column; gap:4px; min-width:150px; flex:1; }
.form-group-wide{ flex:3; }
.form-group-submit { flex:0 0 auto; min-width:unset; }
.form-group label {
font-size: .7em;
font-weight: bold;
color: var(--amber);
text-transform: uppercase;
letter-spacing: .07em;
text-shadow: var(--glow-amber);
}
.form-group input,
.form-group select {
padding: 6px 9px;
border: 2px solid var(--border);
font-family: var(--font);
font-size: .8em;
background: var(--bg3);
color: var(--text);
transition: border-color .15s, box-shadow .15s;
}
.form-group input::placeholder { color: var(--text-muted); }
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--amber);
box-shadow: 0 0 6px rgba(255,176,0,.18);
}
.form-group select option { background: var(--bg3); color: var(--text); }
.form-hint { font-size:.7em; color:var(--text-muted); margin-top:2px; }
.required { color:var(--red); }
/* ── Duration pills ───────────────────────────────────────────────── */
.duration-pills { display:flex; gap:5px; flex-wrap:wrap; margin-bottom:5px; }
.pill {
padding: 3px 10px;
border: 1px solid var(--border);
background: transparent;
font-family: var(--font);
font-size: .72em;
font-weight: bold;
cursor: pointer;
color: var(--text-muted);
transition: all .15s;
letter-spacing: .04em;
}
.pill:hover { border-color:var(--green); color:var(--green); background:var(--green-dim); }
.pill.active,
.pill-manual.active { border-color:var(--amber); color:var(--amber); background:var(--amber-dim); text-shadow:var(--glow-amber); }
/* ── Targets grid ─────────────────────────────────────────────────── */
.targets-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(180px, 1fr)); gap:10px; }
.target-card {
background: var(--bg2);
border: 1px solid var(--border);
padding: 10px;
position: relative;
}
.target-card::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.78rem; line-height:1; }
.target-card::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.78rem; line-height:1; }
.target-name { font-weight:bold; font-size:.82em; margin-bottom:3px; color:var(--amber); }
.target-type { font-size:.7em; color:var(--text-muted); margin-bottom:6px; }
.target-ifaces{ display:flex; flex-wrap:wrap; gap:3px; }
.iface-chip { font-family:var(--font); font-size:.65em; background:var(--bg3); border:1px solid var(--border); padding:1px 5px; color:var(--text-dim); }
/* ── Toast ────────────────────────────────────────────────────────── */
.toast-container { position:fixed; bottom:20px; right:20px; z-index:300; display:flex; flex-direction:column; gap:7px; }
.toast {
padding: 9px 16px;
border: 1px solid;
font-family: var(--font);
font-size: .8em;
font-weight: bold;
background: var(--bg2);
animation: slide-in .15s ease;
letter-spacing: .04em;
}
.toast::before { content:'>> '; }
.toast-success { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.toast-error { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); }
/* ── Link debug page ──────────────────────────────────────────────── */
.link-host-list { display:flex; flex-direction:column; gap:18px; }
.link-host-panel {
background: var(--bg2);
border: 1px solid var(--border);
position: relative;
}
.link-host-panel::before { content:'╔'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.link-host-panel::after { content:'╗'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.link-host-title {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 16px;
background: var(--bg3);
border-bottom: 1px solid var(--border);
}
.link-host-name { font-weight:bold; font-size:.88em; color:var(--amber); text-shadow:var(--glow-amber); letter-spacing:.05em; }
.link-host-name::before { content:'>> '; color:var(--green); }
.link-host-ip { font-size:.72em; color:var(--text-muted); }
.link-host-upd { font-size:.65em; color:var(--text-muted); margin-left:auto; }
.link-ifaces-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
.link-iface-card {
border-right: 1px solid rgba(0,255,65,.15);
border-bottom: 1px solid rgba(0,255,65,.15);
padding: 12px 14px;
}
.link-iface-card:last-child { border-right:none; }
.link-iface-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(0,255,65,.12);
}
.link-iface-name { font-weight:bold; font-size:.84em; color:var(--amber); text-shadow:var(--glow-amber); flex:1; }
.link-iface-speed { font-size:.75em; color:var(--cyan); text-shadow:var(--glow-cyan); font-weight:bold; }
.link-iface-type { font-size:.65em; color:var(--text-muted); padding:1px 5px; border:1px solid var(--text-muted); letter-spacing:.04em; }
.link-iface-type.type-fibre { color:var(--cyan); border-color:var(--cyan-dim); text-shadow:var(--glow-cyan); }
.link-iface-type.type-copper{ color:var(--green); border-color:var(--border); }
.link-iface-type.type-da { color:var(--amber); border-color:var(--amber-dim); }
/* Link stats 2-column grid */
.link-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 16px;
margin-bottom: 10px;
}
.link-stat { display:flex; flex-direction:column; gap:1px; }
.link-stat-label { font-size:.6em; color:var(--text-muted); text-transform:uppercase; letter-spacing:.07em; }
.link-stat-value { font-size:.78em; font-weight:bold; color:var(--text-dim); }
.val-good { color:var(--green); text-shadow:var(--glow); }
.val-warn { color:var(--orange); }
.val-crit { color:var(--red); text-shadow:var(--glow-red); }
.val-neutral { color:var(--text-muted); }
.val-cyan { color:var(--cyan); text-shadow:var(--glow-cyan); }
/* Traffic bars */
.traffic-section { margin-top:8px; padding-top:8px; border-top:1px solid rgba(0,255,65,.1); }
.traffic-row { display:flex; align-items:center; gap:8px; margin-bottom:5px; }
.traffic-label { font-size:.62em; color:var(--text-muted); width:20px; text-transform:uppercase; letter-spacing:.04em; flex-shrink:0; }
.traffic-bar-track { flex:1; height:5px; background:var(--bg); border:1px solid rgba(0,255,65,.2); position:relative; overflow:hidden; }
.traffic-bar-fill { height:100%; position:absolute; left:0; top:0; transition:width .4s; }
.traffic-tx { background:var(--cyan); box-shadow:0 0 3px rgba(0,255,255,.4); }
.traffic-rx { background:var(--green); box-shadow:0 0 3px rgba(0,255,65,.4); }
.traffic-value { font-size:.7em; color:var(--text-dim); width:68px; text-align:right; flex-shrink:0; }
/* SFP / optical panel */
.sfp-panel {
margin-top: 10px;
padding: 10px 10px 8px;
background: var(--bg3);
border: 1px solid rgba(0,255,255,.2);
position: relative;
}
.sfp-panel::before {
content: '[ SFP / OPTICAL ]';
position: absolute;
top: -8px; left: 10px;
font-size: .6em;
color: var(--cyan);
text-shadow: var(--glow-cyan);
background: var(--bg3);
padding: 0 4px;
letter-spacing: .09em;
font-weight: bold;
}
.sfp-vendor-row { font-size:.7em; color:var(--text-muted); margin-bottom:8px; }
.sfp-vendor-row span { color:var(--text-dim); }
.sfp-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px 12px;
}
.sfp-stat { display:flex; flex-direction:column; gap:1px; }
.sfp-stat-label { font-size:.58em; color:var(--text-muted); text-transform:uppercase; letter-spacing:.07em; }
.sfp-stat-value { font-size:.78em; font-weight:bold; }
/* Power level with bar */
.power-row { display:flex; align-items:center; gap:5px; margin-top:1px; }
.power-track { flex:1; height:3px; background:var(--bg); border:1px solid rgba(0,255,65,.2); position:relative; overflow:hidden; }
.power-fill { height:100%; position:absolute; left:0; top:0; transition:width .4s; }
.power-ok { background:var(--green); box-shadow:0 0 3px var(--green); }
.power-warn { background:var(--orange); }
.power-crit { background:var(--red); box-shadow:0 0 3px var(--red); }
/* Collapsible link panels */
.link-host-title {
cursor: pointer;
user-select: none;
}
.link-host-title:hover { background: rgba(0,255,65,.04); }
.panel-toggle {
font-size: .65em;
color: var(--text-muted);
letter-spacing: .04em;
flex-shrink: 0;
margin-left: 6px;
padding: 0 4px;
border: 1px solid rgba(0,255,65,.2);
}
.link-host-panel.collapsed > .link-ifaces-grid { display: none; }
/* Collapse all / Expand all bar */
.link-collapse-bar {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
/* Link panel states */
.link-no-data { padding:14px; color:var(--text-muted); font-size:.78em; text-align:center; }
.link-loading { padding:20px; text-align:center; color:var(--text-muted); font-size:.8em; }
.link-loading::after { content:' ...'; animation:blink 1s step-end infinite; }
/* Counters (errors/drops) */
.counter-zero { color:var(--green); }
.counter-nonzero { color:var(--red); text-shadow:var(--glow-red); }
/* UniFi switch section divider */
.unifi-section-header {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0 12px;
color: var(--cyan);
font-size: .75em;
letter-spacing: .1em;
text-shadow: var(--glow-cyan);
}
.unifi-section-header::before,
.unifi-section-header::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
}
/* Port badges (UPLINK, PoE, #N) */
.port-badge {
font-size: .58em;
padding: 1px 5px;
border: 1px solid;
letter-spacing: .05em;
font-weight: bold;
vertical-align: middle;
}
.port-badge-uplink { color:var(--amber); border-color:var(--amber-dim); }
.port-badge-poe { color:var(--cyan); border-color:var(--cyan-dim); }
.port-badge-num { color:var(--text-muted); border-color:rgba(0,255,65,.2); }
/* LLDP neighbor + PoE info lines on link debug cards */
.port-lldp {
font-size: .68em;
color: var(--cyan);
text-shadow: var(--glow-cyan);
margin: -4px 0 6px;
letter-spacing: .02em;
}
.port-poe-info {
font-size: .68em;
color: var(--amber);
margin: -4px 0 6px;
letter-spacing: .02em;
}
/* Amber value colour used in inspector */
.val-amber { color:var(--amber); text-shadow:var(--glow-amber); }
/* Down port card — dim everything */
.link-iface-card.port-down {
opacity: .42;
filter: saturate(.3);
}
/* ── Inspector page ───────────────────────────────────────────────── */
/* Layout: main chassis area + collapsible right panel */
.inspector-layout {
display: flex;
gap: 16px;
align-items: flex-start;
min-height: 300px;
}
.inspector-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
/* Switch chassis card */
.inspector-chassis {
background: var(--bg2);
border: 1px solid var(--border);
position: relative;
}
.inspector-chassis::before { content:'╔'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.inspector-chassis::after { content:'╗'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.chassis-header {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--bg3);
border-bottom: 1px solid var(--border);
}
.chassis-name { font-weight:bold; font-size:.88em; color:var(--amber); text-shadow:var(--glow-amber); letter-spacing:.05em; }
.chassis-name::before { content:'>> '; color:var(--green); }
.chassis-ip { font-size:.72em; color:var(--text-muted); }
.chassis-meta { font-size:.65em; color:var(--text-muted); margin-left:auto; }
.chassis-body {
padding: 12px 16px 14px;
}
/* Port rows */
.chassis-rows { display:flex; flex-direction:column; gap:5px; margin-bottom:8px; }
.chassis-row { display:flex; flex-wrap:wrap; gap:4px; }
/* SFP section below main rows */
.chassis-sfp-section {
display: flex;
gap: 6px;
padding-top: 8px;
border-top: 1px solid rgba(0,255,255,.15);
margin-top: 4px;
}
/* Individual port block */
.switch-port-block {
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
font-size: .6em;
font-weight: bold;
border: 1px solid;
cursor: pointer;
transition: box-shadow .1s, border-color .1s, background .1s;
user-select: none;
flex-shrink: 0;
letter-spacing: 0;
}
/* SFP port (in rows — slightly narrower to suggest cage) */
.switch-port-block.sfp-port {
width: 28px;
height: 38px;
font-size: .55em;
}
/* SFP section block (standalone cage) */
.switch-port-block.sfp-block {
width: 44px;
height: 30px;
font-size: .55em;
letter-spacing: .04em;
}
/* State colours */
.switch-port-block.down {
background: var(--bg3);
border-color: rgba(0,255,65,.15);
color: rgba(0,255,65,.25);
}
.switch-port-block.up {
background: rgba(0,255,65,.06);
border-color: var(--green-muted);
color: var(--green);
text-shadow: 0 0 4px rgba(0,255,65,.5);
}
.switch-port-block.up:hover {
background: rgba(0,255,65,.13);
border-color: var(--green);
box-shadow: var(--glow);
}
.switch-port-block.poe-active {
background: var(--amber-dim);
border-color: var(--amber);
color: var(--amber);
text-shadow: 0 0 4px rgba(255,176,0,.5);
}
.switch-port-block.poe-active:hover {
box-shadow: var(--glow-amber);
}
.switch-port-block.uplink {
background: var(--cyan-dim);
border-color: var(--cyan);
color: var(--cyan);
text-shadow: 0 0 4px rgba(0,255,255,.5);
}
.switch-port-block.uplink:hover {
box-shadow: var(--glow-cyan);
}
.switch-port-block.selected {
outline: 2px solid #fff;
outline-offset: 1px;
}
/* Right-side detail panel */
.inspector-panel {
width: 0;
overflow: hidden;
flex-shrink: 0;
transition: width .2s ease;
display: flex;
flex-direction: column;
}
.inspector-panel.open {
width: 310px;
}
.inspector-panel-inner {
width: 310px;
background: var(--bg2);
border: 1px solid var(--border);
padding: 14px 14px 18px;
position: relative;
overflow-y: auto;
max-height: calc(100vh - 120px);
}
.inspector-panel-inner::before { content:'╔'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.inspector-panel-inner::after { content:'╗'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.panel-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.panel-port-name { font-weight:bold; font-size:.92em; color:var(--amber); text-shadow:var(--glow-amber); }
.panel-meta { font-size:.68em; color:var(--text-muted); margin-top:2px; }
.panel-close {
background: none;
border: 1px solid var(--border);
color: var(--text-muted);
cursor: pointer;
font-size: .8em;
padding: 1px 7px;
font-family: var(--font);
flex-shrink: 0;
transition: all .15s;
}
.panel-close:hover { color:var(--red); border-color:var(--red); }
.panel-section-title {
font-size: .62em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
text-transform: uppercase;
letter-spacing: .1em;
margin: 10px 0 5px;
padding-bottom: 3px;
border-bottom: 1px solid rgba(0,255,65,.12);
}
.panel-section-title:first-of-type { margin-top: 0; }
.panel-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 2px 0;
}
.panel-label { font-size:.68em; color:var(--text-muted); text-transform:uppercase; letter-spacing:.05em; flex-shrink:0; }
.panel-val { font-size:.75em; font-weight:bold; color:var(--text-dim); text-align:right; word-break:break-all; }
/* Path debug two-column layout */
.path-conn-type {
font-size: .68em;
color: var(--cyan);
font-weight: normal;
margin-left: 6px;
text-shadow: none;
text-transform: none;
letter-spacing: normal;
}
.path-debug-cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 6px;
}
.path-col {
background: var(--bg3);
border: 1px solid rgba(0,255,65,.18);
padding: 7px 8px;
}
.path-col-header {
font-size: .62em;
font-weight: bold;
color: var(--amber);
margin-bottom: 5px;
padding-bottom: 3px;
border-bottom: 1px solid rgba(0,255,65,.15);
letter-spacing: .04em;
}
.path-row {
display: flex;
justify-content: space-between;
gap: 4px;
font-size: .65em;
padding: 1px 0;
}
.path-row span:first-child { color:var(--text-muted); flex-shrink:0; }
.path-row span:last-child { color:var(--text-dim); font-weight:bold; text-align:right; word-break:break-all; }
.path-dom {
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid rgba(0,255,255,.15);
}
.path-dom-row {
display: flex;
justify-content: space-between;
font-size: .65em;
padding: 1px 0;
color: var(--cyan);
}
.path-dom-row span:first-child { color:var(--text-muted); }
/* ── Link Diagnostics ─────────────────────────────────────────────── */
.diag-bar {
display: flex;
align-items: center;
gap: 10px;
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.btn-diag {
font-family: var(--font);
font-size: .65em;
color: var(--cyan);
background: transparent;
border: 1px solid var(--cyan);
padding: 4px 10px;
cursor: pointer;
letter-spacing: .04em;
transition: background .15s, box-shadow .15s;
animation: diag-pulse 2.5s ease-in-out infinite;
}
.btn-diag:hover {
background: var(--cyan-dim);
box-shadow: var(--glow-cyan);
}
@keyframes diag-pulse {
0%, 100% { box-shadow: none; }
50% { box-shadow: 0 0 6px rgba(0,255,255,.4); }
}
.diag-status {
font-size: .6em;
color: var(--text-muted);
font-style: italic;
}
.diag-error {
color: var(--red);
font-size: .65em;
margin-top: 8px;
}
.diag-results {
margin-top: 4px;
}
.diag-results-inner {
display: flex;
flex-direction: column;
gap: 6px;
}
/* Health banner */
.diag-health-banner {
display: flex;
gap: 8px;
padding: 6px 0 4px;
margin-bottom: 2px;
}
.diag-health-critical {
background: var(--red-dim);
color: var(--red);
border: 1px solid var(--red);
padding: 2px 8px;
font-size: .62em;
font-weight: bold;
letter-spacing: .05em;
}
.diag-health-warning {
background: var(--amber-dim);
color: var(--amber);
border: 1px solid var(--amber);
padding: 2px 8px;
font-size: .62em;
font-weight: bold;
letter-spacing: .05em;
}
.diag-health-ok {
background: var(--green-dim);
color: var(--green);
border: 1px solid var(--green);
padding: 2px 8px;
font-size: .62em;
font-weight: bold;
letter-spacing: .05em;
}
/* Issue list */
.diag-issue-list {
display: flex;
flex-direction: column;
gap: 3px;
}
.diag-issue-row {
font-size: .62em;
padding: 3px 6px;
background: var(--bg2);
border-left: 2px solid var(--border);
line-height: 1.4;
}
.diag-code {
font-weight: bold;
color: var(--amber);
}
/* Sections */
.diag-section {
background: var(--bg2);
border: 1px solid rgba(0,255,65,.12);
}
.diag-section-header {
font-size: .62em;
font-weight: bold;
color: var(--amber);
padding: 4px 8px;
letter-spacing: .04em;
border-bottom: 1px solid rgba(0,255,65,.12);
background: rgba(255,176,0,.04);
}
/* Collapsible sections */
.diag-collapsible .diag-section-body {
display: none;
}
.diag-collapsible.diag-open .diag-section-body {
display: block;
}
.diag-toggle {
cursor: pointer;
user-select: none;
}
.diag-toggle-hint {
font-weight: normal;
color: var(--text-muted);
font-size: .9em;
}
.diag-collapsible.diag-open .diag-toggle-hint::after {
content: '';
}
/* Data tables */
.diag-table {
width: 100%;
border-collapse: collapse;
font-size: .62em;
}
.diag-table td {
padding: 3px 8px;
vertical-align: top;
}
.diag-table td:first-child {
color: var(--text-muted);
width: 40%;
white-space: nowrap;
}
.diag-table td:last-child {
color: var(--text-dim);
font-weight: bold;
word-break: break-all;
}
.diag-table tr:nth-child(even) {
background: rgba(0,255,65,.025);
}
/* Value colour classes */
.diag-val-good { color: var(--green); }
.diag-val-warn { color: var(--amber); }
.diag-val-bad { color: var(--red); }
/* SFP power bar */
.diag-power-bar-wrap {
position: relative;
display: inline-block;
width: 60px;
height: 7px;
background: var(--bg3);
border: 1px solid var(--border);
vertical-align: middle;
margin-left: 6px;
overflow: visible;
}
.diag-power-bar {
display: inline-block;
position: absolute;
left: 0;
top: 0;
height: 100%;
}
.diag-power-bar.diag-val-good { background: var(--green); }
.diag-power-bar.diag-val-warn { background: var(--amber); }
.diag-power-bar.diag-val-bad { background: var(--red); }
.diag-power-zone-warn,
.diag-power-zone-crit {
position: absolute;
top: -2px;
width: 1px;
height: calc(100% + 4px);
pointer-events: none;
}
.diag-power-zone-warn { background: var(--amber); opacity: .7; }
.diag-power-zone-crit { background: var(--red); opacity: .7; }
/* ethtool -S stat table */
.diag-stat-table {
width: 100%;
border-collapse: collapse;
font-size: .58em;
}
.diag-stat-table td {
padding: 2px 8px;
}
.diag-stat-table td:first-child { color: var(--text-muted); }
.diag-stat-table td:last-child { color: var(--text-dim); text-align: right; }
.diag-stat-nonzero-warn {
background: var(--amber-dim);
}
.diag-stat-nonzero-warn td { color: var(--amber); }
/* dmesg */
.diag-dmesg-wrap {
max-height: 200px;
overflow-y: auto;
padding: 6px 8px;
}
.diag-dmesg-line {
font-family: var(--font);
font-size: .58em;
white-space: pre-wrap;
word-break: break-all;
padding: 1px 0;
color: var(--text-dim);
}
.diag-dmesg-warn { color: var(--amber); }
.diag-dmesg-err { color: var(--red); }
/* Pulse link */
.diag-pulse-link {
font-size: .62em;
padding: 4px 0;
text-align: right;
}
.diag-pulse-link a {
color: var(--cyan);
text-decoration: none;
}
.diag-pulse-link a:hover {
text-shadow: var(--glow-cyan);
}
/* ── Topology v2 professional network diagram ──────────────────── */
/* Outer wrapper */
.topo-v2 {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
min-width: 860px;
padding: 20px 24px 24px;
position: relative;
}
/* Each tier row */
.topo-tier {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
/* ── Vertical connector section between tiers ── */
.topo-vc {
display: flex;
justify-content: center;
align-items: flex-start;
width: 100%;
position: relative;
height: 40px;
}
/* Single centered vertical wire */
.topo-vc-wire {
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
width: 2px;
height: 100%;
background: linear-gradient(to bottom, var(--cyan), var(--green));
opacity: .7;
}
/* Labeled vertical connector */
.topo-vc-label {
position: absolute;
left: calc(50% + 6px);
top: 50%;
transform: translateY(-50%);
font-size: .58em;
color: var(--amber);
text-shadow: var(--glow-amber);
white-space: nowrap;
letter-spacing: .06em;
font-family: var(--font);
}
/* ── WAN tier node (Internet + Router side by side) ── */
.topo-tier-wan {
gap: 0;
flex-direction: column;
align-items: center;
}
/* ── Individual node boxes ── */
.topo-v2-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 8px 16px;
border: 1px solid var(--border);
background: var(--bg3);
position: relative;
font-size: .75em;
font-family: var(--font);
min-width: 110px;
text-align: center;
transition: border-color .2s, box-shadow .2s;
}
.topo-v2-node::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; pointer-events:none; }
.topo-v2-node::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; pointer-events:none; }
.topo-v2-icon { font-size:1.3em; line-height:1; }
.topo-v2-label { font-weight:bold; letter-spacing:.04em; }
.topo-v2-sub { font-size:.58em; color:var(--text-muted); letter-spacing:.02em; }
.topo-v2-vlan { font-size:.54em; color:var(--cyan); opacity:.75; letter-spacing:.02em; }
/* Node type colours */
.topo-v2-internet { border-color:var(--cyan); color:var(--cyan); text-shadow:var(--glow-cyan); }
.topo-v2-router { border-color:var(--cyan); color:var(--cyan); text-shadow:var(--glow-cyan); }
.topo-v2-switch { border-color:var(--amber); color:var(--amber); text-shadow:var(--glow-amber); }
.topo-v2-host { border-color:var(--border); color:var(--text); cursor:default; }
/* ── CSS fork: UDM-Pro → two switches, no SVG distortion ── */
/* The fork sits between the router tier and the switch row.
Drops are at left:25% and left:75%, matching each switch's
centre (each switch lives in a 50%-wide half). */
.topo-fork {
position: relative;
width: 100%;
height: 40px;
flex-shrink: 0;
}
/* Vertical stem down from router centre */
.topo-fork-stem {
position: absolute;
left: 50%;
top: 0;
width: 2px;
height: 50%;
transform: translateX(-50%);
background: var(--amber);
opacity: .65;
}
/* Horizontal bar at mid-height, spanning between the two drop points */
.topo-fork-bar {
position: absolute;
left: 25%;
right: 25%;
top: calc(50% - 1px);
height: 2px;
background: var(--amber);
opacity: .55;
display: flex;
justify-content: center;
}
.topo-fork-label {
position: absolute;
top: -13px;
font-size: .54em;
color: var(--amber);
white-space: nowrap;
letter-spacing: .06em;
font-family: var(--font);
opacity: .85;
background: var(--bg);
padding: 0 4px;
text-shadow: var(--glow-amber);
}
/* Left and right vertical drops from bar down to switch tops */
.topo-fork-drop {
position: absolute;
top: 50%;
width: 2px;
height: 50%;
background: var(--amber);
opacity: .55;
}
.topo-fork-drop-l { left: 25%; transform: translateX(-50%); }
.topo-fork-drop-r { left: 75%; transform: translateX(-50%); }
/* ── Switch row: two equal 50% halves ── */
/* Each switch is centred in its half, so their centres are at
exactly 25% and 75% — matching the fork drops above. */
.topo-sw-row {
display: flex;
width: 100%;
position: relative;
align-items: center;
}
.topo-sw-half {
width: 50%;
display: flex;
justify-content: center;
padding: 0 16px;
position: relative;
z-index: 1; /* sit above the ISL line */
}
/* ISL line rendered as ::before — switch boxes (bg3) cover it at their edges */
.topo-sw-row::before {
content: '';
position: absolute;
left: 25%;
right: 25%;
top: 50%;
height: 2px;
background: var(--amber);
opacity: .35;
z-index: 0;
}
/* ISL label centred between the two switches */
.topo-sw-row::after {
content: '10G ISL';
position: absolute;
left: 50%;
transform: translateX(-50%);
top: calc(50% - 14px);
font-size: .5em;
color: var(--amber);
white-space: nowrap;
font-family: var(--font);
letter-spacing: .06em;
opacity: .65;
background: var(--bg);
padding: 0 5px;
z-index: 2;
}
/* ── Dual-home bus section ── */
/* This is the complex section linking two switches to N hosts */
.topo-bus-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
margin-top: 0;
}
/* Bus bar row: the horizontal rail that distributes to hosts */
.topo-bus-bars {
display: flex;
flex-direction: column;
align-items: stretch;
position: relative;
width: 100%;
}
/* The two drop buses: 10G (green) and 1G mgmt (amber dashed) */
.topo-bus-10g {
display: flex;
align-items: center;
position: relative;
height: 20px;
}
.topo-bus-10g-line {
flex: 1;
height: 2px;
background: var(--green);
opacity: .45;
margin: 0 4px;
}
.topo-bus-10g-label {
font-size: .56em;
color: var(--green);
text-shadow: var(--glow);
white-space: nowrap;
letter-spacing: .05em;
font-family: var(--font);
opacity: .85;
padding: 0 8px;
}
.topo-bus-1g {
display: flex;
align-items: center;
position: relative;
height: 18px;
}
.topo-bus-1g-line {
flex: 1;
height: 0;
border-top: 2px dashed var(--amber);
opacity: .35;
margin: 0 4px;
}
.topo-bus-1g-label {
font-size: .56em;
color: var(--amber);
text-shadow: var(--glow-amber);
white-space: nowrap;
letter-spacing: .05em;
font-family: var(--font);
opacity: .8;
padding: 0 8px;
}
/* ── Host row ── */
.topo-v2-hosts {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
padding-top: 4px;
width: 100%;
}
/* Host status colouring */
.topo-v2-status-up { border-color:var(--green); box-shadow:0 0 8px rgba(0,255,65,.2); }
.topo-v2-status-down { border-color:var(--red); box-shadow:0 0 8px rgba(255,68,68,.35); animation:pulse-glow 2s infinite; }
.topo-v2-status-degraded{ border-color:var(--orange); box-shadow:0 0 8px rgba(255,140,0,.2); }
.topo-v2-status-unknown { border-color:var(--border); }
/* Off-rack host: dashed border */
.topo-v2-offrack { border-style: dashed !important; }
/* ── Legend row ── */
.topo-legend {
display: flex;
gap: 18px;
align-items: center;
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid rgba(0,255,65,.12);
flex-wrap: wrap;
justify-content: center;
}
.topo-legend-item {
display: flex;
align-items: center;
gap: 5px;
font-size: .58em;
color: var(--text-muted);
font-family: var(--font);
}
.topo-legend-line-10g {
width: 24px; height: 2px;
background: var(--green);
display: inline-block;
}
.topo-legend-line-1g {
width: 24px; height: 0;
border-top: 2px dashed var(--amber);
display: inline-block;
}
.topo-legend-line-isl {
width: 24px; height: 2px;
background: var(--amber);
display: inline-block;
}
.topo-legend-line-wan {
width: 24px; height: 2px;
background: linear-gradient(to right, var(--cyan), var(--green));
display: inline-block;
}
/* ── Drop-wire stubs for host dual-homing ── */
/* Wrapper that sits above each host showing its two connections */
.topo-v2-host-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
.topo-v2-host-wires {
display: flex;
gap: 6px;
height: 28px;
align-items: flex-start;
}
.topo-v2-wire-10g {
width: 2px;
height: 100%;
background: var(--green);
opacity: .55;
}
.topo-v2-wire-1g {
width: 0;
height: 100%;
border-left: 2px dashed var(--amber);
opacity: .45;
}
/* host badge */
.topo-v2-badge {
font-size: .65em;
padding: 1px 5px;
border: 1px solid;
letter-spacing: .03em;
margin-top: 2px;
}
.topo-v2-badge-up { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.topo-v2-badge-down { color:var(--red); border-color:var(--red); animation:pulse-glow 1.5s infinite; }
.topo-v2-badge-degraded{ color:var(--orange); border-color:var(--orange); }
.topo-v2-badge-unknown { color:var(--text-muted); border-color:var(--border); }
/* (removed: old SVG fork — replaced by .topo-fork CSS above) */
/* ── Drop wires from each switch down to the bus rails ── */
.topo-sw-drops {
position: relative;
width: 100%;
height: 20px;
flex-shrink: 0;
}
.topo-sw-drop {
position: absolute;
top: 0;
width: 2px;
height: 100%;
opacity: .5;
}
.topo-sw-drop-l {
left: 25%;
transform: translateX(-50%);
background: var(--green);
}
.topo-sw-drop-r {
left: 75%;
transform: translateX(-50%);
background: var(--amber);
border-left: 2px dashed var(--amber);
width: 0;
background: none;
}
/* ── Improved chassis legend ── */
.chassis-legend {
display: flex;
gap: 16px;
align-items: center;
padding: 7px 16px 8px;
border-top: 1px solid rgba(0,255,65,.1);
background: var(--bg2);
flex-wrap: wrap;
}
.chassis-legend-item {
display: flex;
align-items: center;
gap: 5px;
font-size: .58em;
color: var(--text-muted);
font-family: var(--font);
letter-spacing: .04em;
text-transform: uppercase;
}
.chassis-legend-swatch {
width: 14px;
height: 14px;
border: 1px solid;
flex-shrink: 0;
display: inline-block;
}
.cls-down { background:var(--bg3); border-color:rgba(0,255,65,.15); }
.cls-up { background:rgba(0,255,65,.06); border-color:var(--green-muted); }
.cls-poe { background:var(--amber-dim); border-color:var(--amber); }
.cls-uplink { background:var(--cyan-dim); border-color:var(--cyan); }
/* ── Port block v2: flex-col with speed sub-label ── */
.switch-port-block {
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1px;
padding: 2px 1px;
}
.port-num {
line-height: 1;
font-weight: bold;
}
.port-speed {
font-size: .72em;
opacity: .7;
line-height: 1;
font-weight: normal;
}
.port-lldp {
font-size: .62em;
opacity: .65;
line-height: 1;
max-width: 32px;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
font-weight: normal;
}
/* US24PRO port group separators (every 2 ports = 1 pair gap) */
.chassis-row.us24pro-row .switch-port-block:nth-child(2n+1):not(:first-child) {
margin-left: 6px;
}
/* SFP block: taller and narrower cage look */
.switch-port-block.sfp-block {
width: 36px;
height: 38px;
font-size: .55em;
letter-spacing: .04em;
border-left-width: 3px;
}
/* SFP port in rows */
.switch-port-block.sfp-port {
width: 26px;
height: 40px;
font-size: .55em;
border-left-width: 2px;
}
/* Chassis mounting ears */
.chassis-body {
position: relative;
}
.chassis-ear-l,
.chassis-ear-r {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 36px;
background: var(--bg3);
border: 1px solid var(--border);
}
.chassis-ear-l { left: -9px; border-right: none; }
.chassis-ear-r { right: -9px; border-left: none; }
.chassis-ear-l::before,
.chassis-ear-r::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--border);
}
/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 768px) {
.host-grid { grid-template-columns:1fr; }
.topology { display:none; }
.form-row { flex-direction:column; }
.status-bar { flex-direction:column; align-items:flex-start; }
.link-ifaces-grid { grid-template-columns:1fr; }
.sfp-grid { grid-template-columns:1fr 1fr; }
.header-nav { display:none; }
.inspector-layout { flex-direction:column; }
.inspector-panel.open { width:100%; }
.inspector-panel-inner { width:100%; }
}
/* ── Stale monitoring banner ──────────────────────────────────────── */
.stale-banner {
background: var(--amber-dim);
border: 1px solid var(--amber);
border-left: 4px solid var(--amber);
color: var(--amber);
padding: 10px 16px;
margin: 12px 0 0;
font-size: 0.88em;
font-family: var(--font);
border-radius: 2px;
}
/* ── Link alert badges (error/flap indicators) ────────────────────── */
.link-alert-badge {
display: inline-block;
font-size: .6em;
font-weight: bold;
padding: 1px 5px;
border-radius: 2px;
background: var(--red-dim);
color: var(--red);
border: 1px solid var(--red);
margin-left: 4px;
vertical-align: middle;
letter-spacing: .05em;
}
.link-alert-badge.link-alert-amber {
background: var(--amber-dim);
color: var(--amber);
border-color: var(--amber);
}
/* ── PoE utilisation bar ──────────────────────────────────────────── */
.poe-bar-track {
height: 3px;
background: var(--bg3);
border-radius: 2px;
margin-top: 3px;
overflow: hidden;
}
.poe-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.4s ease;
}
.poe-bar-ok { background: var(--green); }
.poe-bar-warn { background: var(--amber); }
.poe-bar-crit { background: var(--red); }
/* ── Path mismatch alert ──────────────────────────────────────────── */
.path-mismatch-alert {
background: var(--amber-dim);
border-left: 3px solid var(--amber);
color: var(--amber);
padding: 4px 8px;
margin-bottom: 6px;
font-size: .72em;
border-radius: 2px;
}
/* ── Error state for data containers ─────────────────────────────── */
.error-state {
padding: 16px 20px;
border-left: 3px solid var(--red);
background: var(--red-dim);
color: var(--red);
border-radius: 2px;
font-size: .88em;
}
/* ── Link health summary panel ────────────────────────────────────── */
.link-summary-panel {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 2px;
padding: 12px 16px;
margin-bottom: 12px;
}
.link-summary-panel.link-summary-has-alerts {
border-color: var(--amber);
}
.link-summary-grid {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-end;
}
.link-summary-stat {
min-width: 80px;
}
.link-summary-stat.lss-alert .lss-label {
color: var(--amber);
}
.lss-label {
display: block;
font-size: .62em;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: .05em;
margin-bottom: 2px;
}
.lss-value {
font-size: 1.2em;
font-weight: bold;
color: var(--text);
}
.lss-sub {
font-size: .7em;
color: var(--text-muted);
font-weight: normal;
}
/* ── Recently resolved table ──────────────────────────────────────── */
.row-resolved td {
opacity: 0.75;
}
.badge-resolved {
background: var(--bg3);
color: var(--text-muted);
border-color: var(--border);
text-decoration: line-through;
}
.section-badge-resolved {
background: var(--bg3);
color: var(--text-muted);
border: 1px solid var(--border);
font-size: .65em;
padding: 2px 7px;
border-radius: 10px;
}