Files
gandalf/static/style.css
T
jaredandClaude Opus 5.5 aa7341af06
Lint / JS (eslint) (push) Successful in 21s
Lint / Python (flake8) (push) Successful in 1m20s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m0s
Test / Python Tests (pytest) (push) Successful in 1m48s
Lint / Python (flake8) (pull_request) Successful in 1m44s
Lint / JS (eslint) (pull_request) Successful in 30s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m42s
Test / Python Tests (pytest) (pull_request) Successful in 1m16s
Topology: a live page drawn from the real gear
The dashboard's hand-drawn diagram was out of date. /topology replaces it
with the network as the switches report it, every device drawn to scale
from its faceplate or cutsheet front, and one cable per physical link
leaving the port it is plugged into.

- topology.py builds the graph: switch-to-switch cables seen from both ends
  are merged, servers get one cable per NIC, the gateway's internet port
  goes to the modem and on to the internet, unknown neighbours are stubs,
  and gear with no live cable is drawn where it was last seen.
- Ports named after a different device than the one plugged in are flagged.
- Cables show medium, speed, state and traffic; click one for its details,
  hover a device to trace its path to the internet; pan, zoom and filters.
- The monitor now keeps host_macs (MAC to host NIC) in link_stats.
- The old dashboard diagram and its JS and CSS are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 19:30:59 -05:00

1593 lines
83 KiB
CSS

