Files
meshcore-analyzer/public/style.css
T
Kpa-clawbot 6d17cac40e fix(#1058): fluid + container-queried analytics chart grid (#1098)
## Summary
Makes the analytics chart grid fluid and auto-stacking based on its
**own** width rather than the viewport's. Implements task 5 of #1050.

## What changed
- `public/style.css` — `.analytics-charts` section only:
- Replaced `grid-template-columns: 1fr 1fr` with `repeat(auto-fit,
minmax(min(100%, 380px), 1fr))` so columns wrap when intrinsic space is
too narrow.
- Added `container-type: inline-size` so the grid is a query container
and descendants/future tweaks can size against its own width rather than
the viewport. The `auto-fit minmax` already handles the stack-on-narrow
case, so the previously-included `@container (max-width: 800px)` rule
was redundant and has been dropped to keep one source of truth.
- `min-width: 0` on cards and `max-width: 100%; height: auto` on
`<svg>`/`<canvas>` (descendant selector, robust to wrapper elements
between the card and the chart media) to prevent intrinsic-content
overflow.
- Switched hardcoded `12px` / `16px` spacing to the #1054 tokens
`--space-sm` / `--space-md`.
- Removed the redundant `@media (max-width: 768px) { .analytics-charts {
grid-template-columns: 1fr; } }` rule (the fluid grid supersedes it).

No `analytics.js` / `node-analytics.js` markup changes were required —
the existing classes are reused.

## TDD
- **Red commit (47f56e9)** — `test-analytics-fluid-charts.js`: failing
E2E that loads `public/style.css` against a sized harness and asserts no
overflow + correct stacking. On master: assertion failures on
container-type opt-in + wide-viewport / narrow-container stacking.
- **Green commit (d300dfa)** — CSS fix; all assertions pass.

## E2E (mandatory frontend coverage)
`node test-analytics-fluid-charts.js` — Playwright + Chromium against a
`file://` harness, 8/8 assertions:
- `.analytics-charts` opts in to container queries (`container-type:
inline-size`)
- viewport 1440 / wrapper 1300px → side-by-side (≥2 cols), no overflow
- viewport 1080 / wrapper 1040px → no horizontal overflow
- viewport 768 / wrapper 760px → cards stack to 1 column, no overflow
- viewport 1440 / wrapper 600px → cards stack via fluid grid (the
original bug)
- viewport 1920 / wrapper 1880px → side-by-side (≥2 cols), no overflow
(AC4)
- viewport 2560 / wrapper 2520px → side-by-side (≥2 cols), no overflow
(AC4)
- AC3: open at 1440px wide (side-by-side), shrink wrapper to 760px /
viewport to 768px, assert layout reflows to 1 column (charts redraw on
resize, not stuck at initial value)

`node test-fluid-scaffolding.js` — still green (15/15), confirms #1054
tokens are unaffected.

Partial fix for #1058

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 08:04:25 -07:00

2855 lines
122 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* === CoreScope — style.css === */
/* ============================================================
* FLUID SCAFFOLDING (issue #1054)
* ------------------------------------------------------------
* Global design tokens for spacing, typography, and container
* layout. All values use clamp()/min() so the layout scales
* smoothly between ~768px and ~2560px viewports without media
* queries. Targets at the historic 1440px design width match
* the previous hardcoded px values to within ~1px so existing
* pages render identically there.
*
* Component-specific spacing/typography (nav, tables, charts,
* map, packets, analytics, …) lives in its own marked region
* further below — DO NOT add component CSS in this region.
* ============================================================ */
:root {
/* --- Fluid spacing scale ---------------------------------
* Targets at 1440px viewport: 4 / 8 / 16 / 24 / 32 / 48 px.
* Min/max clamps keep small viewports usable and prevent
* runaway growth on ultra-wide displays.
*/
--space-xs: clamp(3px, 0.15vw + 2px, 6px);
--space-sm: clamp(6px, 0.30vw + 4px, 12px);
--space-md: clamp(10px, 0.50vw + 8px, 20px);
--space-lg: clamp(16px, 0.75vw + 12px, 32px);
--space-xl: clamp(24px, 1.00vw + 16px, 48px);
--space-2xl: clamp(32px, 2.00vw + 20px, 64px);
/* --- Fluid type scale ------------------------------------
* Targets at 1440px viewport: 13 / 16 / 18 / 24 / 32 px.
* Floors ensure readability at 768px; caps prevent giant
* text at 2560px+.
*/
--fs-sm: clamp(12px, 0.15vw + 11px, 14px);
--fs-md: clamp(14px, 0.20vw + 13px, 17px);
--fs-lg: clamp(15px, 0.30vw + 14px, 20px);
--fs-xl: clamp(18px, 0.50vw + 16px, 28px);
--fs-2xl: clamp(22px, 0.75vw + 20px, 36px);
/* --- Fluid radii ----------------------------------------- */
--radius-sm: clamp(3px, 0.1vw + 2px, 6px);
--radius-md: clamp(6px, 0.2vw + 5px, 12px);
--radius-lg: clamp(10px, 0.3vw + 8px, 18px);
/* --- Container layout ------------------------------------
* --gutter scales the side padding; --content-max caps the
* usable content width but always leaves a gutter on each
* side at small viewports.
*/
--gutter: clamp(12px, 2vw, 32px);
--content-max: min(100% - (2 * var(--gutter)), 1600px);
--nav-bg: #0f0f23;
--nav-bg2: #1a1a2e;
--nav-text: #ffffff;
--nav-text-muted: #cbd5e1;
--nav-active-bg: rgba(74, 158, 255, 0.15);
--accent: #4a9eff;
--geo-filter-color: #3b82f6;
--status-green: #22c55e;
--status-yellow: #eab308;
--status-red: #ef4444;
--status-orange: #f97316;
--status-purple: #a855f7;
--status-amber: #f59e0b;
--status-amber-light: #fef3c7;
--status-amber-text: #92400e;
--path-inspector-speculative: #d97706;
--role-observer: #8b5cf6;
--accent-hover: #6db3ff;
--text: #1a1a2e;
--text-muted: #5b6370;
--border: #e2e5ea;
--row-stripe: #f9fafb;
--row-hover: #eef2ff;
--detail-bg: #ffffff;
--badge-radius: 12px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--mono: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
--input-bg: #fff;
--selected-bg: #dbeafe;
--surface-0: #f4f5f7;
--surface-1: #ffffff;
--surface-2: #ffffff;
--surface-3: #ffffff;
--content-bg: var(--surface-0);
--card-bg: var(--surface-1);
--hover-bg: rgba(0,0,0, 0.04);
--trace-ghost-color: #94a3b8;
}
/* ⚠️ DARK THEME VARIABLES — KEEP BOTH BLOCKS IN SYNC
The media query handles OS-level dark mode (auto); [data-theme="dark"] handles manual toggle.
When changing dark theme variables, update BOTH blocks below. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--status-green: #22c55e;
--status-yellow: #eab308;
--status-red: #ef4444;
--status-orange: #f97316;
--status-purple: #a855f7;
--status-amber: #f59e0b;
--status-amber-light: #422006;
--status-amber-text: #fcd34d;
--path-inspector-speculative: #f59e0b;
--surface-0: #0f0f23;
--surface-1: #1a1a2e;
--surface-2: #232340;
--surface-3: #2d2d50;
--content-bg: var(--surface-0);
--card-bg: var(--surface-1);
--text: #e2e8f0;
--text-muted: #a8b8cc;
--border: #334155;
--row-stripe: #1e1e34;
--row-hover: #2d2d50;
--detail-bg: #232340;
--input-bg: #1e1e34;
--selected-bg: #1e3a5f;
--hover-bg: rgba(255,255,255, 0.06);
--trace-ghost-color: #94a3b8;
--section-bg: #1e1e34;
}
}
/* ⚠️ DARK THEME VARIABLES — KEEP IN SYNC with @media block above */
[data-theme="dark"] {
--status-green: #22c55e;
--status-yellow: #eab308;
--status-red: #ef4444;
--status-orange: #f97316;
--status-purple: #a855f7;
--status-amber: #f59e0b;
--status-amber-light: #422006;
--status-amber-text: #fcd34d;
--surface-0: #0f0f23;
--surface-1: #1a1a2e;
--surface-2: #232340;
--surface-3: #2d2d50;
--content-bg: var(--surface-0);
--card-bg: var(--surface-1);
--text: #e2e8f0;
--text-muted: #a8b8cc;
--border: #334155;
--row-stripe: #1e1e34;
--row-hover: #2d2d50;
--detail-bg: #232340;
--input-bg: #1e1e34;
--selected-bg: #1e3a5f;
--hover-bg: rgba(255,255,255, 0.06);
--trace-ghost-color: #94a3b8;
--section-bg: #1e1e34;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; font-family: var(--font); font-size: var(--fs-md); background: var(--content-bg); color: var(--text); }
/* ============================================================
* COMPONENT STYLES — page-specific rules below.
* (Nav, tables, charts, map, packets, analytics, etc.)
* Tasks 1050-3..6 / 1052-* edit sections inside this region.
* ============================================================ */
/* === Skip Link === */
.skip-link { position: absolute; top: -100%; left: 16px; padding: 8px 16px; background: var(--accent); color: #fff; border-radius: 6px; z-index: 999; font-weight: 600; text-decoration: none; }
.skip-link:focus { top: 8px; }
/* === Focus Indicators === */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible,
.data-table tbody tr:focus-visible, [tabindex]:focus-visible {
outline: 2px solid var(--accent); outline-offset: 2px;
}
/* === Touch Targets === */
/* WCAG 2.5.5 / Apple HIG / Material: 48x48 CSS px minimum touch target for
all interactive controls. Targets are achieved with min-height/min-width
plus inline-flex centering so existing visual styling (font-size, padding,
icon size) is preserved on desktop while the *hit area* grows for touch.
Issue #1060. */
.nav-link { min-height: 48px; display: inline-flex; align-items: center; }
/* Generic button surfaces — filter bar, modal buttons, inline .btn usages.
inline-flex keeps text/icons centered without changing visible padding much. */
.btn,
.filter-bar .btn,
.filter-group .btn {
min-height: 48px;
min-width: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.btn-icon {
min-height: 48px;
min-width: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
touch-action: manipulation;
}
.nav-btn {
min-height: 48px;
min-width: 48px;
touch-action: manipulation;
}
.ch-icon-btn,
.ch-remove-btn,
.ch-share-btn {
min-height: 48px;
min-width: 48px;
touch-action: manipulation;
}
.ch-gear-btn {
min-height: 48px;
min-width: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
touch-action: manipulation;
}
.panel-close-btn {
min-height: 48px;
min-width: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
touch-action: manipulation;
}
.mc-jump-btn {
min-height: 48px;
min-width: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
touch-action: manipulation;
}
button.ch-item {
min-height: 48px;
touch-action: manipulation;
}
/* Additional button-like controls discovered during PR #1067 review (Issue
#1060 follow-up). Same 48x48 minimums + touch-action so all interactive
surfaces meet WCAG 2.5.5. */
.btn-link,
.col-toggle-btn,
.filter-toggle-btn,
.ch-add-channel-btn,
.ch-back-btn,
.ch-modal-btn-secondary,
.ch-scroll-btn,
.chooser-btn,
.clock-filter-btn,
.compare-btn,
.copy-link-btn,
.alab-btn {
min-height: 48px;
min-width: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.btn-link:active,
.col-toggle-btn:active,
.filter-toggle-btn:active,
.ch-add-channel-btn:active,
.ch-back-btn:active,
.ch-modal-btn-secondary:active,
.ch-scroll-btn:active,
.chooser-btn:active,
.clock-filter-btn:active,
.compare-btn:active,
.copy-link-btn:active,
.alab-btn:active {
background: var(--row-hover);
transform: scale(0.97);
opacity: 0.9;
}
/* Form controls: native <select> and text-like <input> need 48px tap targets
too. Scoped to interactive input types — checkbox/radio/range keep their
own visible size and rely on a wrapping label/parent for hit area. */
select,
input[type="text"],
input[type="search"],
input[type="number"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="url"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"] {
min-height: 48px;
box-sizing: border-box;
touch-action: manipulation;
}
/* Visible :active states — touch devices have no hover, so :active is the
primary feedback channel. Use opacity + slight scale + background shift so
the press is felt even when the user's finger covers the control. */
.btn:active,
.filter-bar .btn:active,
.filter-group .btn:active {
background: var(--row-hover);
transform: scale(0.97);
opacity: 0.9;
}
.btn-icon:active {
background: var(--row-hover);
transform: scale(0.97);
}
.nav-btn:active {
background: var(--nav-bg2);
transform: scale(0.97);
opacity: 0.9;
}
.ch-icon-btn:active,
.ch-remove-btn:active,
.ch-share-btn:active {
opacity: 1;
transform: scale(0.92);
}
.ch-gear-btn:active,
.panel-close-btn:active,
.mc-jump-btn:active {
background: var(--row-hover);
transform: scale(0.95);
}
/* Hover→tap conversion. Hover-only feedback (e.g., tooltip reveal) is gated
behind @media (hover: hover) so touch devices don't get stuck in a hover
state after a tap. The same content is exposed on tap via :focus-within
below. */
@media (hover: hover) {
.sort-help:hover .sort-help-tip { display: block; }
}
/* Tap-to-reveal tooltip: .sort-help becomes keyboard/tap focusable (set
tabindex="0" in markup). On focus or focus-within, the tip is shown so a
tap on touch devices reveals it without requiring hover. */
.sort-help { outline: none; }
.sort-help:focus,
.sort-help:focus-within {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}
.sort-help:focus .sort-help-tip,
.sort-help:focus-within .sort-help-tip { display: block; }
/* === Nav === */
.top-nav {
display: flex; align-items: center; justify-content: space-between;
background: linear-gradient(135deg, var(--nav-bg) 0%, var(--nav-bg2) 100%); color: var(--nav-text); padding: 0 20px; height: 52px;
position: sticky; top: 0; z-index: 1100;
box-shadow: 0 2px 8px rgba(0,0,0,.3);
flex-wrap: nowrap; overflow: hidden; min-width: 0;
}
.nav-left { display: flex; align-items: center; gap: 24px; min-width: 0; flex-shrink: 1; overflow: hidden; }
.nav-brand { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--nav-text); font-weight: 700; font-size: 16px; }
.brand-icon { font-size: 20px; }
.live-dot {
width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted);
display: inline-block; margin-left: 4px; transition: background .3s;
}
@keyframes pulse-ring {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.6); }
70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
.live-dot.connected { background: var(--status-green); animation: pulse-ring 2s ease-out infinite; }
.nav-links { display: flex; align-items: center; gap: 4px; }
.nav-link {
color: var(--nav-text-muted); text-decoration: none; padding: 14px 12px; font-size: 14px;
border-bottom: 2px solid transparent; transition: all .15s;
background: none; border-top: none; border-left: none; border-right: none;
cursor: pointer; font-family: var(--font);
white-space: nowrap; /* #1046: never wrap labels — wrapping makes the nav bar grow taller */
}
.nav-link:hover { color: var(--nav-text); }
.nav-link.active {
color: var(--nav-text);
border-bottom-color: transparent;
background: var(--nav-active-bg);
border-radius: 6px;
margin: 4px 0;
padding: 10px 12px;
}
.nav-dropdown { position: relative; }
.dropdown-menu {
display: none; position: absolute; top: 100%; left: 0;
background: var(--nav-bg2); border: 1px solid var(--border); border-radius: 6px;
min-width: 140px; padding: 4px 0; box-shadow: 0 8px 24px rgba(0,0,0,.4);
}
.nav-dropdown:hover .dropdown-menu { display: block; }
.dropdown-item {
display: block; padding: 8px 16px; color: var(--text-muted); text-decoration: none; font-size: 13px;
}
.dropdown-item:hover { background: var(--accent); color: #fff; }
.nav-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.nav-btn {
background: none; border: 1px solid var(--border); color: var(--nav-text-muted); padding: 6px 12px;
border-radius: 6px; cursor: pointer; font-size: 14px; transition: all .15s;
min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
}
.nav-btn:hover { background: var(--nav-bg2); color: var(--nav-text); }
/* === Nav Stats === */
.nav-stats {
display: flex; gap: 12px; align-items: center; font-size: 12px; color: var(--nav-text-muted);
font-family: var(--mono); margin-right: 4px; white-space: nowrap;
}
.nav-stats .stat-val { color: var(--nav-text); font-weight: 600; transition: color 0.3s ease; }
.nav-stats .stat-val.updated { color: var(--accent); }
.nav-stats .engine-badge {
font-size: 10px; font-weight: 600; font-family: var(--mono);
color: var(--nav-text-muted); background: rgba(255,255,255,0.1);
padding: 1px 5px; border-radius: var(--badge-radius);
letter-spacing: 0.5px; text-transform: lowercase;
}
.nav-stats .version-badge {
font-size: 10px; font-weight: 600; font-family: var(--mono);
color: var(--nav-text-muted); background: rgba(255,255,255,0.1);
padding: 1px 5px; border-radius: var(--badge-radius);
letter-spacing: 0.5px;
}
.nav-stats .version-badge a {
color: var(--nav-text-muted); text-decoration: none;
}
.nav-stats .version-badge a:hover {
color: var(--nav-text); text-decoration: underline;
}
/* === Layout === */
/* Default: body-scroll mode — content pushes beyond viewport, iOS status-bar
tap-to-scroll works because <body> is the scroll container. Pages that need
a fixed-height container (maps, virtual-scroll, split-panels) add
.app-fixed via the router so their children can use height:100%. */
#app { min-height: calc(100vh - 52px); min-height: calc(100dvh - 52px); }
#app.app-fixed { height: calc(100vh - 52px); height: calc(100dvh - 52px); min-height: 0; overflow: hidden; }
.split-layout {
display: flex; height: 100%; overflow: hidden;
}
.panel-left {
flex: 1; min-width: 0; overflow: auto; padding: 8px 12px;
}
.panel-right {
width: 420px; min-width: 280px; max-width: 70vw; border-left: 1px solid var(--border);
background: var(--detail-bg); overflow-y: auto; padding: 16px;
position: relative;
}
.panel-resize-handle {
position: absolute; top: 0; left: -3px; width: 6px; height: 100%;
cursor: col-resize; z-index: 10; background: transparent;
}
.panel-resize-handle:hover, .panel-resize-handle.dragging {
background: var(--accent); opacity: 0.3;
}
.panel-right.empty {
display: flex; align-items: center; justify-content: center;
color: var(--text-muted); font-size: 14px;
}
.split-layout.detail-collapsed .panel-right { display: none; }
.panel-close-btn {
position: absolute; top: 8px; right: 12px; z-index: 11;
background: none; border: none; font-size: 20px; line-height: 1;
color: var(--text-muted); cursor: pointer; padding: 4px 6px;
border-radius: 4px; transition: color 0.15s, background 0.15s;
}
.panel-close-btn:hover { color: var(--text); background: var(--surface-1); }
.panel-right.empty .panel-close-btn { display: none; }
/* === Page Header === */
.page-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 12px;
}
.page-header h2 { font-size: 18px; font-weight: 700; }
.page-header .count { color: var(--text-muted); font-weight: 400; font-size: 14px; margin-left: 8px; }
/* === Filter Bar === */
.filter-bar {
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; align-items: center;
}
.filter-bar input, .filter-bar select {
padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px;
font-size: 13px; background: var(--input-bg); color: var(--text); font-family: var(--font);
height: 34px; box-sizing: border-box; line-height: 1;
}
.filter-bar input { width: 120px; }
.filter-bar select { min-width: 90px; }
.filter-bar .btn {
padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px;
background: var(--input-bg); cursor: pointer; font-size: 13px; transition: all .15s;
font-family: var(--font); color: var(--text); height: 34px; box-sizing: border-box; line-height: 1;
}
.filter-group { display: flex; gap: 6px; align-items: center; }
.filter-group .btn { padding: 4px 10px; font-size: 12px; border-radius: 12px; border: 1px solid var(--border); background: var(--input-bg); color: var(--text); cursor: pointer; transition: background 0.15s, color 0.15s; }
.filter-group .btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.filter-group .btn:hover:not(.active) { background: var(--surface-2); }
.filter-group + .filter-group { border-left: 1px solid var(--border); padding-left: 12px; margin-left: 6px; }
.sort-help { cursor: help; font-size: 14px; color: var(--text-muted, #888); position: relative; display: inline-block; }
.sort-help-tip {
display: none; position: absolute; top: 130%; left: 50%; transform: translateX(-50%);
background: var(--card-bg, #222); color: var(--text, #eee); border: 1px solid var(--border);
border-radius: 6px; padding: 8px 12px; font-size: 12px; line-height: 1.5;
white-space: pre-line; width: 260px; z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,.3); pointer-events: none;
}
.sort-help:hover .sort-help-tip { display: block; }
.filter-bar .btn:hover { background: var(--row-hover); }
.filter-bar .btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-icon {
background: none; border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 6px 10px; cursor: pointer; font-size: 14px; transition: all .15s;
}
.btn-icon:hover { background: var(--row-hover); }
/* === Tables === */
.data-table {
width: 100%; border-collapse: collapse; font-size: 12px;
}
.data-table th {
text-align: left; padding: 4px 6px; font-weight: 600; font-size: 11px;
text-transform: uppercase; letter-spacing: .3px; color: var(--text-muted);
border-bottom: 2px solid var(--border); background: var(--card-bg);
position: sticky; top: 0; z-index: 1;
}
.data-table th.sortable { cursor: pointer; }
.data-table th.sortable:hover { color: var(--accent); }
.data-table td {
padding: 3px 6px; border-bottom: 1px solid var(--border);
vertical-align: middle; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
max-width: 0; /* forces td to respect table width instead of expanding to content */
}
.data-table td.col-details { white-space: normal; word-break: break-word; }
.data-table td:has(.spark-bar), .data-table td.col-spark { max-width: none; overflow: visible; min-width: 80px; }
.data-table .col-time { min-width: 108px; white-space: nowrap; }
.timestamp-future-icon { margin-left: 4px; cursor: help; }
.data-table tbody tr:nth-child(even) { background: var(--row-stripe); }
.data-table tbody tr:hover { background: var(--row-hover); cursor: pointer; }
.data-table tbody tr.selected { background: var(--selected-bg); }
/* === Badges === */
.badge {
display: inline-block; padding: 2px 8px; border-radius: var(--badge-radius);
font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .3px;
}
.badge-region {
display: inline-block; padding: 2px 6px; border-radius: 4px;
font-size: 10px; font-weight: 700; font-family: var(--mono);
background: var(--nav-bg); color: var(--nav-text); letter-spacing: .5px;
}
/* TODO: expose --transport-badge-bg/fg in customizer THEME_CSS_MAP (tracked in future milestone) */
.badge-transport {
display: inline-block; padding: 1px 5px; border-radius: 4px;
font-size: 9px; font-weight: 700; font-family: var(--mono);
background: var(--transport-badge-bg, #f59e0b20); color: var(--transport-badge-fg, #d97706);
letter-spacing: .5px; vertical-align: middle;
}
.badge-obs {
display: inline-block; padding: 1px 6px; border-radius: 10px;
font-size: 10px; font-weight: 600;
background: #ede9fe; color: #6d28d9;
}
/* === Monospace === */
.mono { font-family: var(--mono); font-size: 12px; }
/* === Detail Panel === */
.detail-title {
font-size: 15px; font-weight: 700; margin-bottom: 12px;
padding-bottom: 8px; border-bottom: 2px solid var(--border);
}
.detail-hash {
font-family: var(--mono); font-size: 12px; color: var(--text-muted);
margin-bottom: 12px; word-break: break-all;
}
.detail-meta {
display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px;
margin-bottom: 16px; font-size: 13px;
}
.detail-meta dt { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: .3px; }
.detail-meta dd { font-weight: 500; margin-bottom: 4px; }
.observation-current { background: var(--accent-bg, rgba(0,122,255,0.1)); font-weight: 600; }
.detail-obs-row:hover { background: var(--hover-bg, rgba(255,255,255,0.05)); }
.detail-obs-table th { font-size: 0.8em; text-transform: uppercase; color: var(--text-muted); }
/* === Hex Dump === */
.hex-dump {
font-family: var(--mono); font-size: 11px; line-height: 1.8;
padding: 12px; border-radius: 8px; background: #1e1e2e; color: #cdd6f4;
overflow-x: auto; margin-bottom: 16px; word-break: break-all;
}
.hex-byte { padding: 1px 2px; border-radius: 2px; }
.hex-header { background: #f38ba8; color: #1e1e2e; }
.hex-pathlen { background: #fab387; color: #1e1e2e; }
.hex-transport { background: #89b4fa; color: #1e1e2e; }
.hex-path { background: #a6e3a1; color: #1e1e2e; }
.hex-payload { background: #f9e2af; color: #1e1e2e; }
.hex-pubkey { background: #f9e2af; color: #1e1e2e; }
.hex-timestamp { background: #fab387; color: #1e1e2e; }
.hex-signature { background: #f38ba8; color: #1e1e2e; }
.hex-flags { background: #94e2d5; color: #1e1e2e; }
.hex-lat, .hex-lon, .hex-location { background: #89b4fa; color: #1e1e2e; }
.hex-name { background: #cba6f7; color: #1e1e2e; }
.hex-legend {
display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; font-size: 11px;
}
.hex-legend span {
display: inline-flex; align-items: center; gap: 4px;
}
.hex-legend .swatch {
display: inline-block; width: 12px; height: 12px; border-radius: 2px;
}
/* === Field Breakdown Table === */
.field-table {
width: 100%; border-collapse: collapse; font-size: 11px; margin-bottom: 12px;
}
.field-table th {
text-align: left; padding: 6px 8px; background: var(--nav-bg); color: var(--nav-text);
font-size: 11px; text-transform: uppercase; letter-spacing: .3px;
}
.field-table td {
padding: 5px 8px; border-bottom: 1px solid var(--border);
}
.field-table .section-row td {
background: var(--section-bg, #eef2ff); font-weight: 700; font-size: 11px;
text-transform: uppercase; letter-spacing: .5px; color: var(--accent);
}
.field-table .section-header td { background: rgba(243,139,168,0.18); }
.field-table .section-transport td { background: rgba(137,180,250,0.18); }
.field-table .section-path td { background: rgba(166,227,161,0.18); }
.field-table .section-payload td { background: rgba(249,226,175,0.18); }
/* === Path display === */
.path-hops {
display: inline-flex; align-items: center; gap: 2px; font-family: var(--mono); font-size: 11px;
}
.path-hops .hop { color: var(--accent); }
.path-hops .hop-named { color: #fff; background: var(--accent); padding: 1px 6px; border-radius: 3px; font-family: var(--font); font-weight: 600; cursor: default; }
.path-hops .arrow { color: var(--text-muted); }
/* === Modal === */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 200;
display: flex; align-items: center; justify-content: center;
}
.modal {
background: var(--card-bg); border-radius: 12px; padding: 24px; width: 500px;
max-width: 90vw; max-height: 80vh; overflow-y: auto;
box-shadow: 0 16px 48px rgba(0,0,0,.2);
}
.modal h3 { margin-bottom: 12px; }
.modal textarea {
width: 100%; height: 120px; font-family: var(--mono); font-size: 13px;
padding: 10px; border: 1px solid var(--border); border-radius: 6px;
resize: vertical; margin-bottom: 12px;
}
.modal .btn-primary {
background: var(--accent); color: #fff; border: none; padding: 8px 20px;
border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;
}
.modal .btn-primary:hover { background: var(--accent-hover); }
.modal .btn-close {
background: none; border: 1px solid var(--border); padding: 8px 16px;
border-radius: 6px; cursor: pointer; margin-left: 8px; font-size: 14px;
}
/* === Map Controls Panel === */
.map-controls {
position: absolute; top: 12px; right: 12px; z-index: 1000;
background: var(--card-bg); border-radius: 10px; padding: 14px 16px;
box-shadow: 0 4px 16px rgba(0,0,0,.15); width: 220px; font-size: 13px;
max-height: calc(100% - 24px); overflow-y: auto;
}
.map-controls h3 { font-size: 15px; margin-bottom: 10px; }
.mc-section { margin-bottom: 10px; }
.mc-label { font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .3px; color: var(--text-muted); margin-bottom: 4px; }
.mc-section label { display: block; padding: 2px 0; cursor: pointer; }
.mc-section select { width: 100%; padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; }
.mc-jumps { display: flex; flex-wrap: wrap; gap: 4px; }
.mc-jump-btn {
padding: 3px 10px; border: 1px solid var(--border); border-radius: 4px;
background: var(--input-bg); cursor: pointer; font-size: 12px; font-weight: 600;
}
.mc-jump-btn:hover { background: var(--row-hover); }
fieldset.mc-section { border: none; padding: 0; margin: 0 0 10px 0; min-width: 0; }
fieldset.mc-section legend.mc-label { padding: 0; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
/* === Misc === */
.text-muted { color: var(--text-muted); }
.text-center { text-align: center; }
.mt-8 { margin-top: 8px; }
.mb-8 { margin-bottom: 8px; }
.flex-between { display: flex; align-items: center; justify-content: space-between; }
/* === Channels Page === */
.ch-layout { display: flex; height: 100%; overflow: hidden; }
.ch-sidebar {
width: 280px; min-width: 280px; background: var(--card-bg);
border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden;
}
.ch-sidebar-header {
padding: 14px 16px; border-bottom: 1px solid var(--border);
}
.ch-sidebar-title {
display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 700; margin-bottom: 8px;
}
.ch-encrypted-toggle {
display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-muted);
cursor: pointer; user-select: none; margin-bottom: 4px;
}
.ch-encrypted-toggle input { margin: 0; cursor: pointer; }
.ch-toggle-label { white-space: nowrap; }
.ch-item.ch-encrypted { opacity: 0.55; }
.ch-item.ch-encrypted .ch-item-name { font-style: italic; }
.ch-icon { font-size: 20px; }
.ch-sidebar-controls { display: flex; align-items: center; gap: 6px; }
.ch-region-select {
flex: 1; padding: 5px 8px; border: 1px solid var(--border); border-radius: 6px;
font-size: 12px; background: var(--input-bg); color: var(--text); font-family: var(--font);
}
.ch-gear-btn {
background: none; border: 1px solid var(--border); border-radius: 6px;
padding: 4px 8px; cursor: pointer; font-size: 14px;
}
.ch-gear-btn:hover { background: var(--row-hover); }
.ch-channel-list { flex: 1; overflow-y: auto; }
button.ch-item {
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
cursor: pointer; transition: background .12s; border: none; border-bottom: 1px solid var(--border);
background: transparent; width: 100%; text-align: left; color: var(--text);
font-family: inherit; font-size: inherit; line-height: inherit;
-webkit-tap-highlight-color: rgba(0,0,0,.08);
touch-action: manipulation;
}
button.ch-item:hover { background: var(--row-hover); }
button.ch-item:active { background: var(--selected-bg); }
button.ch-item.selected { background: var(--selected-bg); }
.ch-badge {
width: 40px; height: 40px; min-width: 40px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 13px; letter-spacing: .3px;
}
.ch-item-body { flex: 1; min-width: 0; }
.ch-item-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; }
.ch-item-name { font-weight: 600; font-size: 14px; }
.ch-item-time { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
.ch-unread-badge {
display: inline-block;
min-width: 18px;
padding: 1px 6px;
margin-left: 4px;
background: var(--accent, #3b82f6);
color: #fff;
font-size: 10px;
font-weight: 600;
border-radius: 9px;
text-align: center;
line-height: 1.4;
}
/* Shared icon button base for sidebar row controls (remove ✕, share ⤴).
WCAG 2.5.5 / Apple HIG: 44x44 CSS px minimum touch target. */
.ch-icon-btn {
display: inline-flex; align-items: center; justify-content: center;
background: none; border: none; color: var(--text-muted);
cursor: pointer; padding: 4px; margin-left: 2px;
min-width: 44px; min-height: 44px; box-sizing: border-box;
opacity: 0.55; transition: opacity 0.15s, color 0.15s;
line-height: 1; user-select: none;
}
.ch-remove-btn {
font-size: 14px;
background: var(--statusRed, #b54a4a);
color: white;
border-radius: 4px;
padding: 4px 8px;
font-weight: bold;
opacity: 0.9;
}
.ch-share-btn { font-size: 13px; padding: 4px 8px; }
button.ch-item:hover .ch-icon-btn { opacity: 1; }
.ch-icon-btn:hover, .ch-icon-btn:focus { opacity: 1 !important; outline: none; }
.ch-remove-btn:hover, .ch-remove-btn:focus { background: #8b3838; color: white; }
.ch-share-btn:hover, .ch-share-btn:focus { color: var(--accent, #3b82f6); }
.ch-user-badge { font-size: 12px; margin-left: 2px; opacity: 0.85; flex-shrink: 0; }
.ch-item-preview { font-size: 12px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ch-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; position: relative; }
/* Sidebar resize handle (#89) */
.ch-sidebar-resize {
position: absolute; top: 0; right: -3px; width: 6px; height: 100%;
cursor: col-resize; z-index: 10; background: transparent;
}
.ch-sidebar-resize:hover { background: var(--accent); opacity: 0.3; }
.ch-sidebar { position: relative; }
.ch-main-header {
padding: 14px 20px; font-size: 16px; font-weight: 700;
border-bottom: 1px solid var(--border); background: var(--card-bg);
display: flex; align-items: center; gap: 8px;
}
.ch-back-btn {
display: none; background: none; border: none; font-size: 22px; cursor: pointer;
color: var(--text); padding: 8px 12px; border-radius: 4px;
align-items: center; justify-content: center;
min-width: 44px; min-height: 44px;
-webkit-tap-highlight-color: rgba(0,0,0,.08);
touch-action: manipulation;
}
.ch-back-btn:hover { background: var(--row-hover); }
.ch-header-text { flex: 1; }
.ch-messages {
flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 16px;
background: var(--content-bg);
}
.ch-empty, .ch-loading { color: var(--text-muted); text-align: center; padding: 40px; font-size: 14px; margin: auto; }
.ch-msg { display: flex; gap: 12px; }
.ch-avatar {
width: 36px; height: 36px; min-width: 36px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 15px; margin-top: 2px;
}
.ch-msg-content { flex: 1; min-width: 0; }
.ch-msg-sender { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
.ch-msg-bubble {
background: var(--surface-1); color: var(--text); padding: 10px 14px; border-radius: 4px 12px 12px 12px;
font-size: 14px; line-height: 1.5; word-break: break-word; display: inline-block; max-width: 100%;
border: 1px solid var(--border);
}
.ch-mention { color: var(--accent); font-weight: 600; }
.ch-encrypted-text { font-size: 11px; color: var(--text-muted); }
.ch-msg-meta { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.ch-analyze-link { color: var(--accent); text-decoration: none; margin-left: 8px; }
.ch-analyze-link:hover { text-decoration: underline; }
.ch-scroll-btn {
position: absolute; bottom: 16px; right: 24px; background: var(--accent); color: #fff;
border: none; border-radius: 20px; padding: 8px 16px; font-size: 12px; font-weight: 600;
cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,.2); z-index: 10;
}
.ch-scroll-btn:hover { background: var(--accent-hover); }
.ch-scroll-btn.hidden { display: none; }
/* Clickable sender names */
.ch-sender-link { cursor: pointer; text-decoration: none; }
.ch-sender-link:hover { text-decoration: underline; }
.ch-avatar { cursor: pointer; }
/* Node tooltip (hover) */
.ch-node-tooltip {
position: fixed; z-index: 1000; background: var(--card-bg); border: 1px solid var(--border);
border-radius: 8px; padding: 10px 14px; box-shadow: 0 4px 16px rgba(0,0,0,.15);
min-width: 180px; max-width: 260px;
}
.ch-tooltip-name { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.ch-tooltip-role { font-size: 12px; color: var(--text-muted); margin-bottom: 2px; }
.ch-tooltip-meta { font-size: 11px; color: var(--text-muted); }
.ch-tooltip-key { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
/* Node detail panel (slide from right) */
.ch-node-panel {
position: absolute; top: 0; right: 0; bottom: 0; width: 320px; max-width: 80%;
background: var(--card-bg); border-left: 1px solid var(--border);
box-shadow: -4px 0 16px rgba(0,0,0,.1); z-index: 20;
transform: translateX(100%); transition: transform .2s ease;
overflow-y: auto; display: flex; flex-direction: column;
}
.ch-node-panel.open { transform: translateX(0); }
.ch-node-panel-header {
display: flex; justify-content: space-between; align-items: center;
padding: 14px 16px; border-bottom: 1px solid var(--border); position: sticky; top: 0;
background: var(--card-bg); z-index: 1;
}
.ch-node-close {
background: none; border: none; font-size: 18px; cursor: pointer; color: var(--text-muted);
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
border-radius: 4px;
}
.ch-node-close:hover { background: var(--row-hover); color: var(--text); }
.ch-node-panel-body { padding: 16px; flex: 1; }
.ch-node-field { font-size: 13px; margin-bottom: 8px; }
.ch-node-label { display: block; font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .3px; margin-bottom: 2px; }
.ch-node-adverts { margin-top: 12px; }
.ch-node-advert { font-size: 12px; color: var(--text-muted); padding: 4px 0; border-bottom: 1px solid var(--border); }
.ch-node-link { display: inline-block; margin-top: 12px; color: var(--accent); text-decoration: none; font-size: 13px; }
.ch-node-link:hover { text-decoration: underline; }
/* === Nodes Page === */
.nodes-page { display: flex; flex-direction: column; height: 100%; }
.nodes-page .split-layout { flex: 1; min-height: 0; }
.nodes-topbar {
padding: 12px 16px; display: flex; align-items: center; gap: 16px;
border-bottom: 1px solid var(--border); background: var(--card-bg); flex-shrink: 0;
}
.nodes-search {
flex: 1; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px;
font-size: 13px; background: var(--input-bg); color: var(--text); font-family: var(--font);
}
.nodes-counts { display: flex; gap: 8px; flex-shrink: 0; }
.node-count-pill {
display: inline-block; padding: 3px 10px; border-radius: 12px;
font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap;
}
.nodes-tabs-bar {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 12px; flex-wrap: wrap; gap: 8px;
}
.nodes-tabs { display: flex; gap: 2px; }
.node-tab {
padding: 7px 16px; border: none; background: none; cursor: pointer;
font-size: 13px; font-weight: 500; color: var(--text-muted); border-bottom: 2px solid transparent;
font-family: var(--font); transition: all .15s;
}
.node-tab:hover { color: var(--text); }
.node-tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.nodes-filters { display: flex; gap: 8px; align-items: center; }
.nodes-filters select {
padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px;
font-size: 13px; background: var(--input-bg); color: var(--text); font-family: var(--font);
}
/* Node Detail */
.node-detail { padding: 4px 0; }
.node-detail-name { font-size: 20px; font-weight: 700; margin: 12px 0 4px; }
.node-detail-role { margin-bottom: 12px; }
.node-detail-section {
background: var(--card-bg); border: 1px solid var(--border);
border-radius: 8px; padding: 12px; margin-bottom: 8px;
}
/* Bug 7 fix: neighbor table text inherits accent color — force readable text */
.node-detail-section .data-table td,
.node-full-card .data-table td {
color: var(--text);
}
.node-detail-section .data-table td a,
.node-full-card .data-table td a {
color: var(--accent);
}
.node-detail-section h4 {
font-size: 12px; text-transform: uppercase; letter-spacing: .5px;
color: var(--text-muted); margin-bottom: 8px; padding-bottom: 4px;
border-bottom: 1px solid var(--border);
}
.node-detail-key {
font-size: 11px; word-break: break-all; color: var(--text-muted);
background: var(--row-stripe); padding: 6px 8px; border-radius: 4px; margin-bottom: 8px;
}
.node-map-container { margin-bottom: 8px; }
.qr-placeholder { text-align: center; padding: 12px; }
.qr-box {
width: 120px; height: 120px; margin: 0 auto; border: 2px dashed var(--border);
display: flex; align-items: center; justify-content: center;
color: var(--text-muted); font-size: 13px; border-radius: 8px;
}
.btn-primary {
background: var(--accent); color: #fff; border: none; padding: 8px 20px;
border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;
transition: background .15s;
}
.btn-primary:hover { background: var(--accent-hover); }
/* Advert Timeline */
.advert-entry {
display: flex; align-items: flex-start; gap: 10px; padding: 8px 0;
border-bottom: 1px solid var(--border);
}
.advert-entry:last-child { border-bottom: none; }
.advert-dot {
width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; margin-top: 4px;
}
/* #829: explicit color so text stays readable when inherited color matches card-bg */
.advert-info { font-size: 12px; line-height: 1.5; color: var(--text); }
.advert-info a { color: var(--accent); }
/* === Traces Page === */
.traces-page { padding: 16px; max-width: var(--trace-max-width, 95vw); margin: 0 auto; }
.trace-search {
display: flex; gap: 8px; margin-bottom: 20px;
}
.trace-search input {
flex: 1; padding: 10px 14px; border: 1px solid var(--border); border-radius: 6px;
font-size: 14px; font-family: var(--mono); background: var(--input-bg); color: var(--text);
}
.trace-empty { text-align: center; padding: 40px; color: var(--text-muted); font-size: 14px; }
.trace-summary {
display: flex; gap: 16px; margin-bottom: 20px; flex-wrap: wrap;
}
.trace-stat {
background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px;
padding: 14px 20px; text-align: center; flex: 1; min-width: 120px;
}
.trace-stat-value { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
.trace-stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-muted); }
.trace-section {
background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px;
padding: 16px; margin-bottom: 16px;
}
.trace-section h3 { font-size: 14px; font-weight: 700; margin-bottom: 12px; }
/* Path Visualization */
.trace-path-viz {
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
padding: 12px; background: var(--nav-bg); border-radius: 6px; margin-bottom: 8px;
}
.trace-path-hop {
display: inline-block; padding: 4px 10px; background: var(--accent); color: #fff;
border-radius: 4px; font-family: var(--mono); font-size: 12px; font-weight: 600;
}
.trace-path-arrow { color: var(--text-muted); font-size: 16px; }
.trace-path-label { color: var(--text-muted); font-size: 12px; font-style: italic; }
.trace-path-info { font-size: 12px; color: var(--text-muted); }
/* Timeline */
.tl-header {
display: grid; grid-template-columns: 160px 1fr 70px 70px 70px;
gap: 8px; font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: .3px; color: var(--text-muted); padding-bottom: 6px;
border-bottom: 1px solid var(--border); margin-bottom: 4px;
}
.tl-row {
display: grid; grid-template-columns: 160px 1fr 70px 70px 70px;
gap: 8px; align-items: center; padding: 6px 0;
border-bottom: 1px solid var(--border);
}
.tl-observer { font-size: 12px; font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tl-bar-container {
position: relative; height: 16px; background: var(--border); border-radius: 8px;
}
.tl-marker {
position: absolute; top: 2px; width: 12px; height: 12px; border-radius: 50%;
background: var(--accent); transform: translateX(-50%);
box-shadow: 0 0 6px rgba(74, 158, 255, .5);
}
.tl-delta { font-size: 11px; color: var(--text-muted); text-align: right; }
.tl-snr { font-size: 12px; font-weight: 600; text-align: right; }
.tl-snr.good { color: var(--status-green); }
.tl-snr.ok { color: var(--status-yellow); }
.tl-snr.bad { color: var(--status-red); }
.tl-rssi { font-size: 12px; color: var(--text-muted); text-align: right; }
/* === Scrollbar === */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
/* === Observers Page === */
.observers-page { padding: 20px; max-width: 1200px; margin: 0 auto; }
.obs-summary { display: flex; gap: 20px; margin-bottom: 16px; flex-wrap: wrap; }
.obs-stat { display: flex; align-items: center; gap: 6px; font-size: 14px; color: var(--text-muted); }
.health-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.health-dot.health-green { background: var(--status-green); box-shadow: 0 0 6px #22c55e80; }
.health-dot.health-yellow { background: var(--status-yellow); box-shadow: 0 0 6px #eab30880; }
.health-dot.health-red { background: var(--status-red); box-shadow: 0 0 6px #ef444480; }
.obs-table td:first-child { white-space: nowrap; }
.obs-table td:nth-child(6) { max-width: none; overflow: visible; }
.col-observer { min-width: 70px; max-width: none; }
.spark-bar { position: relative; min-width: 60px; max-width: 100px; flex: 1; height: 18px; background: var(--border); border-radius: 4px; overflow: hidden; display: inline-block; vertical-align: middle; }
@media (max-width: 640px) { .spark-bar { max-width: 60px; } }
.spark-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-hover, #60a5fa)); border-radius: 4px; transition: width 0.3s; }
.spark-label { position: absolute; right: 4px; top: 0; line-height: 18px; font-size: 11px; color: var(--text); font-weight: 500; }
/* === Dark mode input overrides === */
[data-theme="dark"] .filter-bar input,
[data-theme="dark"] .filter-bar select,
[data-theme="dark"] .nodes-search,
[data-theme="dark"] .ch-region-select,
[data-theme="dark"] .nodes-filters select,
[data-theme="dark"] .trace-search input,
[data-theme="dark"] .mc-jump-btn,
[data-theme="dark"] .filter-bar .btn { background: var(--input-bg); color: var(--text); border-color: var(--border); }
[data-theme="dark"] .filter-bar .btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
[data-theme="dark"] .ch-item.selected,
[data-theme="dark"] .data-table tbody tr.selected { background: var(--selected-bg); }
[data-theme="dark"] .tl-bar-container { background: #334155; }
[data-theme="dark"] .spark-bar { background: #334155; }
[data-theme="dark"] .spark-label { color: #e2e8f0; }
[data-theme="dark"] .hex-dump { border: 1px solid var(--border); }
[data-theme="dark"] .mc-jump-btn { background: var(--surface-2); color: var(--text); }
/* === Search Overlay === */
.search-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 300;
display: flex; align-items: flex-start; justify-content: center; padding-top: 80px;
}
.search-overlay.hidden { display: none; }
.search-box {
background: var(--card-bg); border-radius: 12px; width: 560px; max-width: 90vw;
box-shadow: 0 16px 48px rgba(0,0,0,.3); overflow: hidden;
}
.search-box input {
width: 100%; padding: 16px 20px; border: none; font-size: 16px;
background: var(--card-bg); color: var(--text); font-family: var(--font);
outline: none; border-bottom: 1px solid var(--border);
}
.search-results { max-height: 400px; overflow-y: auto; }
.search-result-item {
padding: 10px 20px; cursor: pointer; font-size: 13px;
border-bottom: 1px solid var(--border); transition: background .1s;
}
.search-result-item:hover { background: var(--row-hover); }
.search-result-type { font-size: 10px; font-weight: 700; text-transform: uppercase; color: var(--accent); margin-right: 8px; }
.search-no-results { padding: 20px; text-align: center; color: var(--text-muted); font-size: 14px; }
/* === Loading States & Transitions (M16) === */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(90deg, var(--border) 25%, var(--row-hover) 50%, var(--border) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 4px;
}
.skeleton-line { height: 14px; margin-bottom: 10px; }
.skeleton-line.short { width: 60%; }
.skeleton-line.shorter { width: 40%; }
.skeleton-table-row { display: flex; gap: 12px; padding: 8px 10px; border-bottom: 1px solid var(--border); }
.skeleton-table-cell { height: 16px; flex: 1; }
.skeleton-table-cell.narrow { max-width: 80px; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.page-enter { animation: fadeIn 150ms ease-out; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; }
.empty-state-text { font-size: 15px; font-weight: 500; margin-bottom: 4px; }
.empty-state-hint { font-size: 13px; }
/* === M17 Visual Juice === */
@keyframes row-flash {
0% { background: rgba(74, 158, 255, 0.25); }
100% { background: transparent; }
}
.data-table tbody tr.new-row { animation: row-flash 800ms ease-out; }
.data-table th.sortable { cursor: pointer; user-select: none; }
.data-table th.sortable:hover { color: var(--accent); }
.data-table th.sort-active { color: var(--accent); }
.data-table th .sort-arrow { font-size: 10px; margin-left: 4px; opacity: 0.7; }
.data-table tbody tr { border-left: 3px solid transparent; transition: border-color 0.15s, background 0.15s; }
.data-table tbody tr:hover { border-left-color: var(--accent); }
.badge { transition: transform 0.15s ease; }
.badge:hover { transform: scale(1.05); }
@keyframes slideInRight { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.panel-right:not(.empty) { animation: slideInRight 200ms ease-out; }
/* === Hamburger (hidden on desktop) === */
.hamburger { display: none; }
/* "More" button (hidden on desktop) */
.nav-more-wrap { display: none; position: relative; }
.nav-more-btn { display: inline-flex; }
.nav-more-menu {
display: none; position: absolute; top: calc(var(--top-nav-h, 52px) - 4px); right: 0;
background: var(--nav-bg); border: 1px solid var(--border); border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); flex-direction: column;
min-width: 160px; padding: 4px 0; z-index: 1200;
}
.nav-more-menu.open { display: flex; }
.nav-more-menu .nav-link {
padding: 10px 16px; border-bottom: none; border-radius: 0; margin: 0;
white-space: nowrap;
}
.nav-more-menu .nav-link:hover { background: var(--nav-bg2); color: var(--nav-text); }
.nav-more-menu .nav-link.active { background: var(--nav-active-bg); }
/* Ensure nav stays above Leaflet map */
.nav-links.open { z-index: 1100; }
#map-wrap .leaflet-container { z-index: 1; }
/* === Responsive — Tablet (≤900px) === */
@media (max-width: 900px) {
.panel-right { width: 320px; min-width: 320px; }
.ch-sidebar { width: 220px; min-width: 220px; }
.nav-stats { display: none; }
.brand-text { font-size: 14px; }
.nav-link { padding: 14px 8px; font-size: 13px; }
.map-controls { width: 180px; font-size: 12px; }
}
/* === Responsive — Tablet/Mid Priority+ nav (7681279px) ===
At <1280px the full nav-links + nav-stats + nav-right buttons can't fit
on one row (they total ~1540px on the home page). Collapse non-priority
links into the "More ▾" menu so .nav-right stays inside the viewport. */
@media (min-width: 768px) and (max-width: 1279px) {
.nav-links { display: flex !important; flex-direction: row; gap: 2px; }
.nav-links a:not([data-priority="high"]) { display: none; }
.nav-more-wrap { display: flex; align-items: center; }
.hamburger { display: none; }
.nav-link { padding: 14px 8px; font-size: 13px; }
.nav-links a[data-priority="high"] { order: -1; }
.nav-link.active { background: var(--nav-active-bg); border-radius: 6px; margin: 4px 0; padding: 10px 8px; }
}
/* === Responsive — Hamburger nav (<768px) === */
@media (max-width: 767px) {
.hamburger { display: inline-flex; }
.nav-more-wrap { display: none !important; }
.nav-links {
display: none; position: absolute; top: 52px; left: 0; right: 0;
background: var(--nav-bg); flex-direction: column; padding: 8px 0;
box-shadow: 0 8px 24px rgba(0,0,0,.4); z-index: 1100;
max-height: calc(100dvh - 52px); overflow-y: auto;
}
.nav-links a:not([data-priority="high"]) { display: flex; }
.nav-links.open { display: flex; }
.nav-link { padding: 12px 20px; border-bottom: none; }
.nav-link.active { background: var(--nav-active-bg); border-radius: 0; margin: 0; padding: 12px 20px; }
.nav-left { gap: 12px; }
body.nav-open { overflow: hidden; }
}
/* === Responsive — Mobile (≤640px) === */
@media (max-width: 640px) {
.brand-text { display: none; }
.nav-right { gap: 4px; }
/* Layouts: stack instead of side-by-side */
.split-layout { flex-direction: column; overflow-y: auto; }
.panel-left { padding: 6px; flex: 1; min-height: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.panel-right { display: none; }
/* Channels: Discord-style full screen toggle */
.ch-layout { flex-direction: row; position: relative; }
.ch-sidebar {
width: 100%; min-width: 0; max-height: none; height: 100%;
border-right: none; position: absolute; top: 0; left: 0; bottom: 0;
z-index: 2; background: var(--card-bg);
}
.ch-main {
width: 100%; position: absolute; top: 0; left: 0; bottom: 0;
transform: translateX(100%); transition: transform .2s ease;
z-index: 3; background: var(--content-bg);
}
.ch-layout.ch-show-main .ch-main { transform: translateX(0); }
.ch-back-btn { display: flex; }
.ch-main-header { display: flex; align-items: center; gap: 8px; }
/* Tables: smaller text for mobile */
.data-table { font-size: 11px; min-width: 0; }
.data-table td { padding: 5px 4px; max-width: 100px; }
.data-table th { padding: 5px 4px; font-size: 10px; }
.data-table .col-time { min-width: 64px; }
.panel-left { overflow-x: auto; }
/* Filters: collapse on mobile */
.filter-bar { flex-direction: row; flex-wrap: wrap; gap: 4px; }
.filter-toggle-btn { display: inline-flex !important; }
.filter-bar > *:not(.filter-toggle-btn):not(.col-toggle-wrap) { display: none; }
/* Must match :not() specificity of the hide rule above, otherwise .filters-expanded loses
the specificity battle and filter children stay hidden (see issue #534). */
.filter-bar.filters-expanded > *:not(.filter-toggle-btn):not(.col-toggle-wrap) { display: inline-flex; }
.filter-bar.filters-expanded > .col-toggle-wrap { display: inline-block; }
.filter-bar.filters-expanded input { width: 100%; }
.filter-bar.filters-expanded select { width: 100%; }
.filter-group { flex-wrap: wrap; }
.filter-group + .filter-group { border-left: none; padding-left: 0; margin-left: 0; }
.filter-bar .btn { min-height: 36px; }
.node-filter-wrap { width: 100%; }
/* Nodes */
.nodes-topbar { flex-direction: column; gap: 8px; padding: 10px; }
.nodes-tabs-bar { flex-direction: column; }
.nodes-counts { flex-wrap: wrap; }
.node-count-pill { font-size: 11px; padding: 2px 8px; }
/* Traces */
.trace-summary { flex-direction: column; }
.tl-header, .tl-row { grid-template-columns: 100px 1fr 50px 50px 50px; gap: 4px; font-size: 11px; }
/* Search overlay: full width */
.search-box { width: 95vw; }
.search-overlay { padding-top: 60px; }
/* Map controls */
.map-controls { width: calc(100vw - 24px); right: 12px; top: 8px; max-height: 200px; font-size: 12px; padding: 10px 12px; }
#leaflet-map { z-index: 0; }
#map-wrap { z-index: 0; }
/* Detail: smaller text */
.detail-meta { grid-template-columns: 1fr; }
.hex-dump { font-size: 10px; }
/* Modal */
.modal { width: 95vw; padding: 16px; }
/* Page header */
.page-header { flex-direction: column; align-items: flex-start; gap: 8px; }
}
/* === Grouped Packet Rows === */
.group-header { cursor: pointer; font-weight: 600; }
.group-header td:first-child::before { content: '▶ '; font-size: 10px; color: var(--accent); transition: transform 0.15s; display: inline-block; }
.group-header.expanded td:first-child::before { content: '▼ '; }
.group-header:hover { background: var(--row-hover); }
.group-child { font-size: 12px; }
.group-child td { padding-left: 20px; }
.group-child.hidden { display: none; }
/* === Reduced Motion === */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* Favorite stars */
.fav-star {
background: none; border: none; cursor: pointer; font-size: 1.1rem;
color: var(--text-muted); padding: 2px 4px; line-height: 1;
transition: color .15s, transform .15s;
}
.fav-star:hover { transform: scale(1.2); }
.fav-star.on { color: var(--status-yellow); }
/* BYOP Decode Modal */
.byop-modal { max-width: 560px; }
.byop-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.byop-header h3 { margin: 0; }
.byop-x { font-size: 1.2rem; color: var(--text-muted); background: none; border: none; cursor: pointer; }
.byop-input {
width: 100%; min-height: 60px; max-height: 120px; resize: vertical;
font-family: var(--mono); font-size: .8rem; padding: 10px;
border: 1px solid var(--border); border-radius: 6px; background: var(--surface-1);
color: var(--text);
}
.byop-input:focus { border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: 1px; }
.byop-err { color: var(--status-red); font-size: .85rem; }
.byop-decoded { margin-top: 8px; }
.byop-section { margin-bottom: 14px; }
.byop-section-title {
font-size: .75rem; text-transform: uppercase; letter-spacing: .05em;
color: var(--accent); font-weight: 600; margin-bottom: 6px;
padding-bottom: 4px; border-bottom: 1px solid var(--border);
}
.byop-kv { display: flex; flex-direction: column; gap: 4px; }
.byop-row { display: flex; gap: 12px; font-size: .85rem; }
.byop-key { color: var(--text-muted); min-width: 110px; flex-shrink: 0; }
.byop-val { color: var(--text); word-break: break-all; }
.byop-path { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; font-family: var(--mono); font-size: .8rem; }
.byop-hex { font-size: .75rem; word-break: break-all; color: var(--text-muted); background: var(--surface-1); padding: 8px; border-radius: 4px; max-height: 80px; overflow: auto; }
.byop-pre { font-size: .75rem; margin: 0; white-space: pre-wrap; }
/* Favorites nav dropdown */
.nav-fav-wrap { position: relative; }
.nav-fav-dropdown {
display: none; position: absolute; top: 100%; right: 0; z-index: 1000;
min-width: 260px; max-height: 360px; overflow-y: auto;
background: var(--surface-1, var(--detail-bg)); border: 1px solid var(--border); border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,.15); margin-top: 6px;
}
.nav-fav-dropdown.open { display: block; }
.fav-dd-empty { padding: 20px; text-align: center; color: var(--text-muted); font-size: .85rem; }
.fav-dd-loading { padding: 16px; text-align: center; color: var(--text-muted); font-size: .85rem; }
.fav-dd-item {
display: flex; align-items: center; gap: 8px; padding: 10px 14px;
text-decoration: none; color: var(--text); transition: background .1s;
border-bottom: 1px solid var(--border);
}
.fav-dd-item:last-child { border-bottom: none; }
.fav-dd-item:hover { background: var(--surface-1); }
.fav-dd-status { font-size: .7rem; flex-shrink: 0; }
.fav-dd-name { flex: 1; font-size: .85rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fav-dd-meta { font-size: .75rem; color: var(--text-muted); flex-shrink: 0; }
.fav-dd-star { font-size: .9rem; }
/* Leaflet popup accessibility — ensure readable in both themes */
.leaflet-popup-content-wrapper {
background: var(--card-bg) !important; color: var(--text) !important;
border-radius: 8px !important; box-shadow: 0 4px 12px rgba(0,0,0,.2) !important;
}
.leaflet-popup-tip { background: var(--card-bg) !important; }
.leaflet-popup-content { color: var(--text) !important; font-size: 13px !important; }
/* Column resize handles */
.col-resize-handle {
position: absolute; top: 0; right: -4px; width: 9px; height: 100%;
cursor: col-resize; z-index: 5; background: transparent; border-radius: 1px;
}
.col-resize-handle::after {
content: ''; position: absolute; top: 4px; left: 3px; width: 3px; height: calc(100% - 8px);
background: var(--border); border-radius: 1px;
}
.col-resize-handle:hover::after, .col-resize-handle.active::after {
background: var(--accent); opacity: 0.6;
}
/* Encrypted channels de-emphasized */
button.ch-item.ch-item-encrypted { opacity: 0.5; }
button.ch-item.ch-item-encrypted:hover { opacity: 0.7; }
button.ch-item.ch-item-encrypted.selected { opacity: 0.8; }
button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
/* Channel key input (#725 M2) */
.ch-key-input {
flex: 1;
min-width: 0;
box-sizing: border-box;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 6px 0 0 6px;
background: var(--card-bg);
color: var(--text);
font-size: 12px;
font-family: inherit;
}
.ch-key-input:focus {
outline: 2px solid var(--accent, #3b82f6);
outline-offset: -1px;
border-color: var(--accent, #3b82f6);
}
.ch-key-input::placeholder { color: var(--text-muted); }
.ch-key-input-wrap { margin-bottom: 4px; }
.ch-wrong-key { color: var(--danger, #ef4444); font-weight: 500; }
/* Add channel form (#759) */
.ch-add-form { margin: 0; }
.ch-add-label { display: block; font-weight: 600; font-size: 13px; color: var(--text); margin-bottom: 4px; }
.ch-key-input, .ch-add-btn { height: 32px; box-sizing: border-box; }
.ch-add-row { display: flex; align-items: stretch; }
.ch-add-btn {
width: 32px; height: 32px; flex-shrink: 0;
border: 1px solid var(--accent, #3b82f6); border-left: none;
border-radius: 0 6px 6px 0;
background: var(--accent, #3b82f6); color: #fff;
font-size: 18px; font-weight: 700; line-height: 1;
cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.ch-add-btn:hover { opacity: 0.85; }
.ch-add-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; line-height: 1.3; }
.ch-add-status { font-size: 12px; margin-top: 4px; padding: 4px 6px; border-radius: 4px; }
.ch-analytics-link {
display: block;
padding: 6px 8px;
font-size: 12px;
text-decoration: none;
color: var(--text-muted);
border-bottom: 1px solid var(--border);
}
.ch-analytics-link:hover { color: var(--accent); }
.ch-add-status--loading { color: var(--text-muted); }
.ch-add-status--success { color: var(--success, #22c55e); }
.ch-add-status--warn { color: var(--warning, #eab308); }
.ch-add-status--error { color: var(--danger, #ef4444); }
/* Touch-friendly tappable elements */
.ch-tappable {
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0,0,0,.08);
user-select: none; -webkit-user-select: none;
}
.ch-msg-sender.ch-tappable {
display: inline-block;
padding: 4px 6px 4px 0;
margin: -4px 0 0 -0px;
min-height: 32px;
line-height: 24px;
border-radius: 4px;
}
.ch-msg-sender.ch-tappable:active { opacity: 0.6; }
@media (max-width: 640px) {
.ch-msg-sender.ch-tappable {
padding: 6px 10px 6px 0;
min-height: 36px;
font-size: 14px;
}
.ch-avatar.ch-tappable { min-width: 44px; min-height: 44px; width: 44px; height: 44px; }
}
/* Full-screen node detail */
.node-fullscreen { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.node-full-header {
display: flex; align-items: center; gap: 8px;
padding: 12px 16px; border-bottom: 1px solid var(--border);
background: var(--card-bg); min-height: 48px;
}
.node-back-btn { display: flex !important; }
.node-full-title { font-weight: 700; font-size: 17px; flex: 1; }
.node-full-body { flex: 1; overflow-y: auto; padding: 16px; }
.node-full-card {
background: var(--card-bg); border-radius: 8px; padding: 16px;
margin-bottom: 12px; border: 1px solid var(--border);
}
.node-full-card h4 { margin: 0 0 10px; font-size: 14px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; }
.node-activity-list { display: flex; flex-direction: column; gap: 6px; }
.node-activity-item {
display: flex; gap: 8px; font-size: 13px; padding: 6px 0;
border-bottom: 1px solid var(--border); align-items: baseline;
}
.node-activity-item:last-child { border-bottom: none; }
.node-activity-time { color: var(--text-muted); white-space: nowrap; min-width: 70px; font-size: 12px; }
/* Analytics page */
.analytics-page { padding: 16px 24px; max-width: 1600px; margin: 0 auto; }
.analytics-header { margin-bottom: 20px; }
.analytics-header h2 { margin: 0 0 4px; }
.analytics-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.analytics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 16px; margin-bottom: 16px; }
.analytics-grid .analytics-card { margin-bottom: 0; }
.analytics-full { grid-column: 1 / -1; }
.analytics-card h3 { margin: 0 0 8px; font-size: 15px; }
.analytics-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.analytics-table th { text-align: left; padding: 8px; border-bottom: 2px solid var(--border); font-size: 12px; text-transform: uppercase; color: var(--text-muted); }
.analytics-table th.sortable { cursor: pointer; user-select: none; }
.analytics-table th.sortable:hover { color: var(--accent); }
.analytics-table th.sort-active { color: var(--accent); }
.analytics-table th .sort-arrow { font-size: 10px; margin-left: 4px; opacity: 0.7; }
.analytics-table td { padding: 8px; border-bottom: 1px solid var(--border); }
.analytics-table .ch-section-row { background: var(--card-bg); }
.analytics-table .ch-section-row td.ch-section-header {
font-weight: 600;
font-size: 12px;
letter-spacing: 0.04em;
color: var(--text-muted);
text-transform: uppercase;
padding: 10px 8px 6px;
border-bottom: 1px solid var(--border);
background: var(--card-bg);
}
.analytics-table .ch-section-row:hover { background: var(--card-bg); cursor: default; }
.hash-bars { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.hash-bar-row { display: flex; align-items: center; gap: 12px; }
.hash-bar-label { min-width: 160px; font-size: 13px; }
.hash-bar-track { flex: 1; height: 24px; background: var(--border); border-radius: 4px; overflow: hidden; }
.hash-bar-fill { height: 100%; border-radius: 4px; transition: width .3s; }
.hash-cell.hash-active:hover { outline: 2px solid var(--accent); outline-offset: -2px; }
.hash-cell.hash-selected { outline: 2px solid var(--accent); outline-offset: -2px; box-shadow: 0 0 6px var(--accent); }
.hash-cell-empty { background: var(--card-bg); color: var(--text-muted); }
.hash-cell-taken { background: var(--status-green); color: #fff; }
.hash-cell-possible { background: var(--status-yellow); color: #fff; }
.hash-cell-collision { color: #fff; }
.hash-matrix-tooltip {
position: fixed; z-index: 9999; background: var(--surface-1); border: 1px solid var(--border);
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); padding: 8px 12px;
font-size: 12px; min-width: 160px; max-width: 260px; pointer-events: none;
}
.hash-matrix-tooltip-hex { font-family: var(--mono); font-size: 13px; font-weight: 700; margin-bottom: 4px; color: var(--accent); }
.hash-matrix-tooltip-status { color: var(--text-muted); font-size: 11px; }
.hash-matrix-tooltip-nodes { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; }
.hash-byte-selector { display: flex; gap: 4px; }
.hash-byte-btn { padding: 4px 12px; border-radius: 20px; border: 1px solid var(--border); background: var(--card-bg); color: var(--text-muted); font-size: 12px; font-weight: 600; cursor: pointer; transition: background .15s, color .15s; }
.hash-byte-btn:hover { background: var(--border); color: var(--text); }
.hash-byte-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.hash-bar-value { min-width: 120px; text-align: right; font-size: 13px; font-weight: 600; }
.badge-hash-1 { background: #ef444420; color: var(--status-red); }
.badge-hash-2 { background: #22c55e20; color: var(--status-green); }
.badge-success { background: #22c55e20; color: var(--status-green); }
.badge-danger { background: #ef444420; color: var(--status-red); }
.badge-hash-3 { background: #3b82f620; color: var(--accent); }
.timeline-legend { display: flex; gap: 16px; justify-content: center; margin-top: 8px; font-size: 12px; }
.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
.timeline-chart svg { display: block; }
@media (max-width: 640px) {
.hash-bar-label { min-width: 80px; }
.hash-bar-value { min-width: 80px; font-size: 12px; }
.hash-bar-label span { display: none; }
}
.clickable-row { cursor: pointer; }
.clickable-row:hover { background: var(--hover-bg, rgba(0,0,0,.04)); }
.analytics-link { color: var(--accent); text-decoration: none; font-weight: 600; }
.analytics-link:hover { text-decoration: underline; }
/* Analytics v2 */
.analytics-tabs { display: flex; gap: 4px; margin-top: 12px; flex-wrap: wrap; }
.tab-btn { padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); color: var(--text); cursor: pointer; font-size: 13px; transition: all .15s; }
.tab-btn:hover { background: var(--hover-bg, rgba(0,0,0,.04)); }
.tab-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 240px)); gap: 12px; margin-bottom: 16px; }
.stat-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px; text-align: center; }
.stat-value { font-size: 24px; font-weight: 700; color: var(--text); }
.stat-label { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.stat-detail { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.stat-spark { margin-top: 6px; display: flex; justify-content: center; }
.analytics-row { display: flex; gap: 16px; margin-bottom: 16px; }
.analytics-row .flex-1 { flex: 1; min-width: 0; }
.rf-stats { display: flex; gap: 16px; flex-wrap: wrap; padding-top: 8px; font-size: 13px; }
.payload-bars { display: flex; flex-direction: column; gap: 6px; }
.payload-bar-row { display: flex; align-items: center; gap: 8px; }
.payload-bar-label { min-width: 100px; font-size: 12px; display: flex; align-items: center; gap: 4px; }
.payload-bar-value { min-width: 100px; text-align: right; font-size: 12px; }
.repeater-list { display: flex; flex-direction: column; gap: 4px; }
.repeater-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
.repeater-name { min-width: 140px; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.repeater-bar { flex: 1; }
.repeater-count { min-width: 60px; text-align: right; font-size: 12px; font-weight: 600; }
.reach-rings { display: flex; flex-direction: column; gap: 6px; }
.reach-ring { display: flex; align-items: baseline; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); }
.reach-hop { min-width: 70px; font-weight: 700; font-size: 13px; }
.reach-nodes { flex: 1; font-size: 12px; line-height: 1.6; }
.reach-count { min-width: 70px; text-align: right; font-size: 12px; color: var(--text-muted); }
@media (max-width: 640px) {
.analytics-row { flex-direction: column; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.repeater-name { min-width: 80px; }
.reach-ring { flex-wrap: wrap; }
.analytics-page { padding: 12px; }
.analytics-grid { grid-template-columns: 1fr; }
}
.observer-selector { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
.node-qr { text-align: center; margin-top: 8px; }
.node-qr svg { max-width: 100px; border-radius: 4px; }
[data-theme="dark"] .node-qr svg rect[fill="#ffffff"] { fill: var(--card-bg); }
[data-theme="dark"] .node-qr svg rect[fill="#000000"] { fill: var(--text); }
.node-map-qr-wrap { position: relative; }
.node-map-qr-overlay { position: absolute; bottom: 8px; right: 8px; z-index: 400; background: rgba(255,255,255,0.5); border-radius: 4px; padding: 4px; line-height: 0; margin: 0; text-align: center; }
.node-map-qr-overlay svg { max-width: 56px !important; display: block; margin: 0; }
[data-theme="dark"] .node-map-qr-overlay { background: rgba(255,255,255,0.4); }
/* Replay on Live Map button in packet detail */
.detail-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.replay-live-btn {
padding: 5px 12px;
background: rgba(168, 85, 247, 0.15);
border: 1px solid rgba(168, 85, 247, 0.3);
color: #c084fc;
font-size: 0.78rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
.replay-live-btn:hover { background: rgba(168, 85, 247, 0.3); }
/* Node filter dropdown */
.node-filter-wrap { display: inline-block; }
.node-filter-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--surface-1, #1e293b);
border: 1px solid var(--border, rgba(255,255,255,0.1));
border-radius: 6px;
max-height: 200px;
overflow-y: auto;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.node-filter-dropdown.hidden { display: none; }
.node-filter-option {
padding: 6px 10px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.1s;
}
.node-filter-option:hover { background: var(--surface-2, rgba(255,255,255,0.08)); }
.node-filter-option.node-filter-active { background: var(--accent); color: #fff; }
/* Hide low-value columns on mobile */
@media (max-width: 640px) {
.col-region, .col-rpt, .col-size, .col-hashsize, .col-pubkey { display: none; }
}
/* Clickable hop links */
.hop-link {
color: var(--accent, #3b82f6);
text-decoration: none;
cursor: pointer;
transition: color 0.15s;
}
.hop-link:hover { color: var(--accent-hover, #60a5fa); text-decoration: underline; }
/* Detail map link */
.detail-map-link {
padding: 5px 12px;
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.25);
color: #fbbf24;
border-radius: 6px;
font-size: 0.78rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.15s;
}
.detail-map-link:hover { background: rgba(245, 158, 11, 0.25); }
.copy-link-btn {
padding: 5px 12px;
background: rgba(59, 130, 246, 0.12);
border: 1px solid rgba(59, 130, 246, 0.25);
color: var(--accent, #3b82f6);
border-radius: 6px;
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.copy-link-btn:hover { background: rgba(59, 130, 246, 0.25); }
/* Route tooltip on map */
.route-tooltip {
background: rgba(0,0,0,0.8) !important;
color: #fbbf24 !important;
border: 1px solid rgba(245,158,11,0.3) !important;
font-weight: 600;
font-size: 0.75rem;
}
/* Ambiguous hop indicator */
.hop-ambiguous { border-bottom: 1px dashed var(--status-yellow, #f59e0b); }
.hop-warn { font-size: 0.7em; margin-left: 2px; vertical-align: super; color: var(--status-yellow, #f59e0b); }
.hop-conflict-btn { background: var(--status-yellow, #f59e0b); color: #000; border: none; border-radius: 4px; font-size: 11px;
font-weight: 700; padding: 1px 5px; cursor: pointer; vertical-align: middle; margin-left: 3px; line-height: 1.2; }
.hop-conflict-btn:hover { background: var(--status-yellow, #d97706); filter: brightness(0.85); }
.hop-conflict-popover { position: absolute; z-index: 9999; background: var(--surface-1); border: 1px solid var(--border);
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); width: 260px; max-height: 300px; overflow-y: auto; }
.hop-conflict-header { padding: 10px 12px; font-size: 12px; font-weight: 700; border-bottom: 1px solid var(--border);
color: var(--text-muted); }
.hop-conflict-list { padding: 4px 0; }
.hop-conflict-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; text-decoration: none;
color: var(--text); font-size: 13px; border-bottom: 1px solid var(--border); }
.hop-conflict-item:last-child { border-bottom: none; }
.hop-conflict-item:hover { background: var(--hover-bg); }
.hop-conflict-name { font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hop-conflict-dist { font-size: 11px; color: var(--text-muted); font-family: var(--mono); white-space: nowrap; }
.hop-conflict-pk { font-size: 10px; color: var(--text-muted); font-family: var(--mono); }
.hop-unreliable { opacity: 0.85; }
.hop-unreliable-btn { background: none; border: none; color: var(--status-yellow, #f59e0b); font-size: 13px;
cursor: help; vertical-align: middle; margin-left: 2px; padding: 0 2px; line-height: 1; }
.hop-global-fallback { border-bottom: 1px dashed var(--status-red); }
.hop-current { font-weight: 700 !important; color: var(--accent) !important; }
/* Self-loop subpath rows */
.subpath-selfloop { opacity: 0.6; }
.subpath-selfloop td:first-child::after { content: ''; }
/* Hop prefix in subpath routes */
.hop-prefix { color: var(--text-muted); font-size: 0.8em; }
/* Subpath split layout */
.subpath-layout { display: flex; gap: 0; flex: 1; min-height: 0; overflow: auto; position: relative; }
.subpath-list { flex: 1; overflow-y: auto; padding: 16px; min-width: 0; }
.subpath-detail { width: 420px; min-width: 360px; max-width: 50vw; border-left: 1px solid var(--border, #e5e7eb); overflow-y: auto; padding: 16px; transition: width 0.2s; }
.subpath-detail.collapsed { width: 0; min-width: 0; padding: 0; overflow: hidden; border: none; }
.subpath-detail-inner h4 { margin: 0 0 4px; word-break: break-word; }
.subpath-meta { display: flex; flex-direction: column; gap: 2px; margin-bottom: 12px; color: var(--text-muted); font-size: 0.9em; }
.subpath-section { margin: 16px 0; }
.subpath-section h5 { margin: 0 0 6px; font-size: 0.9em; }
.subpath-selected { background: var(--accent, #3b82f6) !important; color: #fff; }
.subpath-selected .hop-prefix { color: rgba(255,255,255,0.6); }
tr[data-hops]:hover { background: rgba(59,130,246,0.1); }
/* Hour distribution chart */
.hour-chart { display: flex; align-items: flex-end; gap: 2px; height: 60px; }
.hour-bar { flex: 1; background: var(--accent, #3b82f6); border-radius: 2px 2px 0 0; min-width: 4px; }
.hour-labels { display: flex; justify-content: space-between; font-size: 0.7em; color: var(--text-muted); }
/* Parent paths */
.parent-path { padding: 3px 0; border-bottom: 1px solid var(--border, #e5e7eb); }
@media (max-width: 768px) {
.subpath-layout { flex-direction: column; height: auto; }
.subpath-detail { width: 100%; border-left: none; border-top: 1px solid var(--border, #e5e7eb); }
}
@media (max-width: 480px) {
.subpath-detail { min-width: 100%; width: 100%; max-width: 100%; }
.subpath-layout { flex-direction: column; }
}
/* Legend swatches */
.legend-swatch { display: inline-block; width: 12px; height: 12px; border: 1px solid var(--border); vertical-align: middle; }
/* Subpath jump nav */
.subpath-jump-nav { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; font-size: 0.9em; flex-wrap: wrap; }
.subpath-jump-nav span { color: var(--text-muted); }
.subpath-jump-nav a { padding: 4px 12px; border-radius: 4px; background: var(--accent, #3b82f6); color: #fff; text-decoration: none; font-size: 0.85em; }
.subpath-jump-nav a:hover { opacity: 0.8; }
/* Route patterns table breathing room */
.subpath-list .analytics-table td:nth-child(2) { white-space: normal; word-break: break-word; max-width: 50vw; }
.subpath-list .analytics-table { table-layout: auto; }
.subpath-list h4 { margin-top: 24px; }
/* #70 — BYOP textarea larger on mobile */
@media (max-width: 640px) {
.byop-input { min-height: 120px; }
}
/* #71 — Column visibility toggle */
.col-toggle-wrap { position: relative; display: inline-block; }
.col-toggle-btn { font-size: 13px; padding: 6px 10px; cursor: pointer; background: var(--input-bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text); height: 34px; box-sizing: border-box; line-height: 1; }
.col-toggle-menu { display: none; position: absolute; top: 100%; left: 0; z-index: 50; background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 0; min-width: 150px; box-shadow: 0 4px 12px rgba(0,0,0,.15); }
.col-toggle-menu.open { display: block; }
.col-toggle-menu label { display: flex; align-items: center; gap: 6px; padding: 4px 12px; font-size: .82rem; cursor: pointer; color: var(--text); }
.col-toggle-menu label input[type="checkbox"] { width: 14px; height: 14px; margin: 0; flex-shrink: 0; }
.col-toggle-menu label:hover { background: var(--row-hover); }
/* Column hide classes */
.hide-col-region .col-region,
.hide-col-time .col-time,
.hide-col-hash .col-hash,
.hide-col-size .col-size,
.hide-col-type .col-type,
.hide-col-observer .col-observer,
.hide-col-path .col-path,
.hide-col-rpt .col-rpt,
.hide-col-hashsize .col-hashsize,
.hide-col-details .col-details { display: none; }
/* === Home page fixes === */
/* #25 — Widen home page content cap from 720px to 1200px */
.home-stats,
.home-health,
.home-journey,
.home-checklist,
.home-footer,
.home-favorites { max-width: 1200px; }
/* #40 — Increase suggest-claim touch target to ≥44px */
.suggest-claim { min-height: 44px; min-width: 44px; padding: 10px 14px; display: inline-flex; align-items: center; justify-content: center; }
/* #41 — Lower My Nodes grid minimum to prevent overflow on 375-640px */
.my-nodes-grid { max-width: 1200px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
/* #42 — Stats cards: use grid with max-width per card on wide screens */
.home-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 200px)); justify-content: center; }
/* #44 — Namespaced home sparkline classes (avoid collision with observers .spark-bar) */
.home-spark-label { font-size: .65rem; color: var(--text-muted); margin-bottom: 4px; }
.home-spark-bars { display: flex; align-items: flex-end; gap: 2px; height: 28px; }
.home-spark-bar { flex: 1; background: var(--accent); border-radius: 1px; min-width: 0; }
/* === Bug fixes: #17 #20 #21 #69 === */
/* #17 — Hash matrix mobile overflow */
.hash-matrix-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
@media (max-width: 640px) {
.hash-matrix-table td { width: 24px !important; height: 24px !important; font-size: 0.7em !important; }
.hash-matrix-table td .hash-cell { padding: 0; }
}
/* #20 — Observers table horizontal scroll on mobile */
.obs-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.obs-table-scroll .obs-table { min-width: 720px; }
/* #206 — Analytics/Compare tables scroll wrappers on mobile */
.analytics-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.analytics-table-scroll .analytics-table,
.analytics-table-scroll .analytics-peer-table,
.analytics-table-scroll .compare-table { min-width: 480px; }
@media (max-width: 640px) {
.spark-bar { min-width: 60px; width: auto; }
}
/* #21 — Chat message bubble max-width */
.ch-msg-bubble { max-width: 720px; }
/* #69 — Touch-friendly resize handle */
@media (pointer: coarse) {
.panel-resize-handle { width: 12px !important; }
}
/* #21 — max-width applied via .ch-msg-bubble rule above */
/* === Bug fixes: #16 collapsible controls, #53 detail map height === */
.map-controls-toggle {
position: absolute;
top: 10px;
right: 10px;
z-index: 1001;
width: 36px;
height: 36px;
border-radius: 6px;
border: 1px solid var(--border, #333);
background: var(--card-bg, #1e1e1e);
color: var(--text, #fff);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
.map-controls.collapsed {
display: none;
}
.node-detail-map {
height: 280px;
min-height: 200px;
}
.node-top-row { display: flex; gap: 16px; margin-bottom: 12px; }
.node-top-row .node-map-wrap { flex: 3; min-height: 200px; border-radius: 8px; overflow: hidden; }
.node-top-row .node-map-wrap .node-detail-map { height: 100%; }
.node-top-row .node-qr-wrap { flex: 1; min-width: 120px; max-width: 160px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
.node-qr-wrap--full { max-width: 240px; margin: 0 auto; }
.node-stats-table { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 12px; }
.node-stats-table td { padding: 6px 12px; border-bottom: 1px solid var(--border); }
.node-stats-table tr:last-child td { border-bottom: none; }
.node-stats-table tr:nth-child(even) { background: var(--row-stripe); }
.node-stats-table td:first-child { font-weight: 600; color: var(--text-muted); width: 40%; white-space: nowrap; }
.node-stats-table td:last-child { font-weight: 500; }
@media (max-width: 768px) {
.node-top-row { flex-direction: column; }
.node-top-row .node-qr-wrap { min-height: auto; }
}
@media (max-width: 640px) {
.node-detail-map {
height: 200px;
min-height: 160px;
}
}
.detail-back-btn {
background: none;
border: 1px solid var(--border, #333);
color: var(--text, #fff);
padding: 4px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
.meshcore-marker { background: none !important; border: none !important; }
.marker-stale { opacity: 0.7; filter: grayscale(90%) brightness(0.8); }
.last-seen-active { color: var(--status-green); }
.last-seen-stale { color: var(--text-muted); }
/* === Node Analytics === */
.analytics-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
.analytics-stat-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; text-align: center; }
.analytics-stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-muted); margin-bottom: 2px; }
.analytics-stat-value { font-size: 20px; font-weight: 700; }
.analytics-stat-desc { font-size: 10px; color: var(--text-muted); margin-top: 2px; font-style: italic; }
.analytics-charts {
/* #1058 — fluid + auto-stacking layout. The grid sizes from its own
available width (NOT the viewport), so a narrow side-pane on a wide
screen still stacks. `auto-fit` collapses empty tracks; `minmax()`
guarantees a minimum readable column width before wrapping. The
`container-type: inline-size` opts in to container queries so
descendants (or future tweaks) can size against this element's
own width rather than the viewport. Uses #1054 spacing tokens. */
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
gap: var(--space-sm);
margin-bottom: var(--space-md);
}
.analytics-chart-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; padding: var(--space-sm); min-width: 0; }
.analytics-chart-card.full { grid-column: 1 / -1; }
/* Constrain chart media inside the card (svg/canvas at any depth). The
`.analytics-chart-card svg, .analytics-chart-card canvas` descendant
selector is robust to wrapper elements (legends, tooltips, axis
groups) being added between the card and the chart media. */
.analytics-chart-card svg,
.analytics-chart-card canvas { max-width: 100%; height: auto; display: block; }
.analytics-chart-card h4 { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-muted); margin-bottom: 4px; }
.analytics-chart-desc { font-size: 10px; color: var(--text-muted); margin-bottom: 8px; font-style: italic; }
.analytics-heatmap { display: grid; grid-template-columns: 40px repeat(24, 1fr); gap: 2px; }
.analytics-heatmap-cell { aspect-ratio: 1; border-radius: 2px; cursor: default; }
.analytics-heatmap-label { font-size: 10px; color: var(--text-muted); display: flex; align-items: center; }
.analytics-time-range { display: flex; gap: 8px; margin-bottom: 16px; }
.analytics-time-range button { padding: 4px 12px; border-radius: 4px; border: 1px solid var(--border); background: var(--card-bg); color: var(--text); cursor: pointer; font-size: 12px; }
.analytics-time-range button.active { background: var(--accent); color: white; border-color: var(--accent); }
.analytics-peer-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.analytics-peer-table th { text-align: left; padding: 6px 8px; border-bottom: 2px solid var(--border); color: var(--text-muted); font-size: 11px; text-transform: uppercase; }
.analytics-peer-table td { padding: 6px 8px; border-bottom: 1px solid var(--border); }
.analytics-peer-table tr:hover td { background: var(--card-bg); }
@media (max-width: 768px) { .analytics-stats { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .analytics-stats { grid-template-columns: 1fr; } }
/* Claimed (My Mesh) node rows */
.claimed-row { background: color-mix(in srgb, var(--accent) 8%, transparent) !important; border-left: 3px solid var(--accent); }
.claimed-row:hover { background: color-mix(in srgb, var(--accent) 14%, transparent) !important; }
.claimed-badge { color: var(--accent); font-size: 13px; margin-right: 2px; }
/* Filter toggle button — hidden on desktop */
.filter-toggle-btn { display: none; }
/* Mobile detail bottom sheet */
.mobile-detail-sheet {
display: none;
position: fixed; bottom: 0; left: 0; right: 0;
max-height: 70vh; background: var(--detail-bg);
border-top-left-radius: 16px; border-top-right-radius: 16px;
box-shadow: 0 -4px 24px rgba(0,0,0,.3);
z-index: 200; overflow-y: auto; padding: 8px 16px 24px;
transform: translateY(100%); transition: transform .25s ease;
}
.mobile-detail-sheet.open { display: block; transform: translateY(0); }
.mobile-sheet-handle {
width: 40px; height: 4px; background: var(--border);
border-radius: 2px; margin: 4px auto 8px; cursor: pointer;
}
.mobile-sheet-close {
position: absolute; top: 8px; right: 12px;
background: none; border: none; font-size: 20px;
color: var(--text-muted); cursor: pointer; z-index: 1;
}
.mobile-sheet-close:hover { color: var(--text); }
.mobile-sheet-content { padding-top: 4px; }
/* Perf dashboard */
.perf-card { background: var(--surface-1); border: 1px solid var(--border); border-radius: 8px; padding: 12px 20px; min-width: 120px; text-align: center; }
.perf-num { font-size: 24px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
.perf-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
.perf-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.perf-table th { text-align: left; padding: 6px 10px; border-bottom: 2px solid var(--border); color: var(--text-muted); font-size: 11px; text-transform: uppercase; }
.perf-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
.perf-table code { font-size: 12px; color: var(--text); }
.perf-table .perf-slow { background: rgba(239, 68, 68, 0.08); }
.perf-table .perf-slow td { color: var(--status-red); }
.perf-table .perf-warn { background: rgba(251, 191, 36, 0.06); }
.perf-table .perf-warn td { color: var(--status-yellow); }
/* #204 — Perf page responsive */
@media (max-width: 640px) {
#perfWrapper { padding: 12px !important; }
.perf-card { min-width: 0; flex: 1 1 calc(50% - 8px); }
.perf-table { font-size: 11px; }
.perf-table th, .perf-table td { padding: 4px 6px; }
}
/* ─── Region filter bar ─── */
.region-filter-bar { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 0; }
.region-filter-container { margin: 0; padding: 0; display: inline-flex; align-items: center; }
.region-pill {
display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 16px;
font-size: 12px; font-weight: 500; cursor: pointer; border: 1.5px solid var(--border);
background: transparent; color: var(--text-muted); transition: all 0.15s;
}
.region-pill:hover { border-color: var(--accent); color: var(--accent); }
.region-pill-active {
background: var(--accent); color: #fff; border-color: var(--accent);
}
.region-pill-active:hover { opacity: 0.85; }
.region-filter-label {
font-size: 12px; font-weight: 600; color: var(--text-muted); align-self: center;
margin-right: 2px; user-select: none;
}
.region-dropdown-wrap { position: relative; display: inline-flex; align-items: center; }
.region-dropdown-trigger {
display: inline-flex; align-items: center; padding: 6px 10px; border-radius: 6px;
font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border);
background: var(--input-bg); color: var(--text); transition: all 0.15s;
height: 34px; box-sizing: border-box; white-space: nowrap; line-height: 1;
}
.region-dropdown-trigger:hover { border-color: var(--accent); color: var(--accent); }
.region-dropdown-menu {
position: absolute; top: 100%; left: 0; z-index: 90;
min-width: 220px; width: max-content; max-height: 260px; overflow-y: auto;
background: var(--card-bg, #fff); border: 1px solid var(--border); border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.12); padding: 4px 0;
}
.region-dropdown-item {
display: flex; align-items: center; gap: 6px; padding: 6px 12px;
font-size: 13px; cursor: pointer; color: var(--text); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; max-width: 320px;
}
.region-dropdown-item input[type="checkbox"] {
width: 14px; height: 14px; margin: 0; flex-shrink: 0;
}
.region-dropdown-item:hover { background: var(--row-hover, #f5f5f5); }
/* Generic multi-select dropdown (Observer, Type filters) */
.multi-select-wrap { position: relative; display: inline-flex; align-items: center; }
.multi-select-trigger {
display: inline-flex; align-items: center; padding: 6px 10px; border-radius: 6px;
font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border);
background: var(--input-bg); color: var(--text); transition: all 0.15s;
height: 34px; box-sizing: border-box; white-space: nowrap; line-height: 1;
}
.multi-select-trigger:hover { border-color: var(--accent); color: var(--accent); }
.multi-select-menu {
position: absolute; top: 100%; left: 0; z-index: 90;
min-width: 220px; max-height: 260px; overflow-y: auto;
background: var(--card-bg, #fff); border: 1px solid var(--border); border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.12); padding: 4px 0; display: none;
}
.multi-select-menu.open { display: block; }
.multi-select-item {
display: flex; align-items: center; gap: 6px; padding: 6px 12px;
font-size: 13px; cursor: pointer; color: var(--text); white-space: nowrap;
}
.multi-select-item input[type="checkbox"] {
width: 14px; height: 14px; margin: 0; flex-shrink: 0;
}
.multi-select-item:hover { background: var(--row-hover, #f5f5f5); }
.chan-tag { background: var(--accent, #3b82f6); color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 600; }
/* Matrix mode hex animation */
.matrix-char { background: none !important; border: none !important; }
.matrix-char span { display: block; text-align: center; white-space: nowrap; line-height: 1; }
/* === Matrix Theme === */
.matrix-theme .leaflet-tile-pane {
filter: brightness(1.1) contrast(1.2) sepia(0.6) hue-rotate(70deg) saturate(2);
}
.matrix-theme.leaflet-container::before {
content: ''; position: absolute; inset: 0; z-index: 401;
background: rgba(0, 60, 10, 0.35); mix-blend-mode: multiply; pointer-events: none;
}
.matrix-theme.leaflet-container::after {
content: ''; position: absolute; inset: 0; z-index: 402;
background: rgba(0, 255, 65, 0.06); mix-blend-mode: screen; pointer-events: none;
}
.matrix-theme { background: #000 !important; }
.matrix-theme .leaflet-control-zoom a { background: #0a0a0a !important; color: #00ff41 !important; border-color: #00ff4130 !important; }
.matrix-theme .leaflet-control-attribution { background: rgba(0,0,0,0.8) !important; color: #00ff4180 !important; }
.matrix-theme .leaflet-control-attribution a { color: #00ff4160 !important; }
/* Scanline overlay */
.matrix-scanlines {
position: absolute; inset: 0; z-index: 9999; pointer-events: none;
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,255,65,0.02) 2px, rgba(0,255,65,0.02) 4px);
}
/* Feed panel in matrix mode */
.matrix-theme .live-feed {
background: rgba(0, 10, 0, 0.92) !important;
border-color: #00ff4130 !important;
font-family: 'Courier New', monospace !important;
}
.matrix-theme .live-feed .live-feed-item { color: #00ff41 !important; border-color: #00ff4115 !important; }
.matrix-theme .live-feed .live-feed-item:hover { background: rgba(0,255,65,0.08) !important; }
.matrix-theme .live-feed .feed-hide-btn { color: #00ff41 !important; }
/* Controls in matrix mode */
.matrix-theme .live-controls {
background: rgba(0, 10, 0, 0.9) !important;
border-color: #00ff4130 !important;
color: #00ff41 !important;
}
.matrix-theme .live-controls label,
.matrix-theme .live-controls span,
.matrix-theme .live-controls .lcd-display { color: #00ff41 !important; }
.matrix-theme .live-controls button { color: #00ff41 !important; border-color: #00ff4130 !important; }
.matrix-theme .live-controls input[type="range"] { accent-color: #00ff41; }
/* Node detail panel in matrix mode */
.matrix-theme .live-node-detail {
background: rgba(0, 10, 0, 0.95) !important;
border-color: #00ff4130 !important;
color: #00ff41 !important;
}
.matrix-theme .live-node-detail a { color: #00ff41 !important; }
.matrix-theme .live-node-detail .feed-hide-btn { color: #00ff41 !important; }
/* Node labels on map */
.matrix-theme .node-label { color: #00ff41 !important; text-shadow: 0 0 4px #00ff41 !important; }
.matrix-theme .leaflet-marker-icon:not(.matrix-char) { filter: hue-rotate(90deg) saturate(1) brightness(0.35) opacity(0.5); }
/* Audio controls */
.audio-controls {
display: flex;
gap: 12px;
align-items: center;
padding: 4px 8px;
font-size: 12px;
}
.audio-controls.hidden { display: none; }
.audio-slider-label {
display: flex;
align-items: center;
gap: 4px;
color: var(--text-muted, #6b7280);
font-size: 11px;
white-space: nowrap;
}
.audio-slider {
width: 80px;
height: 4px;
cursor: pointer;
accent-color: #8b5cf6;
}
.audio-slider-label span {
min-width: 24px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.matrix-theme .audio-controls label,
.matrix-theme .audio-controls span { color: #00ff41 !important; }
.matrix-theme .audio-slider { accent-color: #00ff41; }
/* Audio voice selector */
.audio-voice-select {
background: var(--input-bg, #1f2937);
color: var(--text, #e5e7eb);
border: 1px solid var(--border, #374151);
border-radius: 4px;
padding: 2px 4px;
font-size: 11px;
cursor: pointer;
}
.matrix-theme .audio-voice-select {
background: #001a00 !important;
color: #00ff41 !important;
border-color: #00ff4130 !important;
}
/* Audio unlock overlay */
.audio-unlock-overlay {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.6);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.audio-unlock-prompt {
background: #1f2937;
color: #e5e7eb;
padding: 24px 40px;
border-radius: 12px;
font-size: 20px;
font-weight: 600;
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
user-select: none;
}
.matrix-theme .audio-unlock-prompt {
background: #001a00;
color: #00ff41;
box-shadow: 0 0 30px rgba(0,255,65,0.2);
}
/* Packet Filter Language */
.packet-filter-input { transition: border-color 0.2s; }
.packet-filter-input:focus { border-color: var(--accent); outline: none; }
.packet-filter-input.filter-error { border-color: var(--status-red); }
.packet-filter-input.filter-active { border-color: var(--status-green); }
/* === Observer Comparison (#/compare) === */
.compare-controls { margin-bottom: 20px; }
.compare-selector {
display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
}
.compare-select-group { display: flex; flex-direction: column; gap: 4px; }
.compare-select-group label {
font-size: 12px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.3px; color: var(--text-muted);
}
.compare-select {
padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px;
background: var(--input-bg); color: var(--text); font-size: 14px;
min-width: 220px; cursor: pointer;
}
.compare-select:focus { border-color: var(--accent); outline: none; }
.compare-vs {
font-size: 18px; font-weight: 700; color: var(--text-muted);
padding-bottom: 6px;
}
.compare-btn {
padding: 8px 20px; border: none; border-radius: 6px;
background: var(--accent); color: #fff; font-size: 14px; font-weight: 600;
cursor: pointer; transition: background 0.15s;
}
.compare-btn:hover:not(:disabled) { background: var(--accent-hover); }
.compare-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.compare-results { margin-top: 16px; }
.compare-summary {
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px; margin-bottom: 16px;
}
.compare-card {
padding: 16px; border-radius: 8px; text-align: center; cursor: pointer;
border: 2px solid transparent; transition: border-color 0.15s, transform 0.1s;
}
.compare-card:hover { transform: translateY(-2px); }
.compare-card-count { font-size: 28px; font-weight: 700; }
.compare-card-label { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
.compare-card-pct { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.compare-card-both {
background: rgba(34, 197, 94, 0.1); border-color: rgba(34, 197, 94, 0.3);
}
.compare-card-both .compare-card-count { color: var(--status-green); }
.compare-card-a {
background: rgba(74, 158, 255, 0.1); border-color: rgba(74, 158, 255, 0.3);
}
.compare-card-a .compare-card-count { color: var(--accent); }
.compare-card-b {
background: rgba(255, 107, 107, 0.1); border-color: rgba(255, 107, 107, 0.3);
}
.compare-card-b .compare-card-count { color: var(--status-red); }
/* Comparison bar */
.compare-bar-container { margin-bottom: 16px; }
.compare-bar {
display: flex; height: 24px; border-radius: 6px; overflow: hidden;
background: var(--border);
}
.compare-bar-seg { transition: width 0.3s ease; }
.compare-bar-a { background: var(--accent); }
.compare-bar-both { background: var(--status-green); }
.compare-bar-b { background: var(--status-red); }
.compare-bar-legend {
display: flex; gap: 16px; margin-top: 8px; font-size: 12px;
color: var(--text-muted);
}
.compare-legend-item { display: flex; align-items: center; gap: 4px; }
.compare-dot {
width: 10px; height: 10px; border-radius: 50%; display: inline-block;
}
.compare-dot-a { background: var(--accent); }
.compare-dot-both { background: var(--status-green); }
.compare-dot-b { background: var(--status-red); }
.compare-type-summary {
margin-bottom: 16px; font-size: 13px; color: var(--text);
}
.compare-type-badge {
display: inline-block; padding: 2px 8px; margin: 2px;
border-radius: var(--badge-radius); background: var(--surface-0);
font-size: 12px; color: var(--text-muted);
}
.compare-tabs { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
.compare-summary-text { padding: 12px 0; font-size: 14px; line-height: 1.6; }
.compare-summary-text p { margin: 0 0 8px; }
.compare-warning { color: var(--status-yellow); font-weight: 600; }
.compare-good { color: var(--status-green); font-weight: 600; }
.compare-table { font-size: 13px; }
@media (max-width: 640px) {
.compare-selector { flex-direction: column; align-items: stretch; }
.compare-select { min-width: auto; width: 100%; }
.compare-summary { grid-template-columns: 1fr; }
}
/* Neighbor graph canvas focus indicator for keyboard navigation */
#ngCanvas:focus {
outline: 2px solid var(--link-color, #60a5fa);
outline-offset: 2px;
}
#ngCanvas:focus:not(:focus-visible) {
outline: none;
}
/* ===================== RF Health Dashboard ===================== */
.rf-health-container { padding: 0; }
.rf-time-selector {
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
margin-bottom: 8px; padding: 8px 0;
}
.rf-range-btn {
padding: 4px 10px; border: 1px solid var(--border); border-radius: 4px;
background: var(--bg-secondary, var(--card-bg, #1e1e1e)); color: var(--text-primary, #e0e0e0);
cursor: pointer; font-size: 12px; transition: background 0.15s;
}
.rf-range-btn:hover { background: var(--bg-hover, #333); }
.rf-range-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.rf-custom-inputs { display: inline-flex; gap: 4px; align-items: center; margin-left: 8px; }
.rf-datetime {
padding: 3px 6px; border: 1px solid var(--border); border-radius: 4px;
background: var(--bg-secondary, var(--card-bg)); color: var(--text-primary); font-size: 12px;
}
.rf-health-split {
display: flex; height: calc(100vh - 180px); min-height: 300px; overflow: hidden;
}
.rf-health-grid {
flex: 1; min-width: 0; overflow-y: auto; padding: 0 8px 8px 0;
display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 8px; align-content: start;
}
.rf-cell {
border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px;
cursor: pointer; transition: border-color 0.15s, background 0.15s;
background: var(--bg-secondary, var(--card-bg, #1e1e1e));
}
.rf-cell:hover { border-color: var(--accent); }
.rf-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.rf-cell-selected { border-color: var(--accent); background: var(--bg-hover, rgba(96,165,250,0.08)); }
.rf-cell-header { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; margin-bottom: 4px; }
.rf-cell-name { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; }
.rf-cell-nf { font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rf-cell-batt { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
.rf-nf-warning { color: var(--status-yellow, #f59e0b); }
.rf-nf-critical { color: var(--status-red, #ef4444); }
.rf-cell-sparkline { height: 24px; margin: 2px 0; overflow: hidden; }
.rf-cell-stats { display: flex; gap: 8px; font-size: 10px; color: var(--text-muted); }
/* Side panel for observer detail */
.rf-health-detail {
width: 420px; min-width: 280px; max-width: 50vw;
border-left: 1px solid var(--border); background: var(--bg-secondary, var(--card-bg));
overflow-y: auto; padding: 16px; position: relative;
animation: slideInRight 200ms ease-out;
}
.rf-health-detail.rf-panel-empty {
display: flex; align-items: center; justify-content: center;
color: var(--text-muted); font-size: 14px; animation: none;
}
.rf-detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.rf-detail-header h3 { margin: 0; font-size: 16px; }
.rf-detail-close {
background: none; border: none; color: var(--text-muted); cursor: pointer;
font-size: 18px; padding: 2px 6px; border-radius: 4px;
}
.rf-detail-close:hover { background: var(--bg-hover); }
.rf-detail-charts { display: flex; flex-direction: column; gap: 4px; }
.rf-detail-chart { margin: 0; overflow-x: auto; }
.rf-detail-summary { font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
@media (max-width: 640px) {
.rf-health-split { flex-direction: column; height: auto; }
.rf-health-grid { grid-template-columns: 1fr; max-height: 50vh; }
.rf-health-detail {
width: 100% !important; max-width: 100%; min-width: 0;
border-left: none; border-top: 1px solid var(--border);
}
.rf-time-selector { gap: 3px; }
.rf-custom-inputs { margin-left: 0; margin-top: 4px; flex-wrap: wrap; }
}
/* Channel Color Picker Popover (M2, #271) */
/* === Channel Color Picker (#674) === */
.cc-picker-popover {
position: fixed;
z-index: 9999;
background: var(--bg-secondary, #1e1e1e);
border: 1px solid var(--border-color, #333);
border-radius: 8px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.cc-picker-swatches {
display: flex;
gap: 6px;
}
.cc-swatch {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
padding: 0;
transition: border-color 0.15s;
}
.cc-swatch:hover { border-color: rgba(255,255,255,0.6); }
.cc-swatch:focus-visible { border-color: #fff; outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
.cc-swatch-active { border-color: #fff; }
.cc-picker-clear {
display: block;
width: 100%;
margin-top: 6px;
padding: 4px 0;
font-size: 11px;
color: var(--text-muted, #888);
background: none;
border: none;
cursor: pointer;
text-align: center;
}
.cc-picker-clear:hover { color: var(--text-primary, #e0e0e0); }
/* Color dot affordance (#674) */
.ch-color-dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
border: 1.5px solid rgba(255,255,255,0.3);
cursor: pointer;
vertical-align: middle;
margin-left: 6px;
flex-shrink: 0;
}
.ch-color-clear {
display: inline-block;
font-size: 10px;
line-height: 1;
color: var(--text-muted, #888);
cursor: pointer;
margin-left: 3px;
vertical-align: middle;
}
.ch-color-clear:hover { color: var(--text-primary, #e0e0e0); }
.ch-color-dot:not([style*="background"]) {
background: transparent;
border-style: dashed;
border-color: var(--text-muted, #888);
}
/* Mobile bottom-sheet + larger touch targets (#674) */
@media (pointer: coarse) {
.ch-color-dot {
width: 20px;
height: 20px;
margin-left: 8px;
}
.cc-swatch {
width: 36px;
height: 36px;
}
.cc-picker-swatches {
justify-content: center;
gap: 10px;
}
.cc-picker-popover {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
top: auto !important;
width: 100% !important;
max-width: 100% !important;
border-radius: 12px 12px 0 0;
padding: 16px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
}
/* === #630 — Mobile Accessibility Fixes === */
/* #630-1: Touch targets — minimum 44px on touch devices */
@media (pointer: coarse) {
.filter-bar .btn,
.filter-group .btn,
.tab-btn,
.filter-bar input,
.filter-bar select,
.nav-btn,
.region-pill,
.region-dropdown-trigger,
.multi-select-trigger,
.node-count-pill,
.analytics-time-range button,
.detail-back-btn,
.filter-toggle-btn {
min-height: 44px;
min-width: 44px;
}
.filter-bar input,
.filter-bar select {
height: 44px;
}
.region-dropdown-trigger,
.multi-select-trigger {
height: 44px;
}
}
/* #630-3: Status text labels — visually hidden text for screen readers */
.sr-status-label { font-size: 11px; margin-left: 4px; }
/* #630-4: Detail panel as full-width overlay on mobile */
@media (max-width: 640px) {
.split-layout .panel-right:not(.empty) {
display: block;
position: fixed;
top: 52px;
left: 0;
right: 0;
bottom: 0;
width: 100%;
min-width: 0;
z-index: 150;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}
/* #630-5: Analytics tabs — horizontal scroll on small screens */
@media (max-width: 640px) {
.analytics-tabs {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
padding-bottom: 4px;
}
.analytics-tabs .tab-btn {
flex-shrink: 0;
white-space: nowrap;
}
}
/* #630-6: Tables — horizontal scroll wrapper */
.table-scroll-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
@media (max-width: 640px) {
.data-table { min-width: 480px; }
}
/* Table sorting indicators */
th[data-sort-key] { cursor: pointer; user-select: none; }
th[data-sort-key]:hover { background: var(--hover-bg, rgba(255,255,255,0.05)); }
th.sort-active { color: var(--accent, #60a5fa); }
.sort-arrow { font-size: 0.75em; opacity: 0.8; }
/* #690 — Clock Skew badges & fleet table */
.skew-badge { display: inline-block; font-size: 10px; padding: 1px 5px; border-radius: 3px; margin-left: 4px; font-weight: 600; white-space: nowrap; }
.skew-badge--ok { background: var(--status-green); color: #fff; }
.skew-badge--warning { background: var(--status-yellow); color: #000; }
.skew-badge--critical { background: var(--status-orange); color: #fff; }
.skew-badge--absurd { background: var(--status-purple); color: #fff; }
.skew-badge--no_clock { background: var(--text-muted); color: #fff; }
.skew-badge--bimodal_clock { background: var(--status-amber-light); color: var(--status-amber-text); border: 1px solid var(--status-amber); }
.skew-detail-section { padding: 10px 16px; margin-bottom: 8px; }
.skew-sparkline-wrap { margin-top: 6px; }
.skew-sparkline-wrap svg { display: block; }
.clock-fleet-row--warning { background: color-mix(in srgb, var(--status-yellow) 10%, transparent); }
.clock-fleet-row--critical { background: color-mix(in srgb, var(--status-orange) 10%, transparent); }
.clock-fleet-row--absurd { background: color-mix(in srgb, var(--status-purple) 10%, transparent); }
.clock-fleet-row--no_clock { background: color-mix(in srgb, var(--text-muted) 10%, transparent); }
.clock-filter-btn { font-size: 12px; padding: 3px 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--card-bg, #fff); color: var(--text); cursor: pointer; margin-right: 4px; }
.clock-filter-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
/* === Path Inspector (issue #944) === */
.path-inspector-page { padding: 16px; max-width: 900px; margin: 0 auto; }
.path-inspector-input-row { display: flex; gap: 8px; margin-bottom: 12px; }
.path-inspector-input-row .input { flex: 1; }
.path-inspector-error { color: var(--status-red, #ef4444); font-size: 13px; margin-bottom: 8px; }
.path-inspector-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.path-inspector-table th,
.path-inspector-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); text-align: left; }
.path-inspector-table th { background: var(--card-bg); font-weight: 600; }
.speculative-warning { color: var(--path-inspector-speculative, #d97706); font-weight: 600; }
.speculative-badge { cursor: help; }
.speculative-row { background: color-mix(in srgb, var(--path-inspector-speculative, #d97706) 8%, transparent); }
.evidence-row { font-size: 12px; color: var(--text-muted); }
.evidence-row.collapsed { display: none; }
.evidence-detail { padding: 4px 10px; }
.hop-evidence { margin: 2px 0; }
.path-inspector-stats { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
.no-results { color: var(--text-muted); font-style: italic; }
/* Map side pane for path inspector */
.map-side-pane { flex: 0 0 32px; overflow: hidden; transition: flex-basis 0.2s; border-left: 1px solid var(--border); background: var(--card-bg); }
.map-side-pane.expanded { flex: 0 0 320px; overflow-y: auto; padding: 12px; }
.map-side-pane .pane-toggle { cursor: pointer; padding: 8px; font-size: 14px; text-align: center; }
.map-side-pane .pane-content { display: none; }
.map-side-pane.expanded .pane-content { display: block; }
/* Tools landing page */
.tools-landing { padding: 24px; max-width: 600px; }
.tools-menu { display: flex; flex-direction: column; gap: 12px; margin-top: 16px; }
.tools-card { display: block; padding: 16px; border-radius: 8px; border: 1px solid var(--border); background: var(--card-bg); color: var(--text); text-decoration: none; transition: border-color 0.2s; }
.tools-card:hover { border-color: var(--primary); }
.tools-card h3 { margin: 0 0 4px 0; font-size: 16px; }
.tools-card p { margin: 0; font-size: 13px; color: var(--text-muted); }
/* ── Map marker clustering (issue #1036) ── */
.mc-cluster-wrap { background: transparent !important; border: 0 !important; }
.mc-cluster {
width: 48px; height: 48px; border-radius: 50%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
font-family: var(--font, system-ui, sans-serif);
color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.5);
border: 2px solid rgba(255,255,255,0.85);
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
cursor: pointer;
transition: transform 120ms ease;
}
.mc-cluster:hover { transform: scale(1.06); }
.mc-cluster.mc-sm { background: var(--info, #2563eb); width: 40px; height: 40px; }
.mc-cluster.mc-md { background: var(--warning, #d97706); width: 48px; height: 48px; }
.mc-cluster.mc-lg { background: var(--accent, #dc2626); width: 56px; height: 56px; }
.mc-cluster .mc-count { font-size: 14px; font-weight: 700; line-height: 1; }
.mc-cluster.mc-lg .mc-count { font-size: 16px; }
.mc-cluster .mc-pills {
display: flex; gap: 2px; margin-top: 3px;
}
.mc-cluster .mc-pill {
display: inline-block; min-width: 12px; padding: 0 3px;
border-radius: 6px; font-size: 9px; font-weight: 600; line-height: 12px;
color: #fff; text-align: center; text-shadow: none;
border: 1px solid rgba(255,255,255,0.4);
}
/* === #1034 PR1: Channel Add modal + sectioned sidebar === */
.ch-add-channel-btn {
background: var(--accent, #2563eb); color: #fff; border: none;
padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;
}
.ch-add-channel-btn:hover { background: var(--accent-hover, #1d4ed8); }
.ch-modal-overlay { z-index: 1100; }
.ch-modal-overlay.hidden { display: none; }
.ch-modal { width: 560px; max-width: 92vw; padding: 24px 24px 16px; position: relative; }
.ch-modal h3 { margin: 0 0 16px; font-size: 18px; }
.ch-modal-close {
position: absolute; top: 10px; right: 10px;
background: transparent; border: none; cursor: pointer;
font-size: 18px; color: var(--text-muted); padding: 4px 8px; border-radius: 6px;
}
.ch-modal-close:hover { background: var(--row-hover, rgba(0,0,0,0.05)); color: var(--text); }
.ch-modal-section { padding: 12px 0; border-top: 1px solid var(--border); }
.ch-modal-section:first-of-type { border-top: none; padding-top: 0; }
.ch-modal-section-title { margin: 0 0 4px; font-size: 14px; font-weight: 600; }
.ch-modal-section-hint { margin: 0 0 10px; font-size: 12px; color: var(--text-muted); }
.ch-modal-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
.ch-modal-input {
flex: 1; min-width: 0; padding: 7px 10px; font-size: 13px;
border: 1px solid var(--border); border-radius: 6px;
background: var(--input-bg, var(--card-bg)); color: var(--text);
}
.ch-modal-input--mono { font-family: var(--mono, monospace); }
.ch-modal-btn-secondary {
background: var(--card-bg); color: var(--text);
border: 1px solid var(--border); padding: 7px 12px;
border-radius: 6px; cursor: pointer; font-size: 13px;
}
.ch-modal-btn-secondary[disabled] { opacity: .5; cursor: not-allowed; }
.ch-hashtag-row .ch-hashtag-prefix {
font-family: var(--mono, monospace); font-size: 14px; color: var(--text-muted); padding: 0 2px;
}
.ch-modal-warn { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.ch-modal-warn code { background: var(--row-hover, rgba(0,0,0,0.05)); padding: 1px 4px; border-radius: 3px; font-size: 11px; }
.ch-modal-error { color: var(--status-red, #dc2626); font-size: 12px; margin-top: 4px; }
.ch-modal-footer {
margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border);
font-size: 12px; color: var(--text-muted); line-height: 1.4;
}
.ch-modal-callout {
margin: 10px 0 14px; padding: 10px 12px; border-radius: 6px;
background: var(--warn-bg, #fef3c7); color: var(--warn-text, #92400e);
border: 1px solid var(--warn-border, #fcd34d);
font-size: 13px; line-height: 1.4;
}
.ch-section-locality {
font-size: 12px; font-weight: 500; text-transform: none;
letter-spacing: 0; color: var(--text-muted); opacity: 0.85;
margin-left: 4px;
}
.ch-qr-output { font-size: 11px; font-family: var(--mono, monospace); color: var(--text-muted); word-break: break-all; min-height: 14px; padding: 4px 0; }
/* #1087 polish: dedicated Share modal styling. Mirrors .ch-modal* tokens. */
.ch-share-modal { max-width: 480px; }
.ch-share-modal-title { margin: 0 0 12px; font-size: 16px; font-weight: 600; }
.ch-share-modal-body { display: flex; flex-direction: column; gap: 12px; }
.ch-share-qr {
display: flex; align-items: center; justify-content: center;
min-height: 120px; padding: 8px;
background: var(--bg-secondary, rgba(0,0,0,0.02));
border: 1px solid var(--border); border-radius: 6px;
}
.ch-share-field-group { display: flex; flex-direction: column; gap: 4px; }
.ch-share-label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.ch-share-row { display: flex; gap: 6px; align-items: center; }
.ch-share-row .ch-modal-input { flex: 1; min-width: 0; }
.ch-share-error {
color: var(--status-red, #dc2626); font-size: 13px; line-height: 1.4;
padding: 8px 12px; text-align: center;
}
.ch-section { margin-bottom: 8px; }
.ch-section-header {
display: flex; align-items: center; gap: 6px;
padding: 6px 10px; font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: .5px; color: var(--text-muted);
background: transparent; border: none; width: 100%; text-align: left; cursor: default;
}
.ch-section-toggle { cursor: pointer; }
.ch-section-toggle:hover { color: var(--text); }
.ch-section-empty { padding: 8px 12px; font-size: 12px; color: var(--text-muted); font-style: italic; }
.ch-section-caret { display: inline-block; width: 10px; }
/* ── Filter UX (issue #966) ────────────────────────────────────────────── */
.fux-bar { display: flex; gap: 6px; margin-top: 4px; align-items: center; flex-wrap: wrap; position: relative; }
.fux-help-btn,
.fux-saved-trigger { background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 4px; padding: 2px 8px; font-size: 12px; cursor: pointer; }
.fux-help-btn:hover,
.fux-saved-trigger:hover { background: var(--bg-hover, var(--surface)); }
.fux-popover { position: fixed; top: 60px; right: 24px; width: min(720px, 92vw); max-height: 80vh; overflow: auto; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.35); z-index: 10000; padding: 0; }
.fux-popover-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--surface); }
.fux-popover-close { background: transparent; border: none; color: var(--text-muted); font-size: 16px; cursor: pointer; padding: 0 4px; }
.fux-popover-close:hover { color: var(--text); }
.fux-popover-body { padding: 12px 16px; font-size: 13px; }
.fux-popover-body h3,
.fux-popover-body h4 { margin: 12px 0 6px; }
.fux-table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 4px 0 12px; }
.fux-table th,
.fux-table td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
.fux-table th { color: var(--text-muted); font-weight: 600; }
.fux-mono { font-family: var(--mono); font-size: 12px; }
.fux-examples { margin: 4px 0; padding-left: 20px; }
.fux-examples li { margin: 2px 0; }
.fux-ac-dropdown { position: absolute; left: 0; right: 0; top: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: 4px; max-height: 280px; overflow-y: auto; z-index: 9999; box-shadow: 0 4px 12px rgba(0,0,0,0.25); margin-top: 2px; }
.fux-ac-item { padding: 4px 10px; display: flex; justify-content: space-between; gap: 12px; cursor: pointer; font-size: 12px; }
.fux-ac-item:hover,
.fux-ac-item.active { background: var(--bg-hover, rgba(120,160,255,0.12)); }
.fux-ac-val { font-family: var(--mono); color: var(--text); }
.fux-ac-desc { color: var(--text-muted); font-size: 11px; max-width: 60%; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fux-ctx-menu { position: absolute; background: var(--surface); border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 4px 14px rgba(0,0,0,0.35); z-index: 10001; min-width: 200px; padding: 4px 0; }
.fux-ctx-item { display: block; width: 100%; text-align: left; background: transparent; border: none; color: var(--text); padding: 5px 12px; font-size: 12px; cursor: pointer; font-family: var(--mono); }
.fux-ctx-item:hover { background: var(--bg-hover, rgba(120,160,255,0.12)); }
.fux-saved-menu { position: absolute; top: 100%; left: 0; min-width: 320px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; z-index: 9999; box-shadow: 0 4px 14px rgba(0,0,0,0.3); margin-top: 4px; padding: 4px 0; }
.fux-saved-menu.hidden { display: none; }
.fux-saved-header { padding: 6px 10px; font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
.fux-saved-item { display: flex; align-items: center; gap: 8px; padding: 5px 10px; cursor: pointer; font-size: 12px; }
.fux-saved-item:hover { background: var(--bg-hover, rgba(120,160,255,0.12)); }
.fux-saved-name { font-weight: 600; min-width: 120px; }
.fux-saved-expr { color: var(--text-muted); font-size: 11px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fux-saved-del { background: transparent; border: none; color: var(--text-muted); font-size: 12px; cursor: pointer; padding: 0 4px; }
.fux-saved-del:hover { color: var(--status-red, #ef4444); }
.fux-saved-footer { border-top: 1px solid var(--border); padding: 4px 0; }
.fux-saved-save { display: block; width: 100%; text-align: left; background: transparent; border: none; color: var(--text); padding: 6px 10px; font-size: 12px; cursor: pointer; }
.fux-saved-save:hover { background: var(--bg-hover, rgba(120,160,255,0.12)); }
td[data-filter-field] { cursor: context-menu; }