Files
gandalf/static/style.css
Jared Vititoe fa7512a2c2 feat: terminal aesthetic rewrite + link debug page
- Full dark terminal aesthetic (Pulse/TinkerTickets style):
  - #0a0a0a background, #00ff41 green, #ffb000 amber, #00ffff cyan
  - CRT scanline overlay, phosphor glow, ASCII corner pseudoelements
  - Bracket-notation badges [CRITICAL], monospace font throughout
  - style.css, base.html, index.html, suppressions.html all rewritten

- New Link Debug page (/links, /api/links):
  - Per-host, per-interface cards with speed/duplex/port type/auto-neg
  - Traffic bars (TX cyan, RX green) with rate labels
  - Error/drop counters, carrier change history
  - SFP/DOM optical panel: vendor, temp, voltage, bias, TX/RX power dBm bars
  - RX-TX delta shown; color-coded warn/crit thresholds
  - Auto-refresh every 60s, anchor-jump to #hostname

- LinkStatsCollector in monitor.py:
  - SSHes to each host (one connection, all ifaces batched)
  - Parses ethtool + ethtool -m (SFP DOM) output
  - Merges with Prometheus traffic/error/carrier metrics
  - Stores as link_stats in monitor_state table

- config.json: added ssh section for ethtool collection
- app.js: terminal chip style consistency (uppercase, ● bullet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:43:11 -05:00

810 lines
32 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: #008822;
--font: 'Courier New','Consolas','Monaco','Menlo',monospace;
--glow: 0 0 5px #00ff41, 0 0 10px rgba(0,255,65,.4);
--glow-xl: 0 0 8px #00ff41, 0 0 20px rgba(0,255,65,.35);
--glow-amber: 0 0 5px #ffb000, 0 0 10px rgba(255,176,0,.4);
--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);
}
/* ── 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: 13px;
line-height: 1.5;
min-height: 100vh;
position: relative;
animation: flicker .25s ease-in-out 45s infinite;
}
/* CRT scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
rgba(0,0,0,.13) 0px, rgba(0,0,0,.13) 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, .nav-link.active {
color: var(--green);
border-color: var(--border);
background: var(--green-dim);
text-shadow: var(--glow);
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-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; }
/* ── 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: 6px 14px;
border: 1px 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:hover { transform: translateY(-1px); }
.btn-primary { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.btn-primary::before { content:'> '; color:var(--amber); }
.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: 1px 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: 1px solid var(--green);
box-shadow: 0 0 30px rgba(0,255,65,.18);
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: 1px 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); }
/* 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); }
/* ── 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; }
}