/* ==========================================================================
EXCITER DASHBOARD - Responsive Modern Dark Theme (Fixed Bottom Dashboard)
========================================================================== */
:root {
/* Vibrant green - primary accent, status good, active states */
--accent-green: #22c55e;
--accent-green-bright: #4ade80;
--accent-green-dim: #16a34a;
/* Error / alert */
--status-err: #ff5252;
--status-err-dim: #dc2626;
/* Warning / amber */
--status-warn: #f59e0b;
--status-warn-dim: #eab308;
/* Neutral / info */
--status-neutral: #38bdf8;
--status-neutral-dim: #0ea5e9;
/* Dark palette */
--bg-main: #111827;
--bg-panel: #1f2937;
--bg-card: #374151;
--bg-card-hover: #4b5563;
/* Text */
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-muted: #b0b8c4;
--plp-pill-bg: #3a4452;
--chart-inset: #2a313c;
/* Informational text/labels (status notes, progress labels). Light value set in .light-theme. */
--info-text: var(--status-neutral);
/* Mode identity colours (overview Signal Chain page only). Light values set in .light-theme.
Gateway intentionally reuses --accent-green / --accent-green-bright (brand). */
--mode-exciter: #60a5fa; --mode-exciter-hover: #93bbfd;
--mode-translator: #fb923c; --mode-translator-hover: #fdba74;
--mode-standalone: #a78bfa; --mode-standalone-hover: #c4b5fd;
--mode-exciter-tint: rgba(96,165,250,0.10);
--mode-translator-tint: rgba(251,146,60,0.10);
--mode-standalone-tint: rgba(167,139,250,0.10);
--mode-gateway-tint: rgba(34,197,94,0.10);
/* Stronger (hover) variants of the mode tints. */
--mode-exciter-tint-strong: rgba(96,165,250,0.22);
--mode-translator-tint-strong: rgba(251,146,60,0.22);
--mode-standalone-tint-strong: rgba(167,139,250,0.22);
--mode-gateway-tint-strong: rgba(34,197,94,0.22);
/* Borders */
--border-subtle: #4b5563;
--border-accent: var(--accent-green);
/* Sidebar (DARK ISLAND — sidebar stays dark navy in BOTH themes, so these
tokens are NOT overridden in .light-theme; see the .light-theme block). */
--sidebar-bg: #0f172a;
--sidebar-text: #94a3b8;
--sidebar-text-active: #ffffff;
--sidebar-hover: #1e293b;
/* Active-item accent. Lives as its own token (not --accent-green) because the
sidebar is a dark island: this MUST stay bright green in light theme, whereas
--accent-green darkens. Same value as --accent-green in dark theme. */
--sidebar-accent: #22c55e;
/* Primary-action button family (theme-aware). Dark values = today's look,
so dark-only products are visually unchanged. Light values softened
(entry 011) so buttons sit in harmony on the light page, not as heavy
dark blocks. Status dots keep --accent-green and stay vivid. */
--btn-green-bg: var(--accent-green-dim);
--btn-green-bg-hover: var(--accent-green);
--btn-green-text: #06230f;
--btn-green-text-hover: #06230f;
--btn-neutral-bg: #4b5563;
--btn-neutral-text: #ffffff;
/* Sidebar footer/labels: PINNED readable colours (sidebar is a dark island,
so these must NOT follow the theme tokens, which flip dark in light mode). */
--sidebar-footer-h: #cbd5e1;
--sidebar-footer-p: #94a3b8;
--sidebar-footer-link: #7fb3d5;
--sidebar-label: #7c8595;
/* Legacy / UI */
--primary: var(--accent-green);
--primary-hover: var(--accent-green-dim);
--font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
/* Layout */
--container-width: 1250px;
--sidebar-width: 220px;
--header-height: 64px;
--dashboard-height: auto;
/* ===== PLOT PALETTE (family, added 2026-08-11) ==============================
Colours for the spectrum + eye canvases on the Measurements page. Present as
DORMANT family tokens: only smod renders a Measurements page today, but the
page is planned for other products, so the tokens live in the shared sheet
rather than being hand-copied later (the exact mistake that motivated this
whole stylesheet consolidation). Values are the instrument-plot palette; four
of them already equal existing family tokens (trace=--status-neutral,
hold=--status-warn, band=neutral@10%, eye-level near neutral) and are kept as
separate names so a family colour change cannot silently restyle a scope plot.
The plot field stays dark in BOTH themes on purpose: the eye is a density map
whose floor must read as "no signal", which a dark field gives and a light one
does not. Not overridden in .light-theme, deliberately. =================== */
--plot-bg: #0b1220;
--plot-grid: #1e293b;
--plot-grid-zero: #334155;
--plot-axis-label: #94a3b8;
--plot-axis-title: #64748b;
--plot-trace: #38bdf8;
--plot-trace-hold: #f59e0b;
--plot-trace-error: #eab308;
--plot-marker: #f87171;
--plot-band: rgba(56, 189, 248, 0.10);
--plot-eye-level: rgba(57, 208, 255, 0.45);
--plot-eye-decision: #a3e635;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: var(--font-family);
}
html {
font-size: 14px;
}
body {
background-color: var(--bg-main);
color: var(--text-primary);
font-size: 14px;
line-height: 1.5;
height: 100vh;
overflow: hidden;
}
/* ==========================================================================
LAYOUT GRID SYSTEM - Dashboard under header
========================================================================== */
.app-layout {
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: var(--header-height) var(--dashboard-height) 1fr;
grid-template-areas:
"header header"
"sidebar dashboard"
"sidebar main";
height: 100vh;
}
/* ==========================================================================
HEADER
========================================================================== */
.app-header {
grid-area: header;
background: var(--bg-panel);
border-bottom: 2px solid var(--accent-green);
display: flex;
align-items: center;
padding: 0 24px;
position: relative;
}
.app-header h1 {
font-size: 18px;
font-weight: 700;
color: var(--accent-green);
margin-right: 12px;
display: flex;
align-items: center;
}
.app-header h1 .logo-bg {
width: 180px;
height: 64px;
background: url(../images/logo.png) no-repeat left center;
background-size: contain;
}
.app-header h2 {
font-size: 16px;
color: var(--accent-green);
font-weight: 600;
border-left: 1px solid var(--border-subtle);
padding-left: 24px;
margin-left: 12px;
}
.app-header h3 {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
text-align: right;
margin-left: auto;
padding-left: 16px;
}
/* ==========================================================================
SIDEBAR
========================================================================== */
.app-sidebar {
grid-area: sidebar;
background: var(--sidebar-bg);
display: flex;
flex-direction: column;
z-index: 101;
border-right: 1px solid var(--border-subtle);
overflow: hidden;
}
.app-sidebar ul.items {
list-style: none;
margin-bottom: auto;
padding: 12px 0;
flex: 1;
overflow-y: auto;
}
.app-sidebar li a {
display: block;
padding: 11px 16px;
color: var(--sidebar-text);
text-decoration: none;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
border-left: 3px solid transparent;
border-radius: 0 6px 6px 0;
}
.app-sidebar li a:hover {
background: var(--sidebar-hover);
color: var(--sidebar-text-active);
}
.app-sidebar li.active a {
background: transparent;
color: var(--sidebar-text-active);
border-left: 3px solid var(--sidebar-accent);
font-weight: 600;
}
/* --- Sidebar group labels (Operations / Configuration / Admin) ---
Added for ATSC 3.0 pages. Scoped by the .app-sidebar ancestor plus
the .ov-group-label class, so these rules only affect
elements
that opt in with class="ov-group-label". No existing pages in other
products (7x, 2D Marble, ATSC 1.0) use this class, so adding these
rules has zero visual or behavioral impact on them.
The existing .app-sidebar li a and .app-sidebar li.active a rules
require an child; these label items have no , so no collision. */
.app-sidebar li.ov-group-label {
padding: 16px 16px 4px 16px;
font-size: 9px;
font-weight: 700;
letter-spacing: 1.6px;
text-transform: uppercase;
color: var(--sidebar-label);
border-left: 3px solid transparent;
pointer-events: none;
user-select: none;
}
.app-sidebar li.ov-group-label:first-child { padding-top: 6px; }
.sidebar-footer {
padding: 16px;
color: var(--sidebar-footer-p);
font-size: 12px;
line-height: 1.5;
border-top: 1px solid var(--border-subtle);
}
.sidebar-footer h4 {
color: var(--sidebar-footer-h);
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
}
.sidebar-footer p {
font-size: 12px;
font-weight: 400;
margin: 0;
}
.sidebar-footer .redbox-label {
font-weight: 600;
color: var(--sidebar-footer-h);
}
.sidebar-footer a {
color: var(--sidebar-footer-link);
font-weight: 500;
}
/* ==========================================================================
MAIN CONTENT - Scrollable area (1250px centered)
========================================================================== */
.app-main {
grid-area: main;
padding: 20px 24px;
background: var(--bg-main);
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
display: flex;
flex-direction: column;
align-items: center;
}
.app-main > * {
width: 100%;
max-width: 1250px;
}
.app-main::-webkit-scrollbar {
width: 8px;
}
.app-main::-webkit-scrollbar-track {
background: var(--bg-main);
}
.app-main::-webkit-scrollbar-thumb {
background: var(--border-subtle);
border-radius: 4px;
}
.app-main::-webkit-scrollbar-thumb:hover {
background: var(--bg-card-hover);
}
/* ==========================================================================
FIXED DASHBOARD SUMMARY - Top of page, under header
========================================================================== */
.dashboard-fixed {
grid-area: dashboard;
background: var(--bg-panel);
border-top: 2px solid var(--accent-green);
padding: 8px 24px;
z-index: 100;
overflow: hidden;
display: flex;
justify-content: center;
}
.summary-dashboard {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 8px;
width: 100%;
max-width: 1250px;
height: 100%;
}
.summary-card {
background: var(--bg-card);
border-radius: 8px;
border: 1px solid var(--border-subtle);
padding: 6px 8px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
min-height: 0;
}
.summary-card:hover {
border-color: var(--bg-card-hover);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.summary-card:has(.value-err) {
border-left: 3px solid var(--status-err);
background: rgba(239, 68, 68, 0.08);
animation: errPulseBg 2s ease-in-out infinite;
}
@keyframes errPulseBg {
0%, 100% { background: rgba(239, 68, 68, 0.08); }
50% { background: rgba(239, 68, 68, 0.18); }
}
.summary-card:has(.value-warn) {
border-left: 3px solid var(--status-warn);
background: rgba(245, 158, 11, 0.08);
}
.summary-label-row {
display: flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
.status-dot.status-ok {
background-color: var(--accent-green);
box-shadow: 0 0 4px rgba(34, 197, 94, 0.5);
}
.status-dot.status-err {
background-color: var(--status-err);
box-shadow: 0 0 4px rgba(239, 68, 68, 0.5);
}
.status-dot.status-warn {
background-color: var(--status-warn);
box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);
}
.status-dot.status-neutral {
background-color: var(--status-neutral);
box-shadow: 0 0 4px rgba(56, 189, 248, 0.4);
}
.status-dot.status-off {
background-color: var(--text-muted);
box-shadow: 0 0 3px rgba(156, 163, 175, 0.3);
}
.summary-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.summary-value-wrapper {
display: flex;
align-items: baseline;
gap: 2px;
min-width: 0;
}
.summary-value {
font-size: 16px;
font-weight: 700;
line-height: 1.1;
transition: color 0.2s ease;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.summary-value.value-ok { color: var(--accent-green); }
.summary-value.value-err { color: var(--status-err); }
.summary-value.value-warn { color: var(--status-warn); }
.summary-value.value-neutral { color: var(--status-neutral); }
.summary-value.value-muted { color: var(--text-muted); }
.summary-value.value-off { color: var(--text-muted); }
.summary-unit {
font-size: 9px;
font-weight: 500;
color: var(--text-muted);
}
/* ==========================================================================
STATUS ALERT STRIP
========================================================================== */
.content-page-title {
font-size: 18px;
font-weight: 600;
color: var(--accent-green);
margin: 0 0 12px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-subtle);
}
.content-page-title:empty {
display: none;
margin: 0;
padding: 0;
border: none;
}
#status-alert-strip, .status-alert {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 13px;
font-weight: 600;
transition: all 0.3s ease;
}
.status-alert-icon {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
#status-alert-strip.status-alert-ok, .status-alert-ok {
background: rgba(34, 197, 94, 0.15);
border: 1px solid rgba(34, 197, 94, 0.4);
color: var(--accent-green);
}
#status-alert-strip.status-alert-ok .status-alert-icon, .status-alert-ok .status-alert-icon {
background: var(--accent-green);
box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);
}
#status-alert-strip.status-alert-warn, .status-alert-warn {
background: rgba(245, 158, 11, 0.15);
border: 1px solid rgba(245, 158, 11, 0.4);
color: var(--status-warn);
}
#status-alert-strip.status-alert-warn .status-alert-icon, .status-alert-warn .status-alert-icon {
background: var(--status-warn);
}
#status-alert-strip.status-alert-err, .status-alert-err {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.4);
color: var(--status-err);
}
#status-alert-strip.status-alert-err .status-alert-icon, .status-alert-err .status-alert-icon {
background: var(--status-err);
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
#status-alert-detail, .status-alert-detail {
color: var(--text-secondary);
font-weight: 500;
}
.status-alert-detail:empty {
display: none;
}
/* ==========================================================================
CONTENT SECTIONS
========================================================================== */
.app-main h2 {
color: var(--text-primary);
font-size: 17px;
font-weight: 600;
margin: 20px 0 10px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-subtle);
}
.app-main h2:first-of-type {
margin-top: 0;
}
.box {
background: var(--bg-card);
border-radius: 10px;
border: 1px solid var(--border-subtle);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
padding: 14px;
margin-bottom: 14px;
}
/* Grid system for content boxes */
.content-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.content-grid .box {
margin-bottom: 0;
}
.content-grid-full {
grid-column: 1 / -1;
}
/* ==========================================================================
BOX ACCORDION HEADER
========================================================================== */
.box-header {
padding: 10px 14px;
background-color: var(--bg-card-hover);
border-bottom: 1px solid var(--border-subtle);
margin: -14px -14px 14px -14px;
border-radius: 10px 10px 0 0;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: var(--text-primary);
transition: background-color 0.2s ease;
user-select: none;
}
.box-header:hover {
background-color: var(--border-subtle);
}
.box-header::after {
content: '\25BC';
font-size: 12px;
color: var(--accent-green);
}
.box-header.has-error::after,
.box-header.has-error.collapsed::after {
color: var(--status-err) !important;
}
.box-header.collapsed::after {
content: '\25C0';
}
.box-header.collapsed {
border-bottom: none;
margin-bottom: -14px;
border-radius: 10px;
}
.box-header .toggle-text {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
background-color: var(--btn-green-bg);
color: var(--btn-green-text-hover);
padding: 4px 10px;
border-radius: 6px;
margin-left: auto;
margin-right: 10px;
transition: background-color 0.2s ease;
}
.box-header.collapsed .toggle-text {
background-color: var(--btn-neutral-bg);
color: var(--btn-neutral-text);
border: 1px solid var(--border-subtle);
}
/* Accordion Specific Settings */
.app-main .sysset-accordion .box-header {
border-radius: 10px 10px 0 0;
}
.app-main .sysset-accordion .box-header.collapsed {
border-radius: 10px;
margin-bottom: 0;
}
.app-main .sysset-accordion .box-content {
display: none;
padding: 14px;
margin: 0 -14px -14px -14px;
border-radius: 0 0 10px 10px;
border-top: 1px solid var(--border-subtle);
}
.app-main .sysset-accordion .box-header:not(.collapsed) + .box-content {
display: block;
}
.app-main .sysset-accordion .box-header.accordion-header-warning {
background-color: rgba(239, 68, 68, 0.15);
border-left: 4px solid var(--status-err);
}
.app-main .sysset-accordion .box-header.accordion-header-warning:hover {
background-color: rgba(239, 68, 68, 0.22);
}
.app-main .sysset-accordion .box-header.accordion-header-warning::after {
color: var(--status-err);
}
/* ==========================================================================
TABLES & FORMS
========================================================================== */
table {
width: 100%;
border-collapse: collapse;
}
td {
padding: 6px 8px;
vertical-align: middle;
color: var(--text-primary);
border-bottom: 1px solid var(--border-subtle);
}
.app-main .box table td {
border-bottom: 1px solid var(--border-subtle);
}
.app-main .box table tr:last-child td {
border-bottom: none;
}
td:nth-child(odd) {
font-weight: 600;
color: var(--text-secondary);
font-size: 12px;
letter-spacing: 0.4px;
min-width: 130px;
width: 1%;
white-space: nowrap;
background-color: transparent;
}
td:nth-child(even) {
min-width: 0;
width: auto;
}
tr:last-child td {
border-bottom: none;
}
/* Advanced Table Layouts */
.app-main .box table.table-layout-4col,
.app-main .box table.table-layout-dgv,
.app-main .box table.table-layout-freq,
.app-main .box table.table-layout-standard {
table-layout: fixed;
width: 100%;
text-align: left;
}
.app-main .box table.table-layout-freq td:first-child,
.app-main .box table.table-layout-standard td:first-child {
width: 260px;
min-width: 260px;
max-width: 260px;
white-space: nowrap;
}
.app-main .box table.table-layout-4col td:first-child {
width: 260px;
min-width: 260px;
max-width: 260px;
white-space: nowrap;
}
.app-main .box table.table-layout-4col td:nth-child(2) {
width: 220px;
min-width: 220px;
max-width: 220px;
}
.app-main .box table.table-layout-4col td:nth-child(3) {
width: 260px;
min-width: 260px;
max-width: 260px;
white-space: nowrap;
}
.app-main .box table.table-layout-dgv td:first-child {
width: 260px;
min-width: 260px;
max-width: 260px;
white-space: nowrap;
}
.app-main .box table.table-layout-dgv td:nth-child(2) {
width: 100px;
min-width: 100px;
max-width: 100px;
}
.app-main .box table.table-layout-dgv td:nth-child(3) {
width: 120px;
min-width: 120px;
max-width: 120px;
white-space: nowrap;
}
.app-main .box table.table-layout-dgv td:nth-child(4) {
width: 260px;
min-width: 260px;
max-width: 260px;
white-space: nowrap;
}
.app-main .box table.table-layout-dgv td:nth-child(5) {
width: 100px;
min-width: 100px;
max-width: 100px;
}
.app-main .box table.table-layout-dgv td:nth-child(6) {
width: auto;
min-width: 0;
}
/* Error highlighting */
tr.row-has-error td {
background: rgba(239, 68, 68, 0.08);
}
tr.row-has-error td:nth-child(odd) {
background: rgba(239, 68, 68, 0.12);
}
tr.header-row td {
background-color: var(--bg-card-hover);
color: var(--text-primary);
font-weight: 700;
font-size: 13px;
}
/* Log Export Styles */
.log-export-box td.log-export-label {
min-width: 220px;
width: auto;
white-space: nowrap;
}
.log-export-box td.log-export-actions {
display: flex;
align-items: center;
gap: 12px;
}
/* Inputs */
input[type="text"],
input[type="password"],
input[readonly],
input:not([type]),
select {
width: 100%;
max-width: 90px;
padding: 6px 8px;
font-size: 13px;
color: var(--text-secondary);
background-color: var(--bg-panel);
border: 1px solid var(--border-subtle);
border-radius: 6px;
transition: all 0.2s ease;
outline: none;
}
.app-main select {
width: auto;
min-width: 100px;
max-width: 180px;
}
input:focus,
.app-main select:focus,
select:focus {
border-color: var(--accent-green);
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
}
input[readonly] {
background-color: var(--bg-panel);
border-color: var(--border-subtle);
color: var(--text-secondary);
font-weight: 500;
cursor: default;
}
/* Display-only select menus */
select.display-only-select:disabled {
opacity: 1;
color: var(--text-secondary);
-webkit-text-fill-color: var(--text-secondary);
}
select.display-only-select:disabled.state-error {
color: var(--status-err) !important;
-webkit-text-fill-color: var(--status-err) !important;
font-weight: bold !important;
}
select.display-only-select:disabled.state-ok {
color: var(--accent-green) !important;
-webkit-text-fill-color: var(--accent-green) !important;
font-weight: bold !important;
}
/* Buttons */
input[type="button"].btn-modern,
input[type="submit"].btn-modern {
background-color: var(--btn-green-bg);
color: var(--btn-green-text);
font-size: 13px;
font-weight: 600;
padding: 8px 18px;
border: 1px solid var(--border-subtle);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
text-transform: none;
}
input[type="button"].btn-modern:hover,
input[type="submit"].btn-modern:hover {
background-color: var(--btn-green-bg-hover);
border-color: var(--btn-green-bg-hover);
color: var(--btn-green-text-hover);
}
input[type="button"].btn-modern:active,
input[type="submit"].btn-modern:active {
background-color: var(--btn-green-bg);
}
input[type="button"].btn-modern:disabled,
input[type="submit"].btn-modern:disabled {
background-color: var(--bg-card);
color: var(--text-muted);
border-color: var(--border-subtle);
cursor: not-allowed;
}
/* Unsaved reminder */
input[type="button"].btn-modern.is-unsaved:not(:disabled),
input[type="submit"].btn-modern.is-unsaved:not(:disabled) {
background-color: var(--btn-green-bg) !important;
border-color: var(--btn-green-bg-hover) !important;
color: var(--btn-green-text-hover) !important;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.35);
}
/* Cancel / secondary modal button — neutral, theme-aware (replaces hardcoded #64748b). */
input[type="button"].btn-modern.btn-cancel,
input[type="submit"].btn-modern.btn-cancel {
background-color: var(--btn-neutral-bg);
color: var(--btn-neutral-text);
border-color: var(--border-subtle);
}
input[type="button"].btn-modern.btn-cancel:hover,
input[type="submit"].btn-modern.btn-cancel:hover {
background-color: var(--btn-neutral-bg);
border-color: var(--text-muted);
color: var(--btn-neutral-text);
}
fieldset {
border: none;
display: flex;
align-items: center;
gap: 6px;
}
fieldset input {
text-align: center;
}
.app-main .box table td fieldset {
justify-content: flex-start;
}
.app-main .box table td fieldset input {
text-align: left;
}
/* ==========================================================================
DROPDOWN
========================================================================== */
.dropdown {
position: relative;
display: inline-block;
}
.dropbtn-modern {
background-color: var(--bg-card);
color: var(--text-primary);
font-size: 12px;
font-weight: 600;
padding: 6px 12px;
border: 1px solid var(--border-subtle);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
outline: none;
text-transform: none;
}
.dropdown:hover .dropbtn-modern {
border-color: var(--accent-green);
color: var(--accent-green-bright);
}
.dropdown-content {
display: none;
position: absolute;
right: 0;
background-color: var(--bg-card);
min-width: 160px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
border-radius: 8px;
border: 1px solid var(--border-subtle);
z-index: 1000;
overflow: hidden;
}
.dropdown-content a {
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
padding: 10px 14px;
text-decoration: none;
display: flex;
align-items: center;
gap: 10px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.dropdown-content a:hover {
background-color: var(--sidebar-hover);
color: var(--accent-green-bright);
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown-content a svg {
width: 16px;
height: 16px;
color: var(--text-muted);
}
.dropdown-content a:hover svg {
color: var(--accent-green);
}
/* ==========================================================================
MODAL
========================================================================== */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
.modal-content {
background-color: var(--bg-card);
margin: 10% auto;
padding: 0;
border-radius: 12px;
border: 1px solid var(--border-subtle);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
width: 400px;
animation: modalFadeIn 0.3s ease-out;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.modal-header {
background-color: var(--bg-card-hover);
padding: 15px 20px;
border-bottom: 1px solid var(--border-subtle);
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 12px 12px 0 0;
}
.modal-header h3 {
margin: 0;
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
}
.modal-header .close {
color: var(--text-secondary);
font-size: 24px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
}
.modal-header .close:hover {
color: var(--status-err);
}
.modal-body {
padding: 20px;
}
.modal-table {
width: 100%;
}
.modal-table td {
border: none;
padding: 10px 5px;
color: var(--text-primary);
}
.modal-table td:nth-child(odd) {
width: 40%;
text-align: right;
padding-right: 15px;
color: var(--text-secondary);
}
.modal-table td:nth-child(even) {
width: 60%;
}
.modal-table input[type="text"],
.modal-table input[type="password"] {
width: 100%;
max-width: none;
padding: 8px 10px;
box-sizing: border-box;
background-color: var(--bg-panel);
border-color: var(--border-subtle);
color: var(--text-primary);
}
.modal-footer {
padding-top: 20px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* ==========================================================================
LOGIN PAGE SPECIFIC
========================================================================== */
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - var(--header-height) - var(--dashboard-height) - 40px);
}
.login-box {
width: 100%;
max-width: 400px;
}
/* ==========================================================================
UTILITY CLASSES
========================================================================== */
/* Highlighted input fields (editable emphasis) */
.highlight {
border-color: var(--accent-green) !important;
background-color: rgba(34, 197, 94, 0.08);
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.highlight:focus {
background-color: rgba(34, 197, 94, 0.15);
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
}
/* Clearfix for floated elements */
.clear::after {
content: "";
display: table;
clear: both;
}
/* Responsive table wrapper for horizontal scroll on narrow screens */
.table-responsive-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* ==========================================================================
BUTTON VIEW DETAILS
========================================================================== */
.btn-view-details {
display: inline-block;
background-color: var(--bg-card-hover);
color: var(--text-primary);
font-size: 12px;
font-weight: 600;
padding: 8px 16px;
border: 1px solid var(--border-subtle);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
margin-top: 12px;
}
.btn-view-details:hover {
background-color: var(--btn-green-bg-hover);
border-color: var(--btn-green-bg-hover);
color: var(--btn-green-text-hover);
}
/* ==========================================================================
HAMBURGER MENU TOGGLE
========================================================================== */
.menu-toggle {
display: none;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: none;
border: 1px solid var(--border-subtle);
border-radius: 6px;
cursor: pointer;
padding: 0;
margin-right: 12px;
transition: border-color 0.2s ease;
}
.menu-toggle:hover {
border-color: var(--accent-green);
}
.menu-toggle svg {
width: 20px;
height: 20px;
color: var(--text-primary);
}
/* ==========================================================================
RESPONSIVE BREAKPOINTS
========================================================================== */
/* 1080P and above (1920px+) */
@media (min-width: 1920px) {
:root {
--container-width: 1440px;
--sidebar-width: 240px;
}
.summary-dashboard {
grid-template-columns: repeat(12, 1fr);
}
.content-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Standard 1080P (1250px - 1919px) */
@media (min-width: 1250px) and (max-width: 1919px) {
:root {
--container-width: 1250px;
--sidebar-width: 220px;
}
.summary-dashboard {
grid-template-columns: repeat(6, 1fr);
}
.content-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Tablet landscape (1024px - 1279px) */
@media (min-width: 1024px) and (max-width: 1279px) {
:root {
--container-width: 100%;
--sidebar-width: 200px;
}
.summary-dashboard {
grid-template-columns: repeat(6, 1fr);
}
.content-grid {
grid-template-columns: 1fr;
}
}
/* Tablet portrait (768px - 1023px) */
@media (min-width: 768px) and (max-width: 1023px) {
.app-layout {
grid-template-columns: 1fr;
grid-template-rows: var(--header-height) var(--dashboard-height) 1fr;
grid-template-areas:
"header"
"dashboard"
"main";
}
/* Overlay sidebar */
.app-sidebar {
position: fixed;
top: 0;
left: -220px;
width: 220px;
height: 100vh;
z-index: 500;
transition: left 0.3s ease;
box-shadow: none;
}
.app-sidebar.sidebar-open {
left: 0;
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
}
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 499;
}
.sidebar-overlay.active {
display: block;
}
.menu-toggle {
display: flex;
}
.summary-dashboard {
grid-template-columns: repeat(4, 1fr);
}
.content-grid {
grid-template-columns: 1fr;
}
}
/* Mobile (< 768px) */
@media (max-width: 767px) {
:root {
}
.app-layout {
grid-template-columns: 1fr;
grid-template-rows: var(--header-height) var(--dashboard-height) 1fr;
grid-template-areas:
"header"
"dashboard"
"main";
}
/* Overlay sidebar */
.app-sidebar {
position: fixed;
top: 0;
left: -220px;
width: 220px;
height: 100vh;
z-index: 500;
transition: left 0.3s ease;
box-shadow: none;
}
.app-sidebar.sidebar-open {
left: 0;
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
}
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 499;
}
.sidebar-overlay.active {
display: block;
}
.menu-toggle {
display: flex;
}
.summary-dashboard {
grid-template-columns: repeat(3, 1fr);
}
.content-grid {
grid-template-columns: 1fr;
}
.app-header {
padding: 0 16px;
}
.app-main {
padding: 16px;
}
.dashboard-fixed {
padding: 8px 16px;
}
}
/* Very small screens */
@media (max-width: 480px) {
:root {
}
.summary-dashboard {
grid-template-columns: repeat(2, 1fr);
}
}
/* ==========================================================================
GATEWAY STATUS - UTILIZATION BARS
========================================================================== */
/* Total Status - enhanced alarm display */
.total-status-grid {
display: grid;
grid-template-columns: auto 1fr auto 1fr;
align-items: center;
gap: 4px 12px;
padding: 4px 0;
}
.total-status-grid .status-label {
font-weight: 600;
color: var(--text-secondary);
font-size: 12px;
letter-spacing: 0.4px;
white-space: nowrap;
}
.total-status-grid .status-value {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
}
.total-status-grid .status-value.alarm-ok {
color: var(--accent-green);
}
.total-status-grid .status-value.alarm-overflow {
color: var(--status-err);
animation: alarmPulse 1.2s ease-in-out infinite;
}
.total-status-grid .frame-time-value {
font-size: 15px;
font-weight: 700;
color: var(--status-neutral);
font-variant-numeric: tabular-nums;
}
.total-status-grid .frame-time-unit {
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
margin-left: 3px;
}
@keyframes alarmPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.alarm-overflow-border {
animation: alarmBorderPulse 1.2s ease-in-out infinite;
}
@keyframes alarmBorderPulse {
0%, 100% { border-color: var(--status-err); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
50% { border-color: var(--status-err); box-shadow: 0 0 12px 2px rgba(239, 68, 68, 0.35); }
}
/* Subframe container */
.subframe-title {
font-size: 15px;
font-weight: 700;
color: var(--accent-green);
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-subtle);
letter-spacing: 0.3px;
}
/* PLP row layout — same style as subframe-header: label | bar | bitrate | pct */
.plp-row {
display: grid;
grid-template-columns: 56px 1fr auto auto auto;
gap: 2px 10px;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(75, 85, 99, 0.4);
}
.plp-row:last-of-type {
border-bottom: none;
}
/* PLP label (PLP0, PLP1, etc.) */
.plp-label {
grid-row: 1;
font-size: 12px;
font-weight: 800;
color: var(--text-secondary);
letter-spacing: 0.8px;
text-transform: uppercase;
text-align: center;
padding: 4px 0;
border-radius: 4px;
background: var(--plp-pill-bg);
}
/* Bar track (background) */
.plp-bar-track {
grid-column: 2;
grid-row: 1;
position: relative;
height: 18px;
background: rgba(75, 85, 99, 0.35);
border-radius: 4px;
overflow: hidden;
}
/* Bar fill (colored portion) */
.plp-bar-fill {
height: 100%;
border-radius: 4px;
min-width: 0;
transition: width 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94),
background-color 0.4s ease;
position: relative;
}
/* Bar fill color states — muted tones for dark theme readability */
.plp-bar-fill.bar-green {
background: linear-gradient(90deg, #166534, #22c55e);
opacity: 0.75;
}
.plp-bar-fill.bar-amber {
background: linear-gradient(90deg, #92400e, #f59e0b);
opacity: 0.78;
}
.plp-bar-fill.bar-red {
background: linear-gradient(90deg, #991b1b, #ef4444);
opacity: 0.78;
}
.plp-bar-fill.bar-muted {
background: var(--bg-card-hover);
}
/* Bitrate text (right of bar, same line) */
.plp-row .plp-bitrate-text {
grid-column: 3;
grid-row: 1;
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
/* Percentage text (right of bitrate, same line) */
.plp-pct-text {
grid-column: 4;
grid-row: 1;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.plp-pct-text.pct-green { color: var(--accent-green); }
.plp-pct-text.pct-amber { color: var(--status-warn); }
.plp-pct-text.pct-red { color: var(--status-err); }
.plp-pct-text.pct-muted { color: var(--text-muted); }
/* FULL indicator — beside pct text in the same row */
.plp-full-indicator {
grid-column: 5;
grid-row: 1;
font-size: 11px;
font-weight: 700;
color: var(--status-err);
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.35);
padding: 1px 7px;
border-radius: 4px;
white-space: nowrap;
display: none;
}
.plp-full-indicator.visible {
display: inline-block;
animation: alarmPulse 1.5s ease-in-out infinite;
}
/* PLP trend sparkline container — inline chart below each utilization bar.
JS renderer writes SVG into this container on each 2-sec poll.
Placed in grid row 2 (row 1 = bar + pct + FULL indicator) spanning the bar columns.
Services displayed as left sidebar panel inside the SVG when present. */
.plp-trend-spark {
grid-column: 2 / -1;
grid-row: 2;
margin-top: 4px;
overflow: visible;
}
/* Hide trend container when PLP row is hidden */
.plp-row[style*="display: none"] .plp-trend-spark,
.plp-row[style*="display:none"] .plp-trend-spark {
display: none;
}
/* Sparkline SVG element — fills container width.
ViewBox 700×120 with preserveAspectRatio="none" — stretches to match bar width.
When services exist, left margin widens (ML += svcBandW) to accommodate sidebar panel. */
.plp-trend-svg {
display: block;
width: 100%;
/* height set dynamically by JS via inline style — matches viewBox ratio */
}
/* Max/min envelope band — semi-transparent green fill (set inline by JS). */
.plp-trend-svg polygon {
stroke: none;
}
/* Subframe header trend — matches bar height */
.subframe-header .plp-trend-spark {
grid-column: 2 / -1;
grid-row: 2;
margin-top: 4px;
}
/* Global header trend — extra tall for emphasis */
.global-header .plp-trend-spark {
grid-column: 2 / -1;
grid-row: 2;
margin-top: 4px;
}
/* Subframe header bar (total payload at top) */
.subframe-header {
display: grid;
grid-template-columns: 56px 1fr auto;
gap: 0 10px;
align-items: center;
padding: 2px 0 10px 0;
margin-bottom: 6px;
border-bottom: 1px solid var(--border-subtle);
}
.subframe-header .summary-label {
font-size: 11px;
font-weight: 800;
color: var(--accent-green);
letter-spacing: 0.8px;
text-transform: uppercase;
text-align: center;
}
.subframe-header .plp-bar-track {
height: 22px;
border: 1px solid var(--border-subtle);
}
.subframe-header .plp-bar-fill {
border-radius: 3px;
}
.subframe-header .plp-bitrate-text {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.subframe-header .summary-pct {
font-weight: 700;
margin-left: 4px;
}
.subframe-header .summary-pct.pct-green { color: var(--accent-green); }
.subframe-header .summary-pct.pct-amber { color: var(--status-warn); }
.subframe-header .summary-pct.pct-red { color: var(--status-err); }
/* Global payload bar (inside Total Status box) */
.global-header {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border-subtle);
border-bottom: none;
padding-bottom: 2px;
margin-bottom: 0;
}
.global-header .plp-bar-track {
height: 26px;
border: 1px solid var(--border-subtle);
}
.global-header .summary-label {
color: var(--status-neutral);
font-size: 12px;
}
.global-header .plp-bitrate-text {
font-size: 14px;
font-weight: 800;
}
/* Responsive adjustments for utilization bars */
@media (max-width: 767px) {
.plp-row {
grid-template-columns: 48px 1fr auto auto;
}
.plp-row .plp-bitrate-text {
font-size: 11px;
}
.plp-pct-text {
display: none;
}
.subframe-header {
grid-template-columns: 48px 1fr;
}
.subframe-header .plp-bitrate-text {
grid-column: 1 / -1;
text-align: center;
margin-top: 4px;
}
.global-header {
grid-template-columns: 48px 1fr;
}
.global-header .plp-bitrate-text {
grid-column: 1 / -1;
text-align: center;
margin-top: 4px;
}
}
/* ==========================================================================
SESSION TIMEOUT WARNING
========================================================================== */
#session-timeout-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
}
.session-timeout-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
}
.session-timeout-dialog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-panel, #1e293b);
border: 1px solid var(--status-warn, #f59e0b);
border-radius: 12px;
padding: 28px 32px;
text-align: center;
min-width: 320px;
max-width: 420px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.session-timeout-icon {
font-size: 40px;
margin-bottom: 12px;
}
.session-timeout-dialog h3 {
margin: 0 0 12px 0;
color: var(--status-warn, #f59e0b);
font-size: 18px;
}
.session-timeout-dialog p {
margin: 0 0 8px 0;
color: var(--text-secondary, #94a3b8);
font-size: 14px;
line-height: 1.5;
}
.session-timeout-dialog #session-countdown {
font-weight: 700;
color: var(--status-err, #ef4444);
font-size: 16px;
}
.session-timeout-actions {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: center;
}
.session-timeout-actions .btn-modern {
min-width: 120px;
}
/* =============================================================================
Sprint 1 — dashboard bar mode-adaptive visibility + group separators
Additive only. No existing rule, selector, or variable is modified.
Shared with 2D and exciter UIs — these classes are new and will only apply
where the new data-tile attribute is present on summary-card elements.
============================================================================= */
/* Marker class for tile-hidden state (reserved; current implementation uses
inline style="display:none" set by applyBarTileVisibility for idempotency.
Class kept for future use: allow CSS-only hiding via toggled class if we
move to a transition/animation approach later.) */
.summary-card[data-tile].tile-hidden { display: none; }
/* Group-separator spacing for the bar.
The bar is a flex row; group separation is a slightly larger left margin
on the first tile of each group (applied via a data-group-first attribute
that pages set on select tiles).
Purely additive. Tiles without data-group-first are unaffected. */
.summary-card[data-group-first="input"] { margin-left: 8px; }
.summary-card[data-group-first="output"] { margin-left: 8px; }
.summary-card[data-group-first="housekeeping"] { margin-left: 8px; }
/* =============================================================================
Sprint 1 Increment 6 — Live Trends panel (Overview page only)
Additive only. No existing rule, selector, or variable is modified.
These classes are new (ov-trend-*); they only apply to the new panel
container on Overview and do not affect the shared 2D / exciter UI.
============================================================================= */
/* Panel container — visual weight matches the existing ov-chain-card. */
.ov-trend-panel {
background: var(--bg-panel);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 16px 20px;
margin: 16px 0;
}
.ov-trend-panel:empty { display: none; }
/* Header row — small caps, dot, title, buffer-age label. */
.ov-trend-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 12px;
text-transform: uppercase;
}
.ov-trend-header .ov-trend-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--status-ok, #22c55e);
box-shadow: 0 0 4px rgba(34, 197, 94, 0.5);
}
.ov-trend-header .ov-trend-title {
color: var(--text-primary);
font-weight: 600;
}
.ov-trend-header .ov-trend-sep {
opacity: 0.5;
}
.ov-trend-header .ov-trend-age {
color: var(--text-muted);
}
/* Grid of per-metric cards. Uses flex so cards auto-size to available
width, wrapping to a new row only when needed. */
.ov-trend-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.ov-trend-card {
flex: 1 1 200px;
min-width: 180px;
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: 6px;
padding: 10px 12px;
}
.ov-trend-label {
font-size: 10px;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 4px;
}
.ov-trend-value {
display: flex;
align-items: baseline;
gap: 4px;
margin-bottom: 8px;
white-space: nowrap;
}
.ov-trend-num {
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.ov-trend-unit {
font-size: 10px;
color: var(--text-muted);
/* No text-transform: units carry meaningful case (Mbps / dB / dBm /
dBc / MHz / %). Matches the signal chain's .ov-blk-primary .ov-u
convention; avoids collapsing dB / dBm / dBc distinctions into the
same upper-cased suffix. */
letter-spacing: 0.04em;
}
/* The sparkline itself. Height fixed; width fills the card. */
.ov-trend-spark {
display: block;
width: 100%;
height: 40px;
stroke: var(--status-ok, #22c55e);
stroke-width: 1.5;
fill: none;
stroke-linejoin: round;
stroke-linecap: round;
}
/* N/A overlay wrapper -- anchors the overlay text above the sparkline.
Positioned so overlay sits centered on the sparkline area. */
.ov-trend-spark-wrap {
position: relative;
}
/* Shown on FSSI-affected charts (SNR, FWD%, UIMD, LIMD) when FSSI is
active. Per v0.4 B4: overlay reads "N/A -- FSSI active" and line
drawing is suppressed (handled by extractors returning null).
pointer-events: none so the overlay never intercepts clicks. */
.ov-trend-na-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
letter-spacing: 0.04em;
color: var(--text-muted, #8a8a8a);
background: rgba(0, 0, 0, 0.04);
pointer-events: none;
text-transform: none;
}
/* When FSSI-active, tone down the card's current-value number. */
.ov-trend-card-na .ov-trend-num-na {
color: var(--text-muted, #8a8a8a);
font-weight: 500;
}
/* Min/max labels overlaid on the sparkline's right edge. Show the raw
data range (not the minSpan-floored display range) so operators can
always read the actual values — important when minSpan stretches a
tiny real variation into a taller chart. pointer-events: none so the
labels never intercept interaction with the svg. */
.ov-trend-spark-labels {
position: absolute;
top: 0;
right: 2px;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
pointer-events: none;
font-family: ui-monospace, Menlo, Consolas, monospace;
font-size: 9px;
line-height: 1;
color: var(--text-secondary);
}
.ov-trend-label-max,
.ov-trend-label-min {
/* Units are not repeated on these labels — the card's main value
already shows the unit, and repeating it here adds clutter. */
padding: 1px 2px;
background: var(--bg-main);
border-radius: 2px;
}
/* ==========================================================================
CONTEXTUAL WARNING NOTES (added 2026-05-31)
Shared classes for the amber "this feature is gated / mode-restricted"
messages that were previously hand-written inline on each page.
Two variants:
.warning-banner — block callout (tinted box) above a section
.field-note — small inline note appended after a field/label
IMPORTANT: these classes intentionally do NOT set `display`. Visibility is
toggled by page JS via `element.style.display = ''` (show) / 'none' (hide),
which relies on the element's own inline `display:none` default. Adding
`display` here would break that toggling (a cleared inline value would fall
through to this rule). Keep `style="display:none"` on the element.
========================================================================== */
.warning-banner {
padding: 8px 14px;
margin-bottom: 8px;
font-size: 12px;
color: var(--status-warn);
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.2);
border-radius: 4px;
}
.field-note {
font-size: 11px;
color: var(--status-warn);
margin-left: 8px;
}
/* Error state for .warning-banner — red instead of amber (e.g. expired cert).
Toggle `is-error` in JS instead of setting inline colors. */
.warning-banner.is-error {
color: var(--status-err);
background: rgba(239, 68, 68, 0.08);
border-color: rgba(239, 68, 68, 0.2);
}
/* ==========================================================================
SECONDARY (CANCEL) BUTTON (added 2026-05-31)
Neutral-grey companion to .btn-modern, for Cancel / dismiss actions (e.g. the
account-settings modal). Use: class="btn-modern btn-secondary"
It only overrides the colour of .btn-modern; size/shape/hover come from
.btn-modern. Theme-aware via --bg-card-hover.
========================================================================== */
input[type="button"].btn-secondary,
input[type="submit"].btn-secondary {
background-color: var(--bg-card-hover);
}
input[type="button"].btn-secondary:hover,
input[type="submit"].btn-secondary:hover {
background-color: var(--border-subtle);
border-color: var(--border-subtle);
}
/* ==========================================================================
PROGRESS BARS (added 2026-05-31)
Restart / reset / firmware-update progress. The bar fill is styled via the
browser pseudo-elements (the plain `color` property does NOT colour a