diff --git a/src/web/src/cgi/auth_html.c b/src/web/src/cgi/auth_html.c
index bdc78ed..173916c 100755
--- a/src/web/src/cgi/auth_html.c
+++ b/src/web/src/cgi/auth_html.c
@@ -18,6 +18,7 @@
/* 受保护的 HTML 页面列表 */
static const char *protected_pages[] = {
+ "overview.html",
"status_info.html",
"sys_set.html",
"sys_rf.html",
@@ -43,7 +44,7 @@ static const char* get_requested_page(const char *query_string) {
static char page_name[256] = {0};
if (!query_string || strlen(query_string) == 0) {
- return "status_info.html"; /* 默认页面 */
+ return "overview.html"; /* 默认页面 */
}
/* 解析 query string 中的 page=xxx 参数 */
@@ -57,7 +58,7 @@ static const char* get_requested_page(const char *query_string) {
return page_name;
}
- return "status_info.html";
+ return "overview.html";
}
/* 发送重定向响应 */
@@ -130,7 +131,7 @@ int main(int argc, char **argv) {
if (cookie && extract_cookie(cookie, SESSION_COOKIE_NAME,
session_id, sizeof(session_id))) {
if (validate_session(session_id, NULL, 0)) {
- send_redirect("status_info.html");
+ send_redirect("overview.html");
}
}
/* 否则返回登录页 */
diff --git a/src/web/src/cgi_login/cgi_main.c b/src/web/src/cgi_login/cgi_main.c
index a2a1f10..a44881c 100644
--- a/src/web/src/cgi_login/cgi_main.c
+++ b/src/web/src/cgi_login/cgi_main.c
@@ -51,6 +51,29 @@ int simplified_create_client_socket(int port)
return sockfd;
}
+/* Read an application value for a public login-page endpoint. */
+static int simplified_get_data_from_process(int modulePort, int moduleKey, char *buf, size_t bufLen)
+{
+ cgi_process_elem_t elem;
+ int fd;
+
+ if(!buf || bufLen == 0 || modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE)
+ return -1;
+ memset(&elem, 0, sizeof(elem));
+ elem.method = GET_METHOD;
+ elem.keyType = moduleKey;
+ fd = simplified_create_client_socket(modulePort);
+ if(fd < 0)
+ return -1;
+ if(write(fd, &elem, sizeof(elem)) != sizeof(elem) || read(fd, &elem, sizeof(elem)) != sizeof(elem)){
+ close(fd);
+ return -1;
+ }
+ close(fd);
+ snprintf(buf, bufLen, "%s", elem.data);
+ return 0;
+}
+
/* Define simplified version of set_data_from_process to avoid linking issues */
void simplified_set_data_from_process(int modulePort, int moduleKey, char *buf) {
if(modulePort <= START_PROCESS_MODULE || modulePort >= END_PROCESS_MODULE)
@@ -398,7 +421,7 @@ int main(int argc, char **argv) {
sizeof(cookie_header), 0);
printf("Content-type: text/html\r\n");
printf("%s\r\n", cookie_header);
- printf("Location: status_info.html\r\n\r\n");
+ printf("Location: overview.html\r\n\r\n");
exit(0);
}
}
@@ -412,6 +435,16 @@ int main(int argc, char **argv) {
/* === Handle GET request - get current username === */
if (method && strcasecmp(method, "GET") == 0 && query) {
+ /* Public read-only title used before a session exists on login.html. */
+ if (strstr(query, "action=get_title")) {
+ char title[MSG_DATA_LEN] = {0};
+ printf("Content-type: text/plain\r\nCache-Control: no-store\r\n\r\n");
+ if(simplified_get_data_from_process(WEB_SOCKET_PORT, CGIKEY_WEB_TITLE,
+ title, sizeof(title)) == 0)
+ printf("%s", title);
+ exit(0);
+ }
+
/* Check for get_user action */
if (strstr(query, "action=get_user")) {
char *cookie = getenv("HTTP_COOKIE");
diff --git a/src/web/src/cgi_login/cgi_main.h b/src/web/src/cgi_login/cgi_main.h
index b485779..2817e89 100644
--- a/src/web/src/cgi_login/cgi_main.h
+++ b/src/web/src/cgi_login/cgi_main.h
@@ -19,7 +19,7 @@
#include
#include
-#define USER_WEB_INDEX_HTML "status_info.html"
+#define USER_WEB_INDEX_HTML "overview.html"
#endif
diff --git a/src/web/www/css/styles.css b/src/web/www/css/styles.css
index c3c6dcf..4e71619 100755
--- a/src/web/www/css/styles.css
+++ b/src/web/www/css/styles.css
@@ -7,7 +7,7 @@
--accent-green-bright: #4ade80;
--accent-green-dim: #16a34a;
/* Error / alert */
- --status-err: #ef4444;
+ --status-err: #ff5252;
--status-err-dim: #dc2626;
/* Warning / amber */
--status-warn: #f59e0b;
@@ -23,15 +23,54 @@
/* Text */
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
- --text-muted: #9ca3af;
+ --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 */
+ /* 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);
@@ -41,6 +80,31 @@
--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;
}
* {
@@ -165,20 +229,41 @@ body {
.app-sidebar li.active a {
background: transparent;
color: var(--sidebar-text-active);
- border-left: 3px solid var(--accent-green);
+ 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(--text-muted);
+ color: var(--sidebar-footer-p);
font-size: 12px;
line-height: 1.5;
border-top: 1px solid var(--border-subtle);
}
.sidebar-footer h4 {
- color: var(--text-secondary);
+ color: var(--sidebar-footer-h);
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
@@ -192,11 +277,11 @@ body {
.sidebar-footer .redbox-label {
font-weight: 600;
- color: var(--text-secondary);
+ color: var(--sidebar-footer-h);
}
.sidebar-footer a {
- color: var(--status-neutral);
+ color: var(--sidebar-footer-link);
font-weight: 500;
}
@@ -537,8 +622,8 @@ body {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
- background-color: var(--accent-green);
- color: #fff;
+ background-color: var(--btn-green-bg);
+ color: var(--btn-green-text-hover);
padding: 4px 10px;
border-radius: 6px;
margin-left: auto;
@@ -547,7 +632,9 @@ body {
}
.box-header.collapsed .toggle-text {
- background-color: var(--text-muted);
+ background-color: var(--btn-neutral-bg);
+ color: var(--btn-neutral-text);
+ border: 1px solid var(--border-subtle);
}
/* Accordion Specific Settings */
@@ -773,8 +860,8 @@ select.display-only-select:disabled.state-ok {
/* Buttons */
input[type="button"].btn-modern,
input[type="submit"].btn-modern {
- background-color: var(--accent-green-dim);
- color: var(--text-primary);
+ background-color: var(--btn-green-bg);
+ color: var(--btn-green-text);
font-size: 13px;
font-weight: 600;
padding: 8px 18px;
@@ -788,14 +875,14 @@ input[type="submit"].btn-modern {
input[type="button"].btn-modern:hover,
input[type="submit"].btn-modern:hover {
- background-color: var(--accent-green);
- border-color: var(--accent-green);
- color: #fff;
+ 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(--accent-green-dim);
+ background-color: var(--btn-green-bg);
}
input[type="button"].btn-modern:disabled,
@@ -809,12 +896,26 @@ input[type="submit"].btn-modern:disabled {
/* Unsaved reminder */
input[type="button"].btn-modern.is-unsaved:not(:disabled),
input[type="submit"].btn-modern.is-unsaved:not(:disabled) {
- background-color: var(--accent-green) !important;
- border-color: var(--accent-green) !important;
- color: #fff !important;
+ 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;
@@ -1073,9 +1174,9 @@ BUTTON VIEW DETAILS
}
.btn-view-details:hover {
- background-color: var(--accent-green);
- border-color: var(--accent-green);
- color: #fff;
+ background-color: var(--btn-green-bg-hover);
+ border-color: var(--btn-green-bg-hover);
+ color: var(--btn-green-text-hover);
}
/* ==========================================================================
@@ -1372,12 +1473,12 @@ BUTTON VIEW DETAILS
/* PLP row layout — same style as subframe-header: label | bar | bitrate | pct */
.plp-row {
- display: grid;
- grid-template-columns: 42px 1fr auto auto;
- gap: 2px 10px;
- align-items: center;
- padding: 6px 0;
- border-bottom: 1px solid rgba(75, 85, 99, 0.4);
+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 {
@@ -1395,7 +1496,7 @@ BUTTON VIEW DETAILS
text-align: center;
padding: 4px 0;
border-radius: 4px;
- background: rgba(75, 85, 99, 0.3);
+ background: var(--plp-pill-bg);
}
/* Bar track (background) */
@@ -1465,20 +1566,13 @@ BUTTON VIEW DETAILS
.plp-pct-text.pct-red { color: var(--status-err); }
.plp-pct-text.pct-muted { color: var(--text-muted); }
-/* Detail line (services + FULL indicator, below bar) */
-.plp-detail {
- grid-column: 2 / -1;
- grid-row: 2;
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
-}
-
+/* FULL indicator — beside pct text in the same row */
.plp-full-indicator {
- font-size: 11px;
- font-weight: 700;
- color: var(--status-err);
+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;
@@ -1488,32 +1582,53 @@ BUTTON VIEW DETAILS
}
.plp-full-indicator.visible {
- display: inline-block;
- animation: alarmPulse 1.5s ease-in-out infinite;
+ display: inline-block;
+ animation: alarmPulse 1.5s ease-in-out infinite;
}
-.plp-services {
- font-size: 13px;
- font-weight: 600;
- color: var(--status-neutral);
- max-width: 400px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+/* 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;
}
-.plp-services:empty {
- display: none;
+/* 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;
}
-.plp-services::before {
- content: '[';
- color: var(--border-subtle);
+/* 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 */
}
-.plp-services::after {
- content: ']';
- color: var(--border-subtle);
+/* 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) */
@@ -1589,22 +1704,16 @@ BUTTON VIEW DETAILS
/* Responsive adjustments for utilization bars */
@media (max-width: 767px) {
- .plp-row {
- grid-template-columns: 36px 1fr auto;
- }
+.plp-row {
+grid-template-columns: 48px 1fr auto auto;
+}
.plp-row .plp-bitrate-text {
font-size: 11px;
}
.plp-pct-text {
display: none;
}
- .plp-detail {
- flex-wrap: wrap;
- }
- .plp-services {
- max-width: none;
- }
- .subframe-header {
+.subframe-header {
grid-template-columns: 48px 1fr;
}
.subframe-header .plp-bitrate-text {
@@ -1615,12 +1724,12 @@ BUTTON VIEW DETAILS
.global-header {
grid-template-columns: 48px 1fr;
}
- .global-header .plp-bitrate-text {
- grid-column: 1 / -1;
- text-align: center;
- margin-top: 4px;
- }
-}
+.global-header .plp-bitrate-text {
+ grid-column: 1 / -1;
+ text-align: center;
+ margin-top: 4px;
+ }
+ }
/* ==========================================================================
SESSION TIMEOUT WARNING
@@ -1694,3 +1803,448 @@ SESSION TIMEOUT WARNING
.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
+