Files
gandalf/static/style.css
Jared Vititoe 3dce602938 Redesign topology diagram with dual-homed bus layout and improve inspector chassis
- Replace flat topology with tiered bus-bar layout: Internet → UDM-Pro → SVG fork → USW-Agg + Pro 24 PoE → dual-homed servers
- Show 10G VLAN90 (Ceph) bus from USW-Agg and 1G DHCP management bus from Pro 24 PoE per host
- Add per-host drop wires (solid 10G + dashed 1G) with correct rack positions
- Mark large1 as off-rack (dashed border), ZimaBoards as off-rack mon-01/mon-02
- Add topology legend, inter-switch 10G ISL indicator
- Add recently resolved events section (last 24h) to dashboard
- Add last_seen column and relative timestamps to events table
- Add stale data banner when monitoring data >15 min old
- Improve inspector chassis with port speed labels, LLDP neighbor info, mounting ears, chassis legend
- Add duplex/speed mismatch warnings and carrier changes to path debug panel
- Bump updateTopology() to handle both topo-v2-status-* and topo-status-* classes

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

2127 lines
62 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ══════════════════════════════════════════════════════════════════════
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; }
/* ── Switch tier: both switches with inter-switch link ── */
.topo-switch-pair {
display: flex;
align-items: center;
gap: 0;
}
.topo-isl {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 12px;
gap: 4px;
}
.topo-isl-wire {
width: 80px;
height: 2px;
background: linear-gradient(to right, var(--amber), var(--amber));
opacity: .6;
position: relative;
}
.topo-isl-wire::before,
.topo-isl-wire::after {
content: '';
position: absolute;
top: -3px;
width: 2px;
height: 8px;
background: var(--amber);
opacity: .6;
}
.topo-isl-wire::before { left: 0; }
.topo-isl-wire::after { right: 0; }
.topo-isl-label {
font-size: .54em;
color: var(--amber);
opacity: .75;
white-space: nowrap;
letter-spacing: .05em;
font-family: var(--font);
}
/* ── 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); }
/* vertical connector from router to switch tier */
.topo-v2-fork {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
position: relative;
height: 50px;
}
/* Fork tree SVG lines from UDM-Pro down to two switches */
.topo-v2-fork svg {
width: 100%;
height: 100%;
overflow: visible;
}
/* ── 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;
}