Files
gandalf/static/style.css
T
jared 9d6583a08a
Lint / Python (flake8) (push) Successful in 1m13s
Lint / JS (eslint) (push) Successful in 9s
Security / Python Security (bandit) (push) Failing after 45s
Test / Python Tests (pytest) (push) Successful in 57s
Lint / Notify on failure (push) Has been skipped
Lint / Deploy (push) Successful in 5s
Add LDAP avatar photos, UX polish, and TDS component upgrades
- Add /api/avatar endpoint querying lldap for user jpegPhoto; disk cache
  with sentinel pattern avoids repeat LDAP hits for users without photos
- Add ldap3 dependency and ldap config block to config.json
- Wire lt-avatar img overlay in base.html with capture-phase error
  fallback (lt-avatar-img-err) to reveal initials when image is absent
- Fix lt-avatar CSS shim: position:relative + absolute inset on img
  (local base.css was missing these; added to style.css)
- Replace all empty-state paragraphs with proper lt-empty-state markup
  (icon + title + body) across index, suppressions, inspector, app.js
- Add lt-spinner--cyan next to refresh button; shows during refreshAll()
- Replace inspector panel-section-title with lt-divider throughout
- Add data-tooltip attributes to SFP DOM metrics, TX/RX/Carrier/Duplex/
  Auto-neg/Error labels in links.html and inspector panel
- Add tooltips to events table column headers (Sev, First Seen, Failures)
- Fix links.html host panel timestamp (was reading sample.updated which
  is always undefined; now uses data.updated)
- Fix UniFi status text casing (Online→ONLINE to match server render)
- Remove dead topo-status-* class manipulation from updateTopology()
- Always render alert-count-badge; toggle display:none when count is 0
- Fix double UniFi get_devices() call in monitor.py run loop
- Fix chip-critical animation (was using green pulse-glow; now red)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 21:09:56 -04:00