/* ══════════════════════════════════════════════════════════════════════
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;
}
/* ── 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)); }
}
/* ── Button loading state ─────────────────────────────────────────── */
[data-action="refresh"].is-loading,
.lt-btn.is-loading {
opacity: .5;
pointer-events: none;
cursor: wait;
position: relative;
}
[data-action="refresh"].is-loading::after,
.lt-btn.is-loading::after {
content: '…';
}
/* ── Secondary button: dark-mode definition ─────────────────────────
base.css only defines .lt-btn-secondary in its light-theme block,
so dark mode falls back to the default cyan primary appearance.
This restores a visually distinct secondary look in dark mode. */
.lt-btn-secondary {
background: var(--cyan-dim);
border-color: rgba(0,212,255,.28);
color: var(--cyan);
}
.lt-btn-secondary:hover {
background: rgba(0,212,255,.18);
border-color: rgba(0,212,255,.5);
}
/* ── ⌘K hint button in header ────────────────────────────────────── */
.lt-cmd-hint-btn {
font-size: 0.65rem;
opacity: 0.55;
letter-spacing: 0.03em;
padding: 0.2rem 0.45rem;
}
/* ── Form group modifiers ────────────────────────────────────────── */
.lt-form-group--last { margin-bottom: 0; }
/* ── Search input size variant ───────────────────────────────────── */
.lt-search-input--sm { width: 180px; }
/* ── Notification panel helpers ──────────────────────────────────── */
.lt-notif-empty {
padding: 1rem;
font-size: 0.75rem;
color: var(--text-muted);
text-align: center;
}
.lt-notif-view-all {
width: 100%;
text-align: center;
display: block;
font-size: 0.72rem;
}
.lt-notif-dot {
border-radius: 50%;
margin-top: 4px;
flex-shrink: 0;
}
/* ── Divider variants ────────────────────────────────────────────── */
.lt-divider--compact { margin: 1rem 0 0.75rem; }
.lt-divider--unifi { margin: 20px 0 12px; }
.lt-divider-label--unifi { color: var(--cyan); letter-spacing: .1em; }
/* ── Stats grid spacing variant ──────────────────────────────────── */
.lt-stats-grid--mb { margin-bottom: 16px; }
/* ── 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; }
.sev-pills { display: flex; gap: 4px; }
.g-page-sub { font-size: .78em; color: var(--text-muted); margin-top: 4px; }
.g-page-sub-aside { font-size: .78em; color: var(--text-muted); margin-left: 8px; }
.g-stale-warn { color: var(--orange); font-weight: 600; }
/* ── 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); }
.badge-stale { color: var(--amber, var(--orange)); border-color: transparent; margin-left: 4px; white-space: nowrap; }
.badge-ticket-closed { color: var(--accent-purple); border-color: var(--accent-purple); margin-left: 4px; white-space: nowrap; }
.chip-followup { text-decoration: none; cursor: pointer; }
.chip-followup:hover, .chip-followup:focus-visible { background: rgba(255,107,0,.12); }
.followup-intro { color: var(--text-muted); font-size: .85em; margin: 0 0 8px; }
#followup-table td { white-space: nowrap; }
/* ── 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; }
.lt-table tr.row-suppressed td { opacity: .6; }
.lt-table tr.row-suppressed td:first-child{ border-left-color: var(--text-muted) !important; }
/* ── 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; }
.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: 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); }
/* Maintenance modal pills: same look, own class so the suppress modal's handler skips them */
.maint-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; }
.maint-pill:hover { border-color: var(--green); color: var(--green); background: var(--green-dim); }
.maint-pill.active { border-color: var(--amber); color: var(--amber); background: var(--amber-dim); text-shadow: var(--glow-amber); }
.maint-intro { margin: 0 0 12px; }
/* Maintenance banner: every page while a global suppression is active */
.maint-banner {
display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
margin-bottom: 16px; padding: 8px 14px; border: 1px solid var(--amber); background: var(--amber-dim);
color: var(--amber); font-size: .82em;
}
.maint-banner strong { letter-spacing: .06em; text-transform: uppercase; }
.maint-reason { color: var(--text-muted); }
/* ── 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); }
/* Pulsing red glow for critical chips and down states */
@keyframes 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); }
}
/* ── 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-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); }
.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); }
/* ── Stat card alert variant (pulsing border when critical > 0) ─── */
.lt-stat-card--alert {
border-color: var(--red) !important;
box-shadow: 0 0 8px rgba(255,45,85,.25) !important;
animation: pulse-down 2s ease-in-out infinite;
}
.lt-stat-card--alert::before { background: var(--red); box-shadow: var(--glow-red); }
/* ── lt-frame inside g-section: no extra bottom margin ────────────── */
.g-section > .lt-frame { margin-bottom: 0; }
/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 768px) {
.host-grid { grid-template-columns: 1fr; }
.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%; }
}
/* ── WAN modem / fiber panel ──────────────────────────────────────── */
.modem-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
padding: 12px;
}
@media (max-width: 700px) {
.modem-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.modem-stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.modem-stat-label { font-size: .68em; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; }
.modem-stat-value { font-size: 1.05em; font-weight: bold; overflow-wrap: anywhere; }
.modem-stat-note { font-size: .7em; color: var(--text-muted); display: block; }
.modem-error { margin: 12px; padding: 8px 10px; border: 1px solid var(--terminal-red, #f44); color: var(--terminal-red, #f44); }
.modem-chart { padding: 0 12px 8px; }
.modem-chart-title { font-size: .72em; color: var(--text-muted); margin-bottom: 4px; }
.modem-chart-title span { margin-left: 8px; opacity: .8; }
.modem-chart svg { display: block; width: 100%; height: 70px; border: 1px solid var(--border-color); }
.modem-chart svg polyline { stroke: var(--terminal-cyan, #0ff); stroke-width: 1.5; }
.modem-chart-empty { padding: 0 12px 8px; font-size: .72em; color: var(--text-muted); }
.modem-updated { padding: 0 12px 10px; }
/* ── Device search (home page) ────────────────────────────────────── */
.device-search { position: relative; }
.device-search label { display: block; margin-bottom: 6px; }
.device-search .lt-search { width: 100%; }
.device-search-input { width: 100%; font-size: 1em; padding-top: 10px; padding-bottom: 10px; }
.device-search-results {
position: absolute; left: 0; right: 0; z-index: 50; margin-top: 4px;
background: var(--bg-card, var(--bg-secondary)); border: 1px solid var(--border-color);
max-height: 420px; overflow-y: auto;
}
.device-search-results[hidden] { display: none; }
.device-search-row {
display: grid; grid-template-columns: 14px minmax(0, 1.4fr) 70px minmax(0, 1fr) minmax(0, 1fr);
gap: 10px; align-items: center; padding: 9px 12px; color: inherit; text-decoration: none;
border-bottom: 1px solid var(--border-color);
}
.device-search-row:last-child { border-bottom: 0; }
.device-search-row:hover, .device-search-row.is-active { background: var(--bg-tertiary); }
.device-search-name { font-weight: bold; overflow-wrap: anywhere; }
.device-search-kind { font-size: .72em; color: var(--text-muted); text-transform: uppercase; }
.device-search-meta { font-size: .8em; color: var(--text-muted); overflow-wrap: anywhere; }
.device-search-empty { padding: 12px; color: var(--text-muted); }
@media (max-width: 700px) {
.device-search-row { grid-template-columns: 14px minmax(0, 1fr) auto; }
.device-search-row .device-search-meta { grid-column: 2 / -1; }
}
/* ── Device page ───────────────────────────────────────────────────── */
a.device-link { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--text-muted); }
a.device-link:hover { color: var(--text-accent); border-bottom-color: currentColor; }
.device-title { display: flex; align-items: center; gap: 10px; overflow-wrap: anywhere; }
.device-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.device-toolbar { padding: 8px 12px; margin: 0; }
.device-table td { vertical-align: middle; }
.device-num { white-space: nowrap; font-variant-numeric: tabular-nums; }
.device-row--down { opacity: .55; }
.device-row-name { overflow-wrap: anywhere; }
.device-row-sub { font-size: .72em; color: var(--text-muted); }
.device-row-toggle { background: none; border: 0; color: inherit; cursor: pointer; font: inherit; padding: 2px 6px; }
.device-row-detail > td { background: var(--bg-tertiary); padding: 10px 14px; }
/* WAN outage list in the modem panel */
.modem-outages { padding: 0 12px 8px; }
.modem-outages ul { list-style: none; margin: 0; padding: 0; border: 1px solid var(--border-color); }
.modem-outages li { display: flex; justify-content: space-between; gap: 12px; padding: 4px 8px; font-size: .8em; }
.modem-outages li + li { border-top: 1px solid var(--border-color); }
.g-section-actions[hidden] { display: none; }
/* Rows open a detail panel holding the fields that are hidden here on narrow screens */
.device-row-detail .link-stats-grid { grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); }
.device-table th:nth-child(-n+2), .device-table td:nth-child(-n+2) { width: 1%; white-space: nowrap; }
@media (max-width: 700px) {
.device-col-wide { display: none; }
.device-table th, .device-table td { padding-left: 6px; padding-right: 6px; }
}
.device-search-row.is-active { box-shadow: inset 3px 0 0 var(--terminal-cyan, #0ff); }
@media (max-width: 700px) {
/* Scroll inside the wrapper rather than squeezing every column to a few letters */
#events-table { min-width: 760px; }
}
/* ── Port inspector on the device page ────────────────────────────── */
.device-inspector { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 14px 16px; align-items: start; }
.device-chassis { grid-column: 1; min-width: 0; }
.device-port-table { grid-column: 1; min-width: 0; }
.device-inspector .inspector-panel { grid-column: 2; grid-row: 1 / span 2; position: sticky; top: 70px; }
.device-inspector:not(.has-chassis) .inspector-panel,
.device-inspector:not(.has-chassis) .device-chassis { display: none; }
tr.device-row[data-port-idx] { cursor: pointer; }
tr.device-row.is-selected { background: var(--bg-tertiary); box-shadow: inset 3px 0 0 var(--terminal-cyan, #0ff); }
.device-chassis .switch-port-block,
.device-chassis .switch-port-block.sfp-port,
.device-chassis .switch-port-block.sfp-block { width: 50px; height: 50px; font-size: .68em; gap: 2px; }
.device-chassis .port-num { font-size: 1.2em; }
.device-chassis .port-speed { font-size: .85em; }
.device-chassis .port-lldp { font-size: .8em; max-width: 46px; margin: 0; }
@media (max-width: 900px) {
.device-inspector { grid-template-columns: minmax(0, 1fr); }
.device-inspector .inspector-panel { grid-column: 1; grid-row: auto; position: static; }
.device-inspector .inspector-panel.open { width: 100%; }
.device-inspector .inspector-panel-inner { width: 100%; max-height: none; }
.device-chassis .chassis-ear-l, .device-chassis .chassis-ear-r { display: none; }
}
/* ── Realistic device faceplates (static/faceplates.js) ───────────── */
/* Hardware colors are fixed: a silver chassis stays silver in every theme. */
.device-chassis { container-type: inline-size; }
.fp-wrap { padding: 14px 16px; }
.fp-frame { overflow: hidden; border-radius: 3px; box-shadow: 0 2px 10px rgba(0,0,0,.55); }
.fp-svg { display: block; width: 100%; height: auto; }
/* Too narrow for the whole unit: zoom to the port area so jacks stay tappable.
Width = viewBox width / visible width; margin = cropX / visible width. */
@container (max-width: 620px) {
.fp-crop-udmpro .fp-svg { width: 308.7%; max-width: none; margin-left: -208.7%; }
}
.fp-body { fill: url(#fp-metal); }
.fp-vent { fill: #242326; }
.fp-screen { fill: #0c1020; stroke: #2a2c34; stroke-width: 1; }
.fp-screen-ring{ fill: none; stroke: #3f7bd9; stroke-width: 2.2; }
.fp-screen-dot { fill: #dfe8f8; }
.fp-brand { fill: #2e2f33; font: 700 7.5px Arial, Helvetica, sans-serif; }
.fp-brand-light{ font-weight: 400; fill: #55565c; }
.fp-brand-bolt-ring { fill: #2e2f33; }
.fp-brand-bolt { fill: #e4e4e6; }
.fp-bay { fill: url(#fp-bay-face); stroke: #54555a; stroke-width: 1; }
.fp-bay-edge { fill: #bdbdc1; }
.fp-bay-led { fill: #3a3a3e; }
.fp-block { fill: #e1e1e3; stroke: #5d5e63; stroke-width: 1; }
.fp-divider { fill: #dcdcde; }
.fp-label { fill: #55565c; font: 9px Arial, Helvetica, sans-serif; }
.fp-label-sm { font-size: 6.5px; letter-spacing: .03em; }
.fp-globe { fill: none; stroke: #3f7bd9; stroke-width: .8; }
.fp-reset { fill: #1b1b1d; }
.fp-jack { fill: #18181b; stroke: #0c0c0d; stroke-width: .6; }
.fp-contacts { fill: #b89545; }
.fp-plug { fill: #9ea1a8; stroke: #5b5d63; stroke-width: .6; }
.fp-plug-clip { fill: #7e8087; stroke: #5b5d63; stroke-width: .5; }
.fp-cage { fill: #17171a; stroke: #6e6f74; stroke-width: 1; }
.fp-cage-spring{ fill: none; stroke: #5d5e63; stroke-width: 1.2; stroke-dasharray: 2.5 1.5; }
.fp-cage-rail { fill: #3b3c41; }
.fp-module { fill: #a5a7ad; stroke: #6a6b70; stroke-width: .6; }
.fp-module-lc { fill: #2a4a7f; stroke: #1a2a44; stroke-width: .5; }
.fp-module-fault { stroke: #e0312b; stroke-width: 1.4; }
.fp-dac-boot { fill: #2b2c30; stroke: #17171a; stroke-width: .5; }
.fp-dac-cable { fill: #111113; }
.fp-led-idle { fill: #6a6b70; }
.fp-led-off { fill: #4b4c50; }
.fp-led-amber { fill: #ffad1f; }
.fp-led-green { fill: #35f06f; }
.fp-led-white { fill: #ffffff; stroke: #8fa6c4; stroke-width: .5; }
.fp-led-unknown{ fill: #b9bcc2; }
.fp-led-amber, .fp-led-green, .fp-led-white { filter: drop-shadow(0 0 1.5px currentColor); }
.fp-led-amber { color: #ffad1f; }
.fp-led-green { color: #35f06f; }
.fp-led-white { color: #cfe2ff; }
.fp-led-active { animation: fp-blink 1.3s steps(2, jump-none) infinite; }
@keyframes fp-blink { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .fp-led-active { animation: none; } }
.fp-port { cursor: pointer; outline: none; }
.fp-hit { fill: transparent; stroke: transparent; stroke-width: 2.5; pointer-events: all; }
.fp-port:hover .fp-hit { stroke: rgba(0,90,220,.55); }
.fp-port:focus-visible .fp-hit,
.fp-port.selected .fp-hit { stroke: #0058e0; fill: rgba(0,88,224,.14); }
.fp-legend-led { width: 12px; height: 5px; border-radius: 1px; display: inline-block; background: #4b4c50; }
.fp-legend-led.fp-led-amber { background: #ffad1f; }
.fp-legend-led.fp-led-green { background: #35f06f; }
.fp-legend-led.fp-led-white { background: #fff; }
.fp-readout { margin-left: auto; font-size: .72em; color: var(--text-dim, var(--text-muted)); text-transform: none; letter-spacing: 0; }
/* Narrow panels: crop to the ports, or scroll within the panel */
@container (max-width: 620px) {
.fp-crop-agg .fp-svg { width: 437%; max-width: none; margin-left: -337%; }
.fp-scroll { overflow-x: auto; }
.fp-scroll .fp-svg { width: 260%; max-width: none; }
}
.fp-desk { box-shadow: none; }
.fp-desk .fp-svg { max-width: 560px; margin: 0 auto; filter: drop-shadow(0 3px 6px rgba(0,0,0,.5)); }
.fp-body-white { fill: #f2f2f3; stroke: #c9c9cc; stroke-width: 1; }
.fp-frame-grey { fill: #c6c8cc; stroke: #8b8d92; stroke-width: 1; }
.fp-foot { fill: #b7b7ba; }
.fp-body-metal { fill: url(#fp-metal); }
.fp-ear { fill: #c4c4c7; stroke: #8f9095; stroke-width: 1; }
.fp-ear-hole { fill: #0b0d12; }
.fp-button { fill: #3a3a3e; stroke: #1b1b1d; stroke-width: 1; }
.fp-speed-bar { fill: #8d8e93; }
.fp-screen-grid{ fill: none; stroke: #2f6fd0; stroke-width: 1; }
.fp-screen-icon{ fill: #6b9be4; }
.fp-static { opacity: .85; }
.fp-bolt { fill: #e88a00; }
.fp-cycle { fill: none; stroke: #0058e0; stroke-width: 1.4; stroke-linecap: round; }
.fp-outlet { cursor: default; outline: none; }
.fp-outlet-face { fill: #2a2a2d; stroke: #111113; stroke-width: 1; }
.fp-slot { fill: #050506; }
.fp-usbc { fill: #1c1c1f; stroke: #8b8d92; stroke-width: 1; }
.fp-usbc-tongue { fill: #b9bcc2; }
.fp-outlet-is-off .fp-outlet-face { fill: #5b5c60; }
.fp-outlet-is-off .fp-usbc { fill: #66676b; }
.fp-outlet:hover .fp-hit { stroke: rgba(0,90,220,.55); }
.fp-outlet:focus-visible .fp-hit { stroke: #0058e0; fill: rgba(0,88,224,.14); }
.fp-outlet-state { fill: #8d8e93; }
.fp-state-on { fill: #12a84a; }
.fp-state-off { fill: #d6332a; }
.fp-ups-band { fill: #8c8d90; }
.fp-ups-vent { fill: #2b2b2e; }
.fp-ups-rib { fill: none; stroke: #77787c; stroke-width: 1; }
.fp-ups-batt { fill: none; stroke: #2b2b2e; stroke-width: 1.3; }
.fp-ups-batt-tip{ fill: #2b2b2e; }
.fp-led-blue { fill: #2f7bff; color: #2f7bff; filter: drop-shadow(0 0 2px currentColor); }
.fp-legend-led.fp-led-blue { background: #2f7bff; }
.fp-legend-led.fp-led-outlet-on { background: #12a84a; width: 7px; height: 7px; border-radius: 50%; }
.fp-legend-led.fp-led-outlet-off { background: #d6332a; width: 7px; height: 7px; border-radius: 50%; }
.fp-legend-icon { width: 11px; height: 11px; flex-shrink: 0; }
/* A white LED needs an edge to show against the light metal around it */
polygon.fp-led-white { stroke: #6b7c95; stroke-width: .7; }
polygon.fp-led-off { fill: #6b6c71; }
.fp-key { fill: none; stroke: #55565c; stroke-width: .8; }
.fp-key-amber { fill: #e8762c; stroke: none; }
.fp-key-green { fill: #7ec23a; stroke: none; }
.fp-ups-bolt { fill: #2b2b2e; }
/* UPS 2U Pro: front and rear panels stacked */
.fp-stack { box-shadow: none; }
.fp-caption { fill: #8b96a6; font: 600 10px Arial, Helvetica, sans-serif; letter-spacing: .1em; }
.fp-ups-band-light { fill: #f4f4f5; }
.fp-power-ring { fill: none; stroke: #8d8e93; stroke-width: 3; }
.fp-power-on { stroke: #2f9bff; filter: drop-shadow(0 0 3px #2f9bff); }
.fp-power-face { fill: #dcdcde; stroke: #a9a9ad; stroke-width: 1; }
.fp-power-icon { fill: none; stroke: #55565c; stroke-width: 1.5; stroke-linecap: round; }
.fp-tile { fill: #1f6fff; }
.fp-tile-num { fill: #fff; font: 600 9px Arial, Helvetica, sans-serif; }
.fp-outlet-is-off .fp-tile { fill: #3a3d46; }
.fp-screen-tile-dim{ fill: #2a2d36; }
.fp-breaker { fill: #4a4a4e; }
.fp-fan { fill: none; stroke: #3a3a3e; stroke-width: 7; }
.fp-fan-strut { fill: none; stroke: #c9c9cc; stroke-width: 5; }
.fp-epo { fill: #1c1c1f; }
.fp-bracket { fill: none; stroke: #55565c; stroke-width: 1; }
/* ── Cutsheet ─────────────────────────────────────────────────────────── */
.cutsheet-check { font-size: 12px; padding: 2px 0; overflow-wrap: anywhere; }
.cutsheet-path { font-size: 11px; color: var(--text-muted, #8aa); padding: 2px 0 8px; overflow-wrap: anywhere; }
.cs-more { margin-top: 8px; }
/* Button styles set display, which would otherwise override the hidden attribute */
.lt-btn[hidden], .cs-link-btn[hidden] { display: none !important; }
.cs-trace { margin-bottom: 16px; }
.cs-trace-form { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.cs-trace-form .lt-form-group { margin: 0; min-width: 120px; }
.cs-trace-buttons { display: flex; gap: 8px; }
.cs-trace-status { margin-top: 10px; font-size: .88rem; line-height: 1.6; }
.cs-trace-found { font-size: 1rem; margin-bottom: 6px; }
.cs-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.cs-summary { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 18px; }
.cs-chip { padding: 5px 10px; border: 1px solid var(--border-color, #234); border-radius: 3px; font-size: .82rem;
color: inherit; text-decoration: none; }
.cs-chip b { font-size: .95rem; }
.cs-chip-ok b { color: var(--status-ok, #2ecc71); }
.cs-chip-bad { border-color: var(--status-critical, #e0312b); }
.cs-chip-bad b { color: var(--status-critical, #e0312b); }
.cs-chip-warn b { color: var(--accent-amber, #f0a020); }
.cs-filters { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 10px; }
.cs-filters .lt-input { flex: 1 1 240px; min-width: 0; }
.cs-filters .lt-select { flex: 0 1 180px; }
.cs-check { display: inline-flex; gap: 6px; align-items: center; font-size: .85rem; cursor: pointer; }
.cs-hint { font-size: .82rem; color: var(--text-muted, #8aa); margin: 0 0 10px; max-width: 75ch; }
.cs-table td { vertical-align: top; }
.cs-id { font-weight: 700; white-space: nowrap; }
.cs-port, .cs-side-tag { font-size: .8rem; color: var(--text-muted, #8aa); }
.cs-side-tag { text-transform: uppercase; letter-spacing: .04em; }
.cs-path, .cs-detail { font-size: .76rem; color: var(--text-muted, #8aa); margin-top: 3px; }
.cs-tag { font-size: .68rem; padding: 1px 5px; border: 1px solid var(--accent-amber, #f0a020); color: var(--accent-amber, #f0a020);
border-radius: 2px; white-space: nowrap; }
.cs-row-problem td:first-child { box-shadow: inset 3px 0 0 var(--status-critical, #e0312b); }
.cs-flash { animation: cs-flash 2.4s ease-out 1; }
@keyframes cs-flash { from { background: rgba(240,160,32,.28); } to { background: transparent; } }
@media (prefers-reduced-motion: reduce) { .cs-flash { animation: none; outline: 2px solid var(--accent-amber, #f0a020); } }
.cs-panel { margin-bottom: 16px; }
.cs-link-btn { background: none; border: 0; padding: 0 0 0 6px; color: var(--accent-cyan, #3cc); cursor: pointer;
font: inherit; font-size: .78rem; text-decoration: underline; }
.cs-label-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; }
.cs-label-card { display: grid; grid-template-columns: auto 1fr auto; gap: 2px 8px; align-items: center;
padding: 8px 10px; border: 1px dashed var(--border-color, #234); border-radius: 3px; }
.cs-label-text { font-family: var(--font-mono, monospace); font-size: 1.05rem; font-weight: 700; letter-spacing: .05em; }
.cs-label-copies { font-size: .8rem; color: var(--text-muted, #8aa); }
.cs-label-where { grid-column: 1 / -1; font-size: .74rem; color: var(--text-muted, #8aa); }
.cs-rack { display: flex; flex-direction: column; gap: 3px; max-width: 560px; }
.cs-rack-unit { display: grid; grid-template-columns: 70px 1fr auto; gap: 10px; align-items: center;
min-height: calc(var(--u, 1) * 26px); padding: 4px 10px; border: 1px solid var(--border-color, #234);
border-radius: 2px; background: rgba(255,255,255,.02); }
.cs-rack-u { font-family: var(--font-mono, monospace); font-size: .78rem; color: var(--text-muted, #8aa); }
.cs-rack-kind { font-size: .74rem; color: var(--text-muted, #8aa); }
.cs-end { border: 1px solid var(--border-color, #234); border-radius: 3px; padding: 4px 12px 0; margin: 0 0 12px; }
.cs-end legend { font-size: .78rem; padding: 0 6px; color: var(--text-muted, #8aa); text-transform: uppercase; }
.cs-side[hidden] { display: none; }
.cs-modal-wide { max-width: 720px; width: calc(100vw - 32px); }
.cs-mono { font-family: var(--font-mono, monospace); font-size: .8rem; }
.cs-errors ul { margin: 6px 0 0 18px; font-size: .8rem; }
@media (max-width: 640px) {
/* Cables become cards: one labeled field per line */
.cs-table thead { display: none; }
.cs-table, .cs-table tbody, .cs-table tr, .cs-table td { display: block; width: 100%; }
.cs-table tr { border: 1px solid var(--border-color, #234); border-radius: 3px; margin-bottom: 10px; padding: 6px 10px; }
.cs-table td { border: 0 !important; padding: 3px 0 !important; overflow-wrap: anywhere; }
.cs-table td[data-label]::before { content: attr(data-label); display: block; font-size: .68rem;
text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted, #8aa); }
.cs-table td.cs-id { font-size: 1rem; }
.cs-row-problem { box-shadow: inset 3px 0 0 var(--status-critical, #e0312b); }
.cs-row-problem td:first-child { box-shadow: none; }
.cs-rack-unit { grid-template-columns: 56px 1fr; }
.cs-rack-kind { display: none; }
}
@media print {
.no-print, .lt-header, .lt-nav-drawer, .lt-nav-drawer-overlay { display: none !important; }
body.cs-print-labels .g-section:not(#cs-labels-section),
body.cs-print-labels .lt-page-header, body.cs-print-labels .cs-summary { display: none !important; }
.cs-label-card { break-inside: avoid; border-color: #000; color: #000; }
.lt-table, .lt-table td, .lt-table th { color: #000 !important; }
}
/* ── Alert history (/events) ───────────────────────────────────────── */
.ev-filters .lt-select { flex: 0 1 170px; }
.ev-type-chip { background: none; cursor: pointer; font: inherit; font-size: .82rem; }
.ev-type-chip:hover, .ev-type-chip:focus-visible { border-color: var(--accent-cyan); }
.ev-type-chip.active { border-color: var(--accent-cyan); background: var(--accent-cyan-dim); }
.ev-link { background: none; border: 0; padding: 0; color: inherit; font: inherit; cursor: pointer;
text-decoration: underline dotted; text-underline-offset: 3px; text-align: left; }
.ev-link:hover, .ev-link:focus-visible { color: var(--accent-cyan); }
.ev-table td { vertical-align: top; }
.ev-table .cs-port { display: block; }
.ev-pager { display: flex; gap: 12px; align-items: center; justify-content: center; margin-top: 12px; font-size: .85rem; }
.g-section-more { margin: 10px 12px 4px; font-size: .85rem; }
.ev-table .lt-badge, .ev-c-type { white-space: nowrap; }
/* Phones: each alert becomes a card instead of a squeezed table row */
@media (max-width: 760px) {
.ev-cards thead { display: none; }
.ev-cards, .ev-cards tbody { display: block; width: 100%; }
.ev-cards tr { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; padding: 10px 12px;
border-bottom: 1px solid var(--border-color); }
.ev-cards td { display: block; padding: 0; border: 0; max-width: none; white-space: normal; font-size: .82rem; }
.ev-cards td[data-label]::before { content: attr(data-label) " "; color: var(--text-muted); font-size: .72rem;
text-transform: uppercase; letter-spacing: .05em; }
.ev-cards .ev-c-target { flex: 1 1 100%; order: -1; font-size: .95rem; }
.ev-cards .ev-c-target .cs-port { display: inline; margin-left: 6px; }
.ev-cards .ev-c-desc { flex: 1 1 100%; color: var(--text-muted); white-space: normal; overflow: visible; }
.ev-cards .ev-c-action { flex: 1 1 100%; }
}
/* ── Cutsheet marks on faceplates ──────────────────────────────────── */
.fp-mark { stroke: #101418; stroke-width: 1.2; pointer-events: none; }
.fp-mark-ok { fill: var(--accent-green, #00ff88); }
.fp-mark-bad { fill: var(--status-critical, #e0312b); }
.fp-mark-doc { fill: #9aa7b4; }
body.fp-marks-off .fp-svg .fp-mark { display: none; }
.fp-mark-toggle { cursor: pointer; gap: 6px; }
.fp-mark-toggle input { margin: 0; }
/* ── Rack (/rack) ──────────────────────────────────────────────────── */
/* The rack is hardware: it keeps its own palette in both themes so labels inside it stay readable */
.rack {
--rk-frame: #0b0f14; --rk-edge: #2a3440; --rk-text: #e6edf3; --rk-muted: #8b97a5; --rk-line: rgba(255,255,255,.07);
--rk-plate-a: #232a33; --rk-plate-b: #1a2028; --rk-plate-edge: #2d3743; --rk-link: #6fd3ff;
max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 2px; padding: 10px 12px 10px 0;
background: var(--rk-frame); border: 1px solid var(--rk-edge); border-radius: 4px; color: var(--rk-text); }
[data-theme="light"] .rack {
--rk-frame: #2b3139; --rk-edge: #1d2228; --rk-muted: #a9b4c0; --rk-line: rgba(255,255,255,.10);
--rk-plate-a: #3a424d; --rk-plate-b: #313841; --rk-plate-edge: #4a5461; --rk-link: #8fdcff;
box-shadow: 0 6px 18px rgba(20,30,45,.25); }
.rack-row { display: grid; grid-template-columns: 34px 1fr; }
.rack-u { display: flex; flex-direction: column; font-size: .62rem; color: var(--rk-muted); text-align: right;
padding-right: 6px; font-family: var(--font-mono, monospace); }
.rack-u span { flex: 1; display: flex; align-items: center; justify-content: flex-end; }
.rack-slot { position: relative; min-width: 0; }
.rack-row-empty .rack-slot { background: repeating-linear-gradient(to bottom, transparent 0 calc(100% / var(--n) - 1px),
var(--rk-line) calc(100% / var(--n) - 1px) calc(100% / var(--n))); border: 1px dashed var(--rk-line); }
.rack-item, .rack-item .fp-wrap, .rack-item .fp-frame { height: 100%; }
.rack-item .fp-wrap { padding: 0; }
.rack-item .fp-frame { box-shadow: none; overflow: hidden; }
.rack-item .fp-svg { width: 100%; height: 100%; }
.rack-item .chassis-legend { display: none; }
.rack-svg { display: block; width: 100%; height: 100%; }
.rack-plate { height: 100%; display: flex; align-items: center; gap: 14px; padding: 0 18px; border-radius: 3px;
background: linear-gradient(var(--rk-plate-a), var(--rk-plate-b)); border: 1px solid var(--rk-plate-edge); }
.rack .rack-plate-name { font-weight: 700; color: var(--rk-text); text-decoration-color: var(--rk-muted); }
.rack .rack-plate-name:hover, .rack .rack-plate-name:focus-visible { color: var(--rk-link); }
.rack-plate-sub { font-size: .78rem; color: var(--rk-muted); }
.rack-kind-shelf .rack-plate { background: repeating-linear-gradient(90deg, var(--rk-plate-b) 0 10px, var(--rk-frame) 10px 20px); }
.rack-pp-body { fill: #1d2229; stroke: #39424e; }
/* The panel's name sits in its top-left corner, above the port numbers, so a long name can't run under the ports */
.rack-pp-name { fill: #9aa7b4; font-size: 12px; font-family: var(--font-mono, monospace); }
.rack-pp-num { fill: #7d8a97; font-size: 10px; }
.rack-pp-label { fill: #c9d3dc; font-size: 9px; }
.rack-pp-jack { fill: #0c0f13; stroke: #4a5560; stroke-width: 1.5; }
.rack-pp-ok .rack-pp-jack { stroke: #00e07a; }
.rack-pp-bad .rack-pp-jack { stroke: var(--status-critical, #e0312b); stroke-width: 2.5; }
.rack-pp-doc .rack-pp-jack { stroke: #9aa7b4; }
.rack-pp-hit { fill: transparent; }
.rack-pp-port[data-cable] { cursor: pointer; outline: none; }
/* A hovered run: every port on it */
.rack-pp-port.rack-hl .rack-pp-jack { fill: rgba(0,212,255,.35); stroke: #00d4ff; stroke-width: 3; }
.rack .fp-port.rack-hl .fp-hit { stroke: #00b8e6; stroke-width: 4; fill: rgba(0,212,255,.3); }
.rack-readout, .rack-legend, .rack-unplaced, .rack-tabs, .rack-warnings { max-width: 1000px;
margin-left: auto !important; margin-right: auto !important; }
.rack-readout { min-height: 1.4em; margin: 0 0 8px; font-size: .85rem; color: var(--accent-cyan); }
.rack-legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: .75rem; color: var(--text-muted); margin-bottom: 8px; }
.rack-key { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; vertical-align: -1px; }
.rack-key-ok { background: var(--accent-green); }
.rack-key-bad { background: var(--status-critical, #e0312b); }
.rack-key-doc { background: #9aa7b4; }
.rack-warnings { border: 1px solid var(--status-critical, #e0312b); padding: 8px 14px; margin-bottom: 14px; font-size: .85rem; }
.rack-warnings ul { margin: 0 0 6px; padding-left: 18px; }
.rack-warnings p { margin: 0; }
.rack-unplaced { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.rack-empty { margin: 12px; }
.rack-gap { height: 26px; display: flex; align-items: center; justify-content: center; font-size: .72rem;
color: var(--rk-muted); border: 1px dashed var(--rk-line); letter-spacing: .04em; }
.rack-toggle { margin-left: auto; display: inline-flex; gap: 6px; align-items: center; cursor: pointer; }
.rack-legend { align-items: center; }
@media (max-width: 600px) {
.rack-row { grid-template-columns: 22px 1fr; }
.rack-u { font-size: .5rem; padding-right: 3px; }
.rack-plate { padding: 0 8px; gap: 8px; }
.rack-plate-sub { display: none; }
}
.rack-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.rack-tab { background: none; border: 1px solid var(--border-color); color: inherit; font: inherit; font-size: .85rem;
padding: 6px 12px; cursor: pointer; border-radius: 3px; }
.rack-tab.active { border-color: var(--accent-cyan); background: var(--accent-cyan-dim); }
.rack-tab-count { color: var(--text-muted); font-size: .75rem; margin-left: 4px; }
.rack-shelf { gap: 10px; padding: 0 10px; container-type: size; }
.rack-shelf-label { font-size: .72rem; color: var(--rk-muted); white-space: nowrap; writing-mode: horizontal-tb; }
.rack-shelf-things { display: flex; gap: 6px; flex: 1; min-width: 0; height: 78%; }
.rack-shelf-thing { flex: 1 1 0; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 6px;
padding: 0 6px; border-radius: 3px; background: linear-gradient(#e6e8eb, #c9cdd2); color: #15191e;
text-decoration: none; font-size: .78rem; font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.5); }
.rack-shelf-thing:hover, .rack-shelf-thing:focus-visible { outline: 2px solid var(--accent-cyan); }
.rack-shelf-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@container (max-height: 50px) { .rack-shelf-label { display: none; } .rack-shelf-thing { font-size: .6rem; gap: 3px; } }
.rack-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between;
max-width: 1000px; margin: 0 auto 14px; }
.rack-bar .rack-tabs { margin: 0 !important; }
.rack-face { display: inline-flex; border: 1px solid var(--border-color); border-radius: 3px; overflow: hidden; margin-left: auto; }
.rack-face-btn { background: none; border: 0; color: inherit; font: inherit; font-size: .82rem; padding: 6px 14px; cursor: pointer; }
.rack-face-btn + .rack-face-btn { border-left: 1px solid var(--border-color); }
.rack-face-btn.active { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
.rack-back { opacity: .75; background: repeating-linear-gradient(135deg, var(--rk-plate-b) 0 8px, var(--rk-frame) 8px 16px); }
/* ── Drawn rack fronts (static/fronts.js) ─────────────────────────── */
.rf-ear, .rf-ear-dark { fill: #2b3037; }
.rf-ear { fill: #c9cbcf; }
.rf-slot { fill: #0d1014; }
.rf-vent { fill: #14181d; }
.rf-body-dark { fill: #1e2329; stroke: #3a424c; }
.rf-blank { fill: #262c33; stroke: #3d4651; }
.rf-bay { fill: #2c333b; stroke: #4c5662; stroke-width: 1.5; }
.rf-latch { fill: #8a96a3; }
.rf-bay-led { fill: #3b8bff; opacity: .8; }
.rf-io { fill: #171b20; stroke: #333b45; }
.rf-power-ring { fill: none; stroke-width: 3; }
.rf-power-icon { fill: none; stroke: #c7ced6; stroke-width: 2.4; stroke-linecap: round; }
.rf-port { fill: #0b0d10; stroke: #6b737d; }
.rf-port-tongue { fill: #2f6fd8; }
.rf-port-led { fill: #2fd36b; }
.rf-lock { fill: #a7b0ba; }
.rf-lock-slot { fill: #2b3037; }
.rf-led { fill: #6b737d; }
.rf-reset { fill: #0b0d10; stroke: #6b737d; }
.rf-brand { fill: #c3ccd5; font-size: 12px; font-weight: 700; letter-spacing: .14em; font-family: Helvetica, Arial, sans-serif; }
.rf-small { fill: #4a4f57; font-size: 10px; font-family: Helvetica, Arial, sans-serif; }
.rf-dark { fill: #1b1e22; font-weight: 700; }
.rf-door { fill: #121519; stroke: #3a424c; stroke-width: 2; }
.rf-mesh { stroke: #262c33; stroke-width: 2; opacity: .55; }
.rf-perf-hole { fill: #000; }
.rf-door-latch { fill: #3a424c; stroke: #59636f; }
.rf-bay-num { fill: #8a96a3; font-size: 11px; font-family: Helvetica, Arial, sans-serif; }
.rf-fan { fill: #15191e; stroke: #2e353e; stroke-width: 6; }
.rf-fan-hub { fill: #262c33; }
.rf-fan-blade { fill: #1f252c; stroke: #2e353e; }
.rf-handle { fill: none; stroke: #8a96a3; stroke-width: 5; }
.rf-unvr-body { fill: #d9dadc; stroke: #a9abaf; }
.rf-unvr-strip { fill: #26282c; }
.rf-unvr-bay { fill: #e9eaec; stroke: #9fa2a7; stroke-width: 1.5; }
.rf-unvr-rail { fill: #55595f; }
.rf-unvr-dot { fill: #ffffff; stroke: #c4c6ca; }
.rf-screen { fill: #0f1726; }
.rf-screen-ring { fill: none; stroke-width: 3; }
/* Ubiquiti UACC-Rack-Panel-Patch-Blank-24 (static/rack.js uaccPanelSvg) */
.ua-ear { fill: #6f7277; }
.ua-hole { fill: #15181c; }
.ua-face { fill: #8e9095; stroke: #6c6f74; }
.ua-lip { fill: #a6a8ad; }
.ua-open { fill: #17191d; }
.ua-bar { fill: #9a9ca1; }
.ua-keystone { fill: #101113; stroke: #2b2d31; }
.ua-keystone-hole { fill: #2f3236; }
.ua-num { fill: #2b2d31; font-size: 9px; font-family: Arial, Helvetica, sans-serif; }
.ua-label { fill: #1f2124; font-size: 8.5px; font-weight: 700; font-family: Arial, Helvetica, sans-serif; }
.ua-mark { fill: none; stroke: transparent; }
.rack-pp-ok .ua-mark { stroke: #00b860; stroke-width: 2.5; }
.rack-pp-bad .ua-mark { stroke: #e0312b; stroke-width: 3; }
.rack-pp-doc .ua-mark { stroke: #4c5662; stroke-width: 2; }
.rack-pp-port.rack-hl .ua-mark { fill: rgba(0,212,255,.25); stroke: #00b8e6; stroke-width: 3.5; }
/* ── Shelves drawn to scale, and the ZimaBoard 832 (static/fronts.js) ─ */
.sh-back { fill: #0f1215; }
.sh-floor { fill: #3a414b; }
.sh-lip { fill: #59636f; }
.sh-name { fill: var(--rk-muted, #8b97a5); font-size: 11px; font-family: Helvetica, Arial, sans-serif; }
.sh-empty { fill: var(--rk-muted, #8b97a5); font-size: 13px; }
.sh-box { fill: #d4d7dc; stroke: #8a929c; }
.sh-box-name { fill: #15191e; font-size: 13px; font-weight: 700; font-family: Helvetica, Arial, sans-serif; }
.sh-tag { fill: #111; stroke: #3a3f45; }
.sh-tag-text { fill: #e9edf1; font-size: 9px; font-weight: 700; font-family: Helvetica, Arial, sans-serif; }
.sh-hit { fill: transparent; stroke: transparent; stroke-width: 2.5; }
.sh-thing:hover .sh-hit, .sh-thing:focus-visible .sh-hit { stroke: #00b8e6; }
.zb-case { fill: #66625b; }
.zb-case-edge { fill: #807c74; }
.zb-case-top { fill: #8b877f; }
.zb-pcb { fill: #a48f55; }
.zb-pad { fill: #d3bd73; }
.zb-base { fill: #121212; }
.zb-base-sheen { fill: #2b2b2b; }
.zb-glow { fill: #ff9a2e; opacity: .85; filter: blur(4px); }
.zb-shield { fill: #cfcfca; stroke: #8f8f8a; stroke-width: 3; }
.zb-hole { fill: #0b0b0b; }
.zb-hole-key { fill: #0b0b0b; }
.zb-contact { fill: #b89545; }
.zb-led { stroke: #5c5c58; stroke-width: 1.5; }
.zb-usb-tongue { fill: #2f7fe0; }
.zb-usb-pin { fill: #c7a64b; }
.zb-dp-pins { fill: #b89545; }
.zb-dc { fill: #3a3a3a; stroke: #222; stroke-width: 2; }
.zb-dc-face { fill: #2b2b2b; }
.zb-dc-ring { fill: #4b4b4b; }
.zb-dc-pin { fill: #c9a43a; }
.cx-pcb { fill: #1d6b43; }
.cx-pcb-edge { fill: #2f8a5a; }
.cx-part { fill: #26292d; }
.cx-heatsink { fill: #18191b; }
.cx-fin { fill: #2c2e31; }
.cx-cage { fill: #c8cbcf; stroke: #85898f; stroke-width: 3; }
.cx-cage-lip { fill: #dde0e3; }
.cx-plug { fill: #2b2e33; }
.cx-boot { fill: #1b1d20; }
.cx-cable { fill: #0e0f10; }
.cx-tab { fill: none; stroke: #c9ccd0; stroke-width: 9; stroke-linejoin: round; }
.cx-link { stroke: #101418; stroke-width: 3; }
.fp-speedbar { fill: #b3b5ba; }
.rf-sl-inside { fill: #141619; }
.rf-sl-ear { fill: #17181a; }
.rf-sl-edge { fill: none; stroke: rgba(0,0,0,.35); stroke-width: 2; }
.rf-sl-button { fill: #c9ccd0; stroke: #8a8e93; stroke-width: 2; }
.rf-sl-bay { opacity: .45; }
.rf-sl-io { fill: #0e0f11; }
.rf-sl-mount { fill: #0c0d0f; }
.rf-sl-handle { fill: #3a3e44; stroke: #0a0b0c; stroke-width: 2; }
.rf-sl-handle-shine { fill: #6b717a; }
.ua-ear-plate { fill: #d3d5d8; stroke: #b2b4b8; stroke-width: 1; }
.ua-ear-seam { fill: #9fa1a6; }
.ua-ear-slot { fill: #2a2c30; }
.ub-frame { fill: #bfc1c5; stroke: #a5a7ac; stroke-width: 1; }
.ub-frame-shadow { fill: #8c8e93; }
.ub-frame-bevel { fill: #f1f2f3; }
.ub-opening { fill: #0d0e10; }
.ub-seam { fill: #050506; opacity: .8; }
.ub-strand { fill: #000; }
/* Interfaces marked unused: greyed, never "down" */
.dot-unused { display: inline-block; width: 8px; height: 8px; border: 1px dashed var(--text-muted);
background: transparent; flex-shrink: 0; vertical-align: middle; }
.state-unused { color: var(--text-muted); }
.device-row--unused td { opacity: .6; }
.device-row--unused td:last-child { opacity: 1; }
.lt-btn-xs { padding: 1px 6px; font-size: .68rem; margin-left: 6px; }
/* ── Topology page ─────────────────────────────────────────────────── */
.tp-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; margin: 8px 0 10px; }
.tp-filters { display: flex; flex-wrap: wrap; gap: 6px 14px; }
.tp-filter { display: inline-flex; align-items: center; gap: 6px; font-size: .78em; color: var(--text-dim); cursor: pointer; }
.tp-zoom { display: inline-flex; gap: 4px; }
.tp-updated { margin-left: auto; font-size: .72em; color: var(--text-muted); }
.tp-warnings { border: 1px solid var(--amber); color: var(--amber); background: var(--amber-dim); padding: 8px 12px; margin-bottom: 10px; font-size: .8em; }
.tp-canvas {
position: relative; overflow: hidden; touch-action: none; cursor: grab;
height: clamp(420px, calc(100vh - 300px), 1400px);
border: 1px solid var(--border-color);
background-color: var(--bg2);
background-image: radial-gradient(circle, rgba(0,212,255,.07) 1px, transparent 1px);
background-size: 22px 22px;
}
[data-theme="light"] .tp-canvas { background-image: radial-gradient(circle, rgba(0,100,160,.08) 1px, transparent 1px); }
.tp-canvas:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.tp-canvas.tp-dragging { cursor: grabbing; }
.tp-svg { display: block; width: 100%; height: 100%; user-select: none; }
.tp-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.tp-loading[hidden] { display: none; }
/* Devices */
.tp-node-link { cursor: pointer; outline: none; }
.tp-node-hit { fill: transparent; stroke: transparent; stroke-width: 4; }
.tp-node-link:hover .tp-node-hit, .tp-node-link:focus-visible .tp-node-hit { stroke: var(--amber); fill: rgba(255,179,0,.04); }
.tp-name { font-family: var(--font); font-size: 40px; font-weight: 700; fill: var(--text); letter-spacing: .02em;
paint-order: stroke; stroke: var(--bg2); stroke-width: 10px; stroke-linejoin: round; }
.tp-sub { font-family: var(--font); font-size: 28px; fill: var(--text-muted);
paint-order: stroke; stroke: var(--bg2); stroke-width: 8px; stroke-linejoin: round; }
.tp-dot { stroke: rgba(0,0,0,.35); stroke-width: 2; }
.tp-dot-up { fill: #2fd36b; }
.tp-dot-degraded { fill: #ffb300; }
.tp-dot-down { fill: #e0312b; }
.tp-dot-unknown { fill: #6b737d; }
.tp-ring { fill: none; stroke-width: 7; }
.tp-ring-critical { stroke: #e0312b; }
.tp-ring-warning { stroke: #ffb300; }
.tp-ring-info { stroke: #34a8ff; }
.tp-badge circle { stroke: var(--bg2); stroke-width: 5; }
.tp-badge text { font-family: var(--font); font-size: 34px; font-weight: 700; fill: #fff; }
.tp-badge-critical circle { fill: #e0312b; }
.tp-badge-warning circle { fill: #c98a00; }
.tp-badge-info circle { fill: #1f7ad6; }
.tp-kind-stub .tp-name { font-size: 32px; font-weight: 400; fill: var(--text-dim); }
.tp-kind-stub .tp-sub { font-size: 24px; }
.tp-cloud { fill: #dfe7ef; stroke: #8fa3b5; stroke-width: 4; }
.tp-cloud-text { font-family: var(--font); font-size: 44px; font-weight: 700; fill: #34495e; letter-spacing: .12em; }
.tp-st-down .tp-cloud { fill: #f3d6d4; stroke: #e0312b; }
.tp-modem-body { fill: #16181b; stroke: #050506; stroke-width: 3; }
.tp-modem-edge { fill: none; stroke: #2c3035; stroke-width: 3; }
.tp-modem-globe { fill: #1f6fd1; }
.tp-modem-globe-line { fill: none; stroke: #9cc6ff; stroke-width: 3; }
.tp-modem-model { font-family: var(--font); font-size: 22px; fill: #8b929b; }
.tp-led { stroke: #000; stroke-width: 2; }
.tp-led-on { fill: #2fd36b; }
.tp-led-off { fill: #3a3f45; }
.tp-led-bad { fill: #e0312b; }
.tp-ap-body { fill: #f2f3f4; stroke: #c3c6ca; stroke-width: 4; }
.tp-ap-ring { fill: none; stroke-width: 10; stroke: #6b737d; }
.tp-ap-up { stroke: #2a8cff; }
.tp-ap-down { stroke: #e0312b; }
.tp-ap-dot { fill: #d7dadd; }
.tp-strip-body { fill: #f0f1f2; stroke: #c3c6ca; stroke-width: 4; }
.tp-strip-outlet rect:first-of-type { fill: #fafafa; stroke: #b9bcc0; stroke-width: 3; }
.tp-strip-slot { fill: #2b2d31; }
.tp-strip-outlet.tp-off rect:first-of-type { fill: #d9dadc; }
.tp-box, .tp-host-box { fill: #22272e; stroke: #3a434e; stroke-width: 3; }
.tp-box-text { font-family: var(--font); font-size: 34px; fill: #8b97a5; }
.tp-host-led { stroke: #000; stroke-width: 2; }
.tp-gen-body { fill: #2a2f36; stroke: #12151a; stroke-width: 3; }
.tp-gen-model { font-family: var(--font); font-size: 30px; fill: #c9d1d9; }
.tp-gen-port { fill: #111418; stroke: #4b5562; stroke-width: 2; }
.tp-gen-up { stroke: #2fd36b; }
.tp-gen-num { font-family: var(--font); font-size: 16px; fill: #8b97a5; }
.tp-stub-body { fill: #3a3f46; stroke: #23272c; stroke-width: 3; }
.tp-stub-jack { fill: #15181c; }
.tp-stub-q { font-family: var(--font); font-size: 30px; font-weight: 700; fill: #9aa3ad; }
.tp-st-down .tp-stub-body { stroke: #e0312b; }
/* Host NIC sockets */
.tp-strip { fill: #1b1f24; stroke: #2f363f; stroke-width: 3; }
.tp-nic-rj45, .tp-nic-sfp { fill: #0b0d10; stroke: #59626e; stroke-width: 3; }
.tp-nic-sfp-in { fill: #23272d; }
.tp-nic-name { font-family: var(--font); font-size: 18px; fill: #aab4bf; }
.tp-nic-led { fill: #3a3f45; }
.tp-nic-up .tp-nic-led { fill: #2fd36b; }
.tp-nic-up .tp-nic-rj45, .tp-nic-up .tp-nic-sfp { stroke: #8f9aa6; }
.tp-nic-down .tp-nic-led { fill: #e0312b; }
.tp-nic-down .tp-nic-rj45, .tp-nic-down .tp-nic-sfp { stroke: #e0312b; }
.tp-nic-unused { opacity: .45; }
.tp-nic-unused .tp-nic-rj45, .tp-nic-unused .tp-nic-sfp { stroke-dasharray: 6 5; }
.tp-nic-alert-critical .tp-nic-name { fill: #ff6b5e; }
.tp-nic-alert-warning .tp-nic-name { fill: #ffb300; }
/* Cables: colours are the real jackets, the same in both themes */
.tp-link { cursor: pointer; outline: none; }
.tp-link path { fill: none; stroke-linecap: round; }
.tp-casing { stroke: #050607; }
.tp-core { stroke: #2f343b; }
.tp-hit { stroke: transparent; stroke-width: 40; }
.tp-s-10g .tp-casing { stroke-width: 17; } .tp-s-10g .tp-core { stroke-width: 11; }
.tp-s-1g .tp-casing { stroke-width: 12; } .tp-s-1g .tp-core { stroke-width: 7; }
.tp-s-100m .tp-casing { stroke-width: 8; } .tp-s-100m .tp-core { stroke-width: 4.5; }
.tp-m-dac .tp-casing { stroke: #8a929c; } .tp-m-dac .tp-core { stroke: #16181c; }
.tp-m-fibre .tp-casing { stroke: #13757c; } .tp-m-fibre .tp-core { stroke: #38c9d3; }
.tp-m-wanfibre .tp-casing { stroke: #8a6a00; } .tp-m-wanfibre .tp-core { stroke: #f2c230; }
.tp-m-sfp .tp-casing { stroke: #23272c; } .tp-m-sfp .tp-core { stroke: #69717c; }
.tp-m-copper .tp-casing { stroke: #173f80; } .tp-m-copper .tp-core { stroke: #3b82f6; }
.tp-down .tp-casing { stroke: transparent; }
.tp-down .tp-core { stroke: #e0312b; stroke-dasharray: 28 18; }
.tp-ghost { opacity: .8; }
.tp-ghost .tp-casing { stroke: transparent; }
.tp-ghost .tp-core { stroke: #e0312b; stroke-dasharray: 2 18; stroke-width: 10; }
.tp-plug { fill: #c9ccd1; stroke: #5d636b; stroke-width: 2; }
.tp-boot { fill: #1e5bbf; stroke: #0d2f66; stroke-width: 2; }
.tp-links-over { pointer-events: none; }
.tp-links-over .tp-warn { pointer-events: auto; }
.tp-flow { stroke: rgba(255,255,255,.4); stroke-width: 3; stroke-dasharray: 3 60; animation: tp-flow linear infinite; pointer-events: none; }
.tp-s-100m .tp-flow { stroke-width: 2; }
@keyframes tp-flow { to { stroke-dashoffset: -63; } }
@media (prefers-reduced-motion: reduce) { .tp-flow { animation: none; display: none; } }
.tp-link:hover .tp-casing, .tp-link:focus-visible .tp-casing, .tp-selected .tp-casing { stroke: var(--amber); }
.tp-tag rect { fill: var(--bg-primary); stroke: var(--border-color); stroke-width: 2; opacity: .92; }
.tp-tag text { font-family: var(--font); font-size: 24px; fill: var(--text); }
.tp-tag-ghost rect { stroke: #e0312b; }
.tp-tag-ghost text { fill: #ff6b5e; }
.tp-rate { display: none; pointer-events: none; }
.tp-near .tp-rate, .tp-hot .tp-rate, .tp-selected .tp-rate { display: inline; }
.tp-warn path { fill: #ffb300; stroke: #5c4100; stroke-width: 3; }
.tp-warn text { font-family: var(--font); font-size: 30px; font-weight: 700; fill: #2b1f00; }
/* Filters and the traced path */
.tp-no-traffic .tp-rate, .tp-no-traffic .tp-flow { display: none !important; }
.tp-only-10g .tp-slow { display: none; }
.tp-alerts-only .tp-node:not(.tp-has-alert) { opacity: .25; }
.tp-alerts-only .tp-link:not(.tp-touches-alert) { opacity: .2; }
.tp-tracing .tp-node:not(.tp-hot), .tp-tracing .tp-link:not(.tp-hot) { opacity: .18; transition: opacity .15s; }
.tp-tracing .tp-link.tp-hot .tp-casing { stroke: var(--amber); }
.tp-far .tp-sub, .tp-far .tp-nic-name { display: none; }
/* Cable panel: a side panel, a bottom sheet on phones */
.tp-panel {
position: absolute; top: 12px; right: 12px; width: min(360px, calc(100% - 24px));
max-height: calc(100% - 24px); overflow: auto; z-index: 2;
background: var(--bg-primary); border: 1px solid var(--border-color); box-shadow: 0 8px 24px rgba(0,0,0,.35);
padding: 10px 14px 14px; cursor: auto; font-size: .8em;
}
.tp-panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.tp-panel-title { font-size: 1em; margin: 0; letter-spacing: .06em; text-transform: uppercase; color: var(--amber); }
.tp-panel-table { width: 100%; border-collapse: collapse; }
.tp-panel-table th { text-align: left; font-weight: 400; color: var(--text-muted); padding: 4px 10px 4px 0; vertical-align: top; width: 42%; }
.tp-panel-table td { padding: 4px 0; overflow-wrap: anywhere; }
.tp-panel-warn { border: 1px solid var(--amber); background: var(--amber-dim); color: var(--text); padding: 6px 8px; margin: 0 0 8px; }
.tp-bad { color: var(--red); }
.tp-warn-text { color: var(--amber); }
.tp-dim-text { color: var(--text-muted); }
@media (max-width: 640px) {
.tp-panel { top: auto; left: 0; right: 0; bottom: 0; width: auto; max-height: 55%; border-width: 1px 0 0; }
.tp-updated { margin-left: 0; width: 100%; }
.tp-canvas { height: clamp(380px, calc(100vh - 260px), 900px); }
}
.tp-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 10px 0 4px; font-size: .74em; color: var(--text-dim); }
.tp-key { display: inline-flex; align-items: center; gap: 6px; }
.tp-key svg { width: 34px; height: 12px; }
.tp-key-line { fill: none; stroke-width: 5; stroke-linecap: round; }
.tp-key-line.tp-m-dac { stroke: #2b2f35; }
.tp-key-line.tp-m-fibre { stroke: #38c9d3; }
.tp-key-line.tp-m-wanfibre { stroke: #f2c230; }
.tp-key-line.tp-m-sfp { stroke: #69717c; }
.tp-key-line.tp-m-copper { stroke: #3b82f6; }
.tp-key-down { stroke: #e0312b; stroke-dasharray: 6 4; }
.tp-key-ghost { stroke: #e0312b; stroke-dasharray: 1 5; }
.tp-key-warn { color: #ffb300; font-weight: 700; }