1010 lines
42 KiB
CSS
Raw Permalink 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 App-specific styles (extends LotusGuild base.css)
base.css handles: body, header, nav, tables, buttons, forms, modals,
badges, toasts, layout shell, and the full design token system.
This file adds only gandalf-specific components.
══════════════════════════════════════════════════════════════════════ */
/* ── lt-avatar image overlay (base.css compat shim) ───────────────── */
/* Older base.css missing position:relative + position:absolute on img */
.lt-avatar { position: relative; }
.lt-avatar img { position: absolute; inset: 0; }
.lt-avatar img.lt-avatar-img-err { display: none; }
/* ── Variable aliases bridging to base.css palette ────────────────── */
:root {
/* Short names used throughout custom components */
--green: var(--accent-green);
--amber: var(--accent-amber);
--cyan: var(--accent-cyan);
--red: var(--accent-red);
--orange: var(--accent-orange);
--bg: var(--bg-primary);
--bg2: var(--bg-secondary);
--bg3: var(--bg-tertiary);
--text: var(--text-primary);
--text-dim: var(--text-secondary);
--border: var(--border-color);
--font: var(--font-mono);
--bg-hover: rgba(255,107,0,.06);
/* Dim / alpha variants */
--green-dim: rgba(0,255,136,.10);
--green-muted: rgba(0,255,136,.30);
--amber-dim: rgba(255,179,0,.12);
--cyan-dim: rgba(0,212,255,.12);
--red-dim: rgba(255,45,85,.12);
--orange-dim: rgba(255,107,0,.12);
/* Glow shadows */
--glow: 0 0 5px var(--accent-green), 0 0 10px rgba(0,255,136,.4);
--glow-amber: 0 0 5px var(--accent-amber), 0 0 10px rgba(255,179,0,.35);
--glow-red: 0 0 5px var(--accent-red), 0 0 10px rgba(255,45,85,.3);
--glow-cyan: 0 0 5px var(--accent-cyan), 0 0 10px rgba(0,212,255,.3);
--glow-xl: 0 0 8px var(--accent-green), 0 0 20px rgba(0,255,136,.5);
}
/* ── Light theme overrides for dim/glow variables ────────────────── */
[data-theme="light"] {
--green-dim: rgba(0,160,80,.08);
--green-muted: rgba(0,160,80,.45);
--amber-dim: rgba(180,120,0,.07);
--cyan-dim: rgba(0,140,180,.08);
--red-dim: rgba(200,30,60,.06);
--orange-dim: rgba(180,80,0,.06);
--glow: none;
--glow-amber: none;
--glow-red: none;
--glow-cyan: none;
--glow-xl: none;
}
[data-theme="light"] .topology {
background-image: radial-gradient(circle, rgba(0,100,160,0.07) 1px, transparent 1px);
}
[data-theme="light"] .topo-vc-label {
background: rgba(235,238,242,.88);
}
/* ── Header overlap fix ───────────────────────────────────────────
.lt-container's padding shorthand resets padding-top, defeating
.lt-main's padding-top. The combined selector restores it. */
.lt-main.lt-container {
padding-top: calc(var(--header-height) + var(--space-lg));
}
@media (max-height: 500px) and (orientation: landscape) {
.lt-main.lt-container { padding-top: calc(42px + var(--space-md)); }
}
@media (max-width: 767px) {
.lt-main.lt-container { padding-top: calc(50px + var(--space-md)); }
}
@media (max-width: 479px) {
.lt-main.lt-container { padding-top: calc(46px + var(--space-sm)); }
}
/* ── Refresh button loading state ────────────────────────────────── */
[data-action="refresh"].is-loading {
opacity: .5;
pointer-events: none;
cursor: wait;
}
[data-action="refresh"].is-loading::after {
content: '…';
}
/* ── Animations used by custom components ─────────────────────────── */
@keyframes pulse-red {
0%,100% { box-shadow: 0 0 0 0 rgba(255,45,85,.5); }
50% { box-shadow: 0 0 6px 3px rgba(255,45,85,.2); }
}
@keyframes pulse-glow {
0%,100% { text-shadow: var(--glow); }
50% { text-shadow: var(--glow-xl); }
}
@keyframes blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
@keyframes diag-pulse {
0%, 100% { box-shadow: none; }
50% { box-shadow: 0 0 6px rgba(0,212,255,.4); }
}
/* ── Section / page headers ───────────────────────────────────────── */
.g-section { margin-bottom: 26px; }
.g-section-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
}
.g-section-title {
font-size: .88em;
font-weight: bold;
color: var(--text-accent);
text-transform: uppercase;
letter-spacing: .08em;
}
.g-section-badge {
font-size: .72em;
font-weight: bold;
color: var(--accent-red);
border: 1px solid var(--accent-red);
padding: 1px 7px;
}
.g-section-badge-resolved {
font-size: .68em;
color: var(--text-muted);
border: 1px solid var(--border-color);
padding: 1px 7px;
}
.g-section-actions { margin-left: auto; }
.events-filter-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.events-filter-bar .lt-input-sm { width: 220px; }
.sev-pills { display: flex; gap: 4px; }
.g-page-header { margin-bottom: 20px; }
.g-page-title {
font-size: 1em;
font-weight: bold;
color: var(--text-accent);
letter-spacing: .06em;
}
.g-page-sub { font-size: .78em; color: var(--text-muted); margin-top: 4px; }
/* ── Badge severity color variants (used with lt-badge) ───────────── */
.badge-critical { color: var(--red); border-color: var(--red); text-shadow: var(--glow-red); }
.badge-warning { color: var(--orange); border-color: var(--orange); }
.badge-high { color: var(--orange); border-color: var(--orange); }
.badge-info { color: var(--cyan); border-color: var(--cyan); }
.badge-ok { color: var(--green); border-color: var(--green-muted); }
.badge-neutral { color: var(--text-muted); border-color: var(--text-muted); }
.badge-resolved { color: var(--text-muted); border-color: var(--border-color); text-decoration: line-through; }
.badge-suppressed { font-size: .9em; padding: 0; border: none; color: var(--text-muted); }
.badge-purple { color: var(--accent-purple); border-color: var(--accent-purple); }
/* ── Table row state colors ───────────────────────────────────────── */
.lt-table tr.row-critical td { background: rgba(255,45,85,.04); }
.lt-table tr.row-critical td:first-child { border-left: 2px solid var(--red); }
.lt-table tr.row-warning td { background: rgba(255,107,0,.04); }
.lt-table tr.row-warning td:first-child { border-left: 2px solid var(--orange); }
.lt-table tr.row-resolved td { opacity: .65; }
/* ── Table size modifier ─────────────────────────────────────────── */
.lt-table-sm th,
.lt-table-sm td { padding: 4px 8px; font-size: .78em; }
/* ── Misc table helpers ───────────────────────────────────────────── */
.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; }
.pagination-notice { font-size: .8em; color: var(--text-muted); padding: 6px 0 8px; }
.pagination-notice a { color: var(--amber); }
/* ── Status bar ───────────────────────────────────────────────────── */
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg2);
border: 1px solid var(--border-color);
padding: 9px 16px;
margin-bottom: 18px;
gap: 12px;
flex-wrap: wrap;
}
.status-chips { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }
.chip {
font-size: .78em;
font-weight: bold;
padding: 3px 10px;
border: 1px solid;
letter-spacing: .04em;
}
.chip-critical { color: var(--red); border-color: var(--red); text-shadow: var(--glow-red); animation: topo-pulse-down 2s ease-in-out infinite; }
.chip-warning { color: var(--orange); border-color: var(--orange); }
.chip-ok { color: var(--green); border-color: var(--green-muted); 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); }
/* ── Stale monitoring banner ──────────────────────────────────────── */
/* .stale-banner replaced by lt-alert--warning */
/* ── Error / empty state containers ───────────────────────────────── */
.error-state {
padding: 16px 20px;
border-left: 3px solid var(--red);
background: var(--red-dim);
color: var(--red);
font-size: .88em;
}
/* ── 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;
vertical-align: middle;
}
.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; }
/* ── Host cards ───────────────────────────────────────────────────── */
.host-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
gap: 12px;
margin-top: 14px;
}
.host-card {
background: var(--bg2);
border: 1px solid var(--border-color);
padding: 12px;
position: relative;
overflow: hidden;
transition: border-color .2s, box-shadow .2s;
}
/* Corner accent triangle — mirrors test code's status-tinted corner */
.host-card::after {
content: '';
position: absolute;
top: 0; right: 0;
width: 0; height: 0;
border-style: solid;
border-width: 0 10px 10px 0;
border-color: transparent var(--border-color) transparent transparent;
transition: border-color .2s;
}
.host-card:hover { border-color: var(--accent-orange); box-shadow: 0 0 12px rgba(255,107,0,.1); }
.host-card-up { border-left: 3px solid var(--green); }
.host-card-up::after { border-color: transparent rgba(0,255,136,.45) transparent transparent; }
.host-card-down { border-left: 3px solid var(--red); box-shadow: inset 3px 0 10px rgba(255,45,85,.08); }
.host-card-down::after { border-color: transparent rgba(255,45,85,.55) transparent transparent; }
.host-card-degraded { border-left: 3px solid var(--orange); }
.host-card-degraded::after { border-color: transparent rgba(255,107,0,.45) transparent transparent; }
.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); letter-spacing: .04em; }
.host-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.host-ip { font-size: .72em; color: var(--text-muted); }
.host-source {
font-size: .65em;
padding: 1px 5px;
border: 1px solid;
letter-spacing: .04em;
font-weight: bold;
}
.source-prometheus { color: var(--orange); border-color: var(--orange-dim); }
.source-ping { color: var(--cyan); border-color: var(--cyan-dim); }
.iface-list { border-top: 1px solid var(--border-color); 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); }
.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-color); padding-top: 6px; margin-bottom: 8px; }
.host-actions { border-top: 1px solid var(--border-color); padding-top: 7px; display: flex; gap: 5px; flex-wrap: wrap; }
/* ── Form layout helpers (used in suppressions.html form grids) ───── */
.form-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.form-row-align { align-items: flex-end; }
.form-group-wide { flex: 3; }
.form-group-submit { flex: 0 0 auto; min-width: unset; }
.required { color: var(--red); }
/* ── Duration pills ───────────────────────────────────────────────── */
.duration-pills { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 6px; }
.pill {
padding: 3px 10px;
border: 1px solid var(--border-color);
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); }
/* ── Available targets grid (suppressions page) ───────────────────── */
.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-color);
padding: 10px;
}
.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-color); padding: 1px 5px; color: var(--text-dim); }
/* ── Topology diagram ─────────────────────────────────────────────── */
.topology {
background-color: var(--bg2);
background-image: radial-gradient(circle, rgba(0,212,255,0.07) 1px, transparent 1px);
background-size: 22px 22px;
border: 1px solid var(--border-color);
padding: 20px 16px 16px;
margin-bottom: 16px;
text-align: center;
overflow-x: auto;
}
/* topo-v2 outer wrapper */
.topo-v2 {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
min-width: 860px;
padding: 20px 24px 24px;
}
.topo-tier {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.topo-vc {
display: flex;
justify-content: center;
align-items: flex-start;
width: 100%;
position: relative;
height: 40px;
}
.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;
}
/* Blurred copy of the wire for a soft glow halo */
.topo-vc-wire::before {
content: '';
position: absolute;
inset: 0;
background: inherit;
filter: blur(5px);
opacity: .5;
}
.topo-vc-label {
position: absolute;
left: calc(50% + 7px);
top: 50%;
transform: translateY(-50%);
font-size: .58em;
color: var(--amber);
white-space: nowrap;
letter-spacing: .06em;
font-family: var(--font);
background: rgba(3,5,8,.7);
padding: 1px 4px;
}
.topo-v2-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 8px 16px;
border: 1px solid var(--border-color);
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;
overflow: hidden;
}
/* Top highlight strip — color matches node type / status */
.topo-v2-node::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: currentColor;
opacity: .4;
}
.topo-v2-status-up.topo-v2-node::before { background: var(--green); opacity: .65; }
.topo-v2-status-down.topo-v2-node::before { background: var(--red); opacity: .75; }
.topo-v2-status-degraded.topo-v2-node::before { background: var(--orange); opacity: .65; }
.topo-v2-status-unknown.topo-v2-node::before { opacity: .15; }
.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; }
.topo-v2-internet { border-color: var(--cyan); color: var(--cyan); text-shadow: var(--glow-cyan); box-shadow: 0 0 12px rgba(0,212,255,.12); }
.topo-v2-router { border-color: var(--cyan); color: var(--cyan); text-shadow: var(--glow-cyan); box-shadow: 0 0 12px rgba(0,212,255,.14); }
.topo-v2-switch { border-color: var(--amber); color: var(--amber); text-shadow: var(--glow-amber); box-shadow: 0 0 12px rgba(255,179,0,.12); }
.topo-v2-host { border-color: var(--border-color); color: var(--text); cursor: default; }
@keyframes topo-pulse-down {
0%,100% { box-shadow: 0 0 6px rgba(255,45,85,.3); }
50% { box-shadow: 0 0 18px rgba(255,45,85,.75), 0 0 30px rgba(255,45,85,.2); }
}
.topo-v2-status-up { border-color: var(--green); box-shadow: 0 0 8px rgba(0,255,136,.2); }
.topo-v2-status-down { border-color: var(--red); animation: topo-pulse-down 2s ease-in-out infinite; }
.topo-v2-status-degraded { border-color: var(--orange); box-shadow: 0 0 8px rgba(255,107,0,.2); }
.topo-v2-status-unknown { border-color: var(--border-color); }
.topo-v2-offrack { border-style: dashed !important; }
.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); animation: topo-pulse-down 1.5s ease-in-out infinite; }
.topo-badge-degraded { color: var(--orange); border-color: var(--orange); }
.topo-badge-unknown { color: var(--text-muted); border-color: var(--border-color); }
/* Host wrap + dual-homing wires */
.topo-v2-host-wrap {
display: flex;
flex-direction: column;
align-items: center;
}
.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; transition: background .3s, opacity .3s; }
.topo-v2-wire-1g { width: 0; height: 100%; border-left: 2px dashed var(--amber); opacity: .45; }
@keyframes wire-dash-anim { to { background-position: 0 -20px; } }
.topo-v2-wire-10g.wire-down {
background: repeating-linear-gradient(to bottom, var(--red) 0 6px, transparent 6px 10px) !important;
background-size: 2px 10px !important;
opacity: .9 !important;
transition: none !important;
animation: wire-dash-anim .7s linear infinite;
}
/* Bus rails */
.topo-bus-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
}
.topo-bus-10g {
display: flex;
align-items: center;
position: relative;
height: 20px;
}
.topo-bus-10g-line {
flex: 1;
height: 2px;
background: var(--green);
opacity: .55;
margin: 0 4px;
box-shadow: 0 0 6px rgba(0,255,136,.4);
}
.topo-bus-10g-label {
font-size: .56em;
color: var(--green);
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);
white-space: nowrap;
letter-spacing: .05em;
font-family: var(--font);
opacity: .8;
padding: 0 8px;
}
/* Hosts row */
.topo-v2-hosts {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
padding-top: 4px;
width: 100%;
}
/* Topology legend */
.topo-legend {
display: flex;
gap: 18px;
align-items: center;
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid rgba(255,107,0,.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; box-shadow: 0 0 4px rgba(0,255,136,.5); }
.topo-legend-line-1g { width: 24px; height: 0; border-top: 2px dashed 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; }
/* ── Link debug page ──────────────────────────────────────────────── */
.link-host-list { display: flex; flex-direction: column; gap: 18px; }
.link-host-panel {
background: var(--bg2);
border: 1px solid var(--border-color);
}
.link-host-title {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 16px;
background: var(--bg3);
border-bottom: 1px solid var(--border-color);
cursor: pointer;
user-select: none;
}
.link-host-title:hover { background: var(--bg-hover); }
.link-host-name { font-weight: bold; font-size: .88em; color: var(--amber); letter-spacing: .05em; }
.link-host-ip { font-size: .72em; color: var(--text-muted); }
.link-host-upd { font-size: .65em; color: var(--text-muted); margin-left: auto; }
.panel-toggle { font-size: .65em; color: var(--text-muted); flex-shrink: 0; margin-left: 6px; padding: 0 4px; border: 1px solid var(--border-color); }
.link-host-panel.collapsed > .link-ifaces-grid { display: none; }
.link-collapse-bar { display: flex; gap: 8px; margin-bottom: 10px; }
.link-ifaces-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
.link-iface-card {
border-right: 1px solid rgba(255,107,0,.10);
border-bottom: 1px solid rgba(255,107,0,.10);
padding: 12px 14px;
}
.link-iface-card:last-child { border-right: none; }
.link-iface-card.port-down { opacity: .42; filter: saturate(.3); }
.link-iface-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255,107,0,.10);
}
.link-iface-name { font-weight: bold; font-size: .84em; color: var(--amber); flex: 1; }
.link-iface-speed { font-size: .75em; color: var(--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); }
.link-iface-type.type-copper{ color: var(--green); border-color: var(--green-muted); }
.link-iface-type.type-da { color: var(--amber); border-color: var(--amber-dim); }
.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); }
.val-amber { color: var(--amber); text-shadow: var(--glow-amber); }
.counter-zero { color: var(--green); }
.counter-nonzero { color: var(--red); text-shadow: var(--glow-red); }
/* Traffic bars — use lt-progress from base.css */
.traffic-section { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,107,0,.08); }
.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-row .lt-progress { flex: 1; height: 5px; }
.traffic-value { font-size: .7em; color: var(--text-dim); width: 68px; text-align: right; flex-shrink: 0; }
/* Amber variant for lt-progress (65-85% utilisation warning) */
.lt-progress--amber .lt-progress-bar { background: var(--amber); box-shadow: 0 0 5px var(--amber), 0 0 10px rgba(255,179,0,.35); }
/* SFP / optical panel */
.sfp-panel {
margin-top: 10px;
padding: 10px 10px 8px;
background: var(--bg3);
border: 1px solid rgba(0,212,255,.2);
position: relative;
}
.sfp-panel::before {
content: '[ SFP / OPTICAL ]';
position: absolute;
top: -8px; left: 10px;
font-size: .6em;
color: var(--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-row { display: flex; align-items: center; gap: 5px; margin-top: 1px; }
.power-track { flex: 1; height: 3px; background: var(--bg-primary); border: 1px solid rgba(255,107,0,.15); 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 alert badges + PoE bars */
.link-alert-badge {
display: inline-block;
font-size: .6em;
font-weight: bold;
padding: 1px 5px;
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); }
.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(255,107,0,.2); }
.port-lldp { font-size: .68em; color: var(--cyan); margin: -4px 0 6px; letter-spacing: .02em; }
.port-poe-info { font-size: .68em; color: var(--amber); margin: -4px 0 6px; }
.poe-bar-track { height: 3px; background: var(--bg3); margin-top: 3px; overflow: hidden; }
.poe-bar-fill { height: 100%; transition: width .4s; }
.poe-bar-ok { background: var(--green); }
.poe-bar-warn { background: var(--amber); }
.poe-bar-crit { background: var(--red); }
/* UniFi 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;
}
.unifi-section-header::before,
.unifi-section-header::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
}
/* Link health summary */
.link-summary-panel {
background: var(--bg2);
border: 1px solid var(--border-color);
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; }
.link-loading { padding: 20px; text-align: center; color: var(--text-muted); font-size: .8em; }
.link-loading::after { content: ' ...'; animation: blink 1s step-end infinite; }
.link-no-data { padding: 14px; color: var(--text-muted); font-size: .78em; text-align: center; }
.lt-alert { margin-bottom: 12px; }
/* ── Inspector page ───────────────────────────────────────────────── */
.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;
}
.inspector-chassis {
background: var(--bg2);
border: 1px solid var(--border-color);
}
.chassis-header {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--bg3);
border-bottom: 1px solid var(--border-color);
}
.chassis-name { font-weight: bold; font-size: .88em; color: var(--amber); letter-spacing: .05em; }
.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; position: relative; }
.chassis-rows { display: flex; flex-direction: column; gap: 5px; margin-bottom: 8px; }
.chassis-row { display: flex; flex-wrap: wrap; gap: 4px; }
.chassis-sfp-section {
display: flex;
gap: 6px;
padding-top: 8px;
border-top: 1px solid rgba(0,212,255,.15);
margin-top: 4px;
}
.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-color);
}
.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-color);
}
/* Port blocks */
.switch-port-block {
width: 34px; height: 34px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1px;
padding: 2px 1px;
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;
}
.switch-port-block.sfp-port { width: 34px; height: 40px; font-size: .55em; border-left-width: 2px; }
.switch-port-block.sfp-block { width: 36px; height: 38px; font-size: .55em; letter-spacing: .04em; border-left-width: 3px; }
.switch-port-block.down { background: var(--bg3); border-color: rgba(255,107,0,.12); color: rgba(255,107,0,.2); }
.switch-port-block.up { background: rgba(0,255,136,.06); border-color: var(--green-muted); color: var(--green); text-shadow: 0 0 4px rgba(0,255,136,.5); }
.switch-port-block.up:hover{ background: rgba(0,255,136,.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); }
.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); }
.switch-port-block.uplink:hover { box-shadow: var(--glow-cyan); }
.switch-port-block.selected { outline: 2px solid rgba(255,255,255,.85); outline-offset: 1px; box-shadow: 0 0 8px rgba(255,255,255,.5); }
.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; }
.chassis-row.us24pro-row .switch-port-block:nth-child(2n+1):not(:first-child) { margin-left: 6px; }
/* Chassis legend */
.chassis-legend {
display: flex;
gap: 16px;
align-items: center;
padding: 7px 16px 8px;
border-top: 1px solid rgba(255,107,0,.1);
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(255,107,0,.12); }
.cls-up { background: rgba(0,255,136,.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); }
/* Inspector detail panel */
.inspector-panel {
width: 0;
overflow: hidden;
flex-shrink: 0;
transition: width .2s ease;
}
.inspector-panel.open { width: 310px; }
.inspector-panel-inner {
width: 310px;
background: var(--bg2);
border: 1px solid var(--border-color);
padding: 14px 14px 18px;
overflow-y: auto;
max-height: calc(100vh - 120px);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.panel-port-name { font-weight: bold; font-size: .92em; color: var(--amber); }
.panel-meta { font-size: .68em; color: var(--text-muted); margin-top: 2px; }
.panel-close {
background: none;
border: 1px solid var(--border-color);
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); }
/* Inspector panel uses lt-divider — compact spacing overrides */
.inspector-panel .lt-divider { margin: 8px 0 4px; }
.inspector-panel .lt-divider-label { color: var(--amber); font-size: .6em; }
.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 */
.path-conn-type { font-size: .68em; color: var(--cyan); font-weight: normal; margin-left: 6px; text-transform: none; letter-spacing: normal; }
.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; }
.path-debug-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 6px; }
.path-col { background: var(--bg3); border: 1px solid rgba(255,107,0,.15); 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(255,107,0,.12); 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,212,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-color); }
.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); }
.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; }
.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; }
.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-color); line-height: 1.4; }
.diag-code { font-weight: bold; color: var(--amber); }
.diag-section { background: var(--bg2); border: 1px solid rgba(255,107,0,.10); }
.diag-section-header { font-size: .62em; font-weight: bold; color: var(--amber); padding: 4px 8px; letter-spacing: .04em; border-bottom: 1px solid rgba(255,107,0,.10); background: rgba(255,107,0,.03); }
.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-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(255,107,0,.02); }
.diag-val-good { color: var(--green); }
.diag-val-warn { color: var(--amber); }
.diag-val-bad { color: var(--red); }
.diag-power-bar-wrap {
position: relative;
display: inline-block;
width: 60px; height: 7px;
background: var(--bg3);
border: 1px solid var(--border-color);
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; }
.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 td { color: var(--amber); }
.diag-stat-nonzero-warn { background: var(--amber-dim); }
.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); }
.diag-pulse-link { font-size: .62em; padding: 4px 0; text-align: right; }
.diag-pulse-link a { color: var(--cyan); }
.diag-pulse-link a:hover { text-shadow: var(--glow-cyan); }
/* ── 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; }
.inspector-layout { flex-direction: column; }
.inspector-panel.open { width: 100%; }
.inspector-panel-inner{ width: 100%; }
}