mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-12 04:25:04 +00:00
494d3022f9
## Partial fix for #1128 — closes the gaps PR #1131 left behind PR #1131 was a partial fix for the packets-page layout chaos (merged 2026-05-06 ~01:55 UTC, then the issue was reopened by the maintainer). #1131 shipped Bug 4 (`--surface` definition), the `.path-popover` flip + lower z-index, the debounced re-measure for Bug 1, the `.filter-bar` row-gap + `.multi-select-trigger` truncation for Bug 3, the new z-index TOKENS, and a single-viewport E2E with five individual-component assertions. This PR closes everything else the issue body and the `specs/packets-layout-audit.md` audit asked for. ### What changed (per gap) **Gap A — apply the z-index scale (audit Section 2)** #1131 added `--z-dropdown` / `--z-popover` / `--z-modal` / `--z-tooltip` but explicitly left existing literal values in place. This PR renumbers the 7 dropdowns/popovers the audit named: | Selector | Before | After | |---|---:|---:| | `.col-toggle-menu` | 50 | `var(--z-dropdown)` (100) | | `.multi-select-menu` | 90 | `var(--z-dropdown)` | | `.region-dropdown-menu` | 90 | `var(--z-dropdown)` | | `.node-filter-dropdown` | 100 | `var(--z-dropdown)` | | `.fux-saved-menu` | `var(--z-tooltip)` (9200) | `var(--z-dropdown)` | | `.fux-ac-dropdown` | `var(--z-tooltip)` | `var(--z-dropdown)` | | `.hop-conflict-popover` | `var(--z-tooltip)` | `var(--z-popover)` (300) | `.fux-ctx-menu` deliberately retains the tooltip band — context menus must float above all toolbar UI. `.region-filter-options-menu` no longer exists in the source (was renamed `.region-dropdown-menu`). The `style.css` doc-block at the top is rewritten to record the applied scale and to point operators at the new lint. **Gap B — CSS-var lint (audit Section 5 #1, "single highest-value addition")** Adds `scripts/check-css-vars.js` (~70 lines). Walks `public/*.css`, extracts every `var(--name)` reference WITHOUT a fallback, asserts the name is defined in some `public/*.css`. References WITH a fallback are tolerated. Wired into CI in the `go-test` job before the JS unit tests. The red commit (`608d81f`) shipped this lint exiting 1 against the master tree — three undefined vars that bypassed earlier review: ``` public/style.css:2628 var(--text-primary) public/style.css:2675 var(--bg-hover) public/style.css:2924 var(--primary) ``` The green commit (`1369d1e`) defines those three as aliases in the :root block (`--text-primary` → `--text`, `--bg-hover` → `--hover-bg`, `--primary` → `--accent`). Light + dark themes inherit through the existing tokens. **Gap C — multi-viewport E2E (issue acceptance criterion)** Adds `test-issue-1128-multi-viewport-e2e.js` — sister of the existing single-viewport test. At each of three viewports (1280×900, 1080×800, 768×1024): - takes a screenshot to `e2e-screenshots/issue-1128-<viewport>.png` - asserts no two `.filter-group` siblings vertically overlap - on desktop+laptop, opens the Saved menu and the Types multi-select and asserts the dropdown does not vertically overlap any `.filter-group` below it Plus three viewport-agnostic assertions: - dropdown selectors compute z-index in `[100,199]` (`.col-toggle-menu`, `.multi-select-menu`, `.region-filter-options-menu`, `.fux-saved-menu`, `.fux-ac-dropdown`) - `.path-hops .hop / .hop-named / .arrow` compute `line-height ≤ 18px` - `.col-path` computes `height ≤ 28px` Wired into the e2e-test job after the existing #1128 test. **Gap D — Bug 5 polish (toolbar reorder)** Audit Section 3 Bug 5: swaps `filter-group-dropdowns` and `filter-group-toggles` in `public/packets.js` so time range + Group by Hash + ★ My Nodes sit next to the search input. Pure markup reorder. No CSS / no JS-handler changes. **Gap E — Bug 1 belt-and-suspenders** Audit Section 3 Bug 1 sub-bullets: - locks `.path-hops .hop / .hop-named / .arrow` to `line-height: 18px` so a chip with mixed font metrics cannot overflow the 22px host vertically and bleed into the row above - converts `.col-path { max-height: 28px }` → `height: 28px` because browsers widely ignore `max-height` on `<td>`s; the earlier rule was a no-op ### TDD discipline (red → green) ``` $ git log --oneline origin/master..HEAD68b0426fix(#1128): Bug 5 — toolbar group reorder (toggles before dropdowns)6d16e6ffix(#1128): apply z-index scale to dropdowns + Bug 1 chip line-height lockb9850c9fix(check-css-vars): strip /* ... */ comments before scanning1369d1efix(#1128): define --text-primary, --bg-hover, --primary aliases (lint green)0d4660ftest(#1128): multi-viewport E2E + wire CSS-var lint into CI (red commit)608d81ftest(#1128): add scripts/check-css-vars.js — fails on 3 undefined vars (red commit) ``` Both red commits (`608d81f`, `0d4660f`) were verified to fail locally before the green commits landed: - `608d81f` runs the lint and exits 1 on the three undefined vars listed above (proven against master). - `0d4660f` introduces the multi-viewport E2E and wires the lint into CI — the lint then fails the build on master, and the E2E z-scale assertion fails because pre-fix `.col-toggle-menu` is 50, the multi-selects are 90, etc. ### Acceptance criteria status From the original issue body: - ✅ Bug 4 root cause fixed (#1131 + this PR's lint guard) - ✅ Bug 1 chip-spill (debounced re-measure from #1131 + line-height lock + col-path height fix from this PR) - ✅ Bug 2 +N popover positioning (#1131) - ✅ Bug 3 toolbar overlap (#1131 + #1131 row-gap) - ✅ Bug 5 group reorder (this PR) - ✅ Z-index scale documented + applied (this PR) - ✅ E2E screenshots at multiple viewports (this PR) - ✅ Bounding-rect collision detection on visible interactive elements (this PR — `.filter-group` siblings + dropdown vs. toolbar) - ✅ CSS-var lint in CI (this PR) ### Why this is "Partial fix for #1128", not "Fixes #1128" Per `AGENTS.md` rule 34, automated closure is reserved for the operator after they verify on staging. The acceptance criteria above appear satisfied in code, but the user should confirm the visual outcome on staging before closing. ### Files changed - `scripts/check-css-vars.js` (new — ~70 lines) - `test-issue-1128-multi-viewport-e2e.js` (new) - `.github/workflows/deploy.yml` (lint step + e2e step wiring) - `public/style.css` (z-renumber, doc-block, Bug 1 polish, alias defs) - `public/packets.js` (Bug 5 reorder) Refs #1128, follows #1131 --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local> Co-authored-by: openclaw-bot <bot@openclaw.local>
3194 lines
140 KiB
CSS
3194 lines
140 KiB
CSS
/* === CoreScope — style.css === */
|
||
|
||
/* ============================================================
|
||
* Z-INDEX SCALE (single source of truth — issues #1128, #1131, #1128 followup)
|
||
* ------------------------------------------------------------
|
||
* Use these tokens for ANY new stacking context. Raw z-index
|
||
* literals are legacy and being migrated. When touching a rule
|
||
* with a literal z-index, replace it with the appropriate token
|
||
* below.
|
||
*
|
||
* --z-base 0 document base, table cells
|
||
* --z-dropdown 100 in-flow dropdowns anchored to a
|
||
* toolbar control (multi-select,
|
||
* saved-filter, column-toggle,
|
||
* autocomplete, region dropdown,
|
||
* node-filter dropdown)
|
||
* --z-popover 300 popovers anchored to a dropdown
|
||
* or table cell (path-overflow +N,
|
||
* hop-conflict, autocomplete row
|
||
* detail). Sits ABOVE dropdowns
|
||
* but BELOW modals.
|
||
* --z-modal-backdrop 9000 full-viewport modal scrim
|
||
* --z-modal 9100 modal panels themselves
|
||
* --z-tooltip 9200 tooltips, ctx menus, hover popovers
|
||
* that must float above modals
|
||
*
|
||
* Lint: scripts/check-css-vars.js asserts every var(--name)
|
||
* reference resolves; CI will fail on undefined custom properties
|
||
* (see #1128 Bug 4 root cause — `--surface` shipped undefined
|
||
* and 8 dropdowns rendered transparent).
|
||
*
|
||
* Migration policy: legacy literal z-index values that work are
|
||
* left in place to avoid behavioural risk; new code MUST use
|
||
* the tokens. The 10 highest-traffic dropdowns/popovers in the
|
||
* packets toolbar were renumbered in the #1128 followup.
|
||
* ============================================================ */
|
||
|
||
/* ============================================================
|
||
* 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;
|
||
|
||
/* --- Logo theme tokens (PR #1137) ------------------------
|
||
* The CoreScope SVG logos are inlined into the DOM (navbar +
|
||
* home hero), so they inherit page CSS custom properties. The
|
||
* legacy --logo-* names are kept so existing themes / brand
|
||
* customizations that override them still work; defaults map
|
||
* to the active theme so wordmark/arc colors track --accent /
|
||
* --nav-text / --text-muted on light AND dark themes.
|
||
* --logo-text → wordmark "CORE" / "SCOPE" / labels
|
||
* --logo-accent → primary node + left-side arcs (default --accent)
|
||
* --logo-accent-hi → secondary node + right-side arcs (default --accent-hover)
|
||
* --logo-muted → tagline + sine wave
|
||
* No --logo-bg here on purpose — the inlined SVGs MUST be
|
||
* transparent so they sit on whatever bg the page paints.
|
||
* --logo-text defaults to --text (the page foreground) so the
|
||
* hero wordmark is readable on any theme; the navbar scopes it
|
||
* to --nav-text below so it stays white on the dark navbar. */
|
||
--logo-text: var(--text);
|
||
--logo-accent: var(--accent);
|
||
--logo-accent-hi: var(--accent-hover);
|
||
--logo-muted: var(--text-muted);
|
||
|
||
--surface-0: #f4f5f7;
|
||
--surface-1: #ffffff;
|
||
--surface-2: #ffffff;
|
||
--surface-3: #ffffff;
|
||
/* #1128 (Bug 4): `--surface` was referenced in 8+ rules (.fux-saved-menu,
|
||
* .fux-popover, .path-popover, .fux-ac-dropdown, .fux-ctx-menu, ...) but
|
||
* never defined → backgrounds resolved transparent → row content bled
|
||
* through dropdowns. Alias it to the opaque card surface. */
|
||
--surface: var(--surface-1);
|
||
--content-bg: var(--surface-0);
|
||
--card-bg: var(--surface-1);
|
||
--hover-bg: rgba(0,0,0, 0.04);
|
||
/* #1128 followup: caught by scripts/check-css-vars.js — these were
|
||
* referenced without a fallback in .rf-detail-meta, .rf-detail-close
|
||
* and .tools-card and would resolve transparent / inherit. Alias them
|
||
* to existing tokens. */
|
||
--text-primary: var(--text);
|
||
--bg-hover: var(--hover-bg);
|
||
--primary: var(--accent);
|
||
/* #1128 followup M3: extended check-css-vars.js to scan JS/HTML inline
|
||
* styles (analytics.js, nodes.js) caught these undefined refs.
|
||
* Aliased to existing tokens — same pattern as --text-primary above. */
|
||
--bg-secondary: var(--surface-2);
|
||
--text-secondary: var(--text-muted);
|
||
--bg: var(--surface);
|
||
--trace-ghost-color: #94a3b8;
|
||
|
||
/* #1128: documented z-index scale. Use these custom props for any new
|
||
* stacking context decision. Existing legacy z-index values that work are
|
||
* left in place to avoid behavioural risk; new code must use these tokens.
|
||
* --z-base 0 document base, table cells
|
||
* --z-dropdown 100 in-flow dropdowns (multi-select, saved, columns)
|
||
* --z-popover 300 popovers anchored to dropdowns / cells
|
||
* --z-modal-backdrop 9000 / --z-modal 9100 / --z-tooltip 9200
|
||
*/
|
||
--z-base: 0;
|
||
--z-dropdown: 100;
|
||
--z-popover: 300;
|
||
--z-modal-backdrop: 9000;
|
||
--z-modal: 9100;
|
||
--z-tooltip: 9200;
|
||
}
|
||
|
||
/* ⚠️ 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;
|
||
--surface: var(--surface-1);
|
||
--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;
|
||
--surface: var(--surface-1);
|
||
--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 (Issue #1055 — Priority+ at all widths) ===
|
||
The top-nav uses Priority+ at ALL widths: only links marked
|
||
data-priority="high" render inline; everything else collapses into
|
||
the "More ▾" menu. This eliminates the historic 1080–1279px overflow
|
||
window AND the 1280–1599px overlap window where the full link strip
|
||
ran underneath .nav-right. Spacing and type use the #1054 fluid
|
||
clamp() tokens so the bar scales smoothly across viewport widths.
|
||
The hamburger and stacked mobile layout still take over at <768px. */
|
||
.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 var(--gutter); 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: var(--space-lg); min-width: 0; flex-shrink: 1; overflow: hidden; }
|
||
.nav-brand { display: flex; align-items: center; gap: var(--space-sm); text-decoration: none; color: var(--nav-text); font-weight: 700; font-size: var(--fs-md); }
|
||
.brand-icon { font-size: 20px; }
|
||
.brand-logo {
|
||
display: block;
|
||
height: 36px;
|
||
/* Width matches the inline SVG's effective content aspect (~3.08:1)
|
||
after cropping to the wordmark+nodes region in the navbar viewBox.
|
||
Pinned explicitly so layout doesn't shift if the SVG is replaced
|
||
(issue #1046 1115px viewport regression). */
|
||
width: 111px;
|
||
max-height: 36px;
|
||
/* The logo is inlined into the DOM (PR #1137) so it inherits page
|
||
CSS variables; theme via --logo-text / --logo-accent / etc. defined
|
||
in :root above. The .top-nav scope below overrides --logo-text to
|
||
--nav-text so the wordmark stays readable on the dark navbar even
|
||
when the page theme is Light. */
|
||
}
|
||
.top-nav {
|
||
/* Navbar lives on --nav-bg (typically dark), so the brand wordmark
|
||
must use --nav-text rather than the page --text default. */
|
||
--logo-text: var(--nav-text);
|
||
}
|
||
.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: var(--space-xs); }
|
||
.nav-link {
|
||
color: var(--nav-text-muted); text-decoration: none;
|
||
padding: 14px clamp(8px, 0.6vw + 4px, 14px); font-size: var(--fs-sm);
|
||
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 clamp(8px, 0.6vw + 4px, 14px);
|
||
}
|
||
|
||
.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: var(--space-sm); 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 toolbar (#1122)
|
||
* Toolbar layout is composed of fenced .filter-group clusters:
|
||
* 1. Quick filters — text inputs (hash, node, observer, region, type, channel)
|
||
* 2. Toggles — Group by Hash, ★ My Nodes
|
||
* 3. Time window — last-N selector
|
||
* 4. Sort & view options — observer sort, columns toggle, hex paths
|
||
* Adjacent .filter-group + .filter-group siblings get a left divider + padding
|
||
* so the visual seam between groups stays readable even when wrapped. */
|
||
.filter-bar {
|
||
display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; align-items: center;
|
||
row-gap: 12px;
|
||
}
|
||
.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; }
|
||
/* #1124 (MAJOR-3): each grouped cluster wraps as a single unit at narrow
|
||
* widths instead of letting individual controls reflow across categories. */
|
||
.filter-bar .filter-group { flex-wrap: nowrap; }
|
||
.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 === */
|
||
/* #1056: Primary tables (packets, nodes, observers) are fluid:
|
||
* - width:100% — columns share the available width via the browser layout
|
||
* - no fixed px widths on individual columns; only min-widths to protect
|
||
* legibility, set in `clamp(...)` so they relax on small viewports
|
||
* - priority-based column hiding done in JS via .col-hidden + a "+N hidden"
|
||
* pill rendered inside thead (clickable to reveal)
|
||
*/
|
||
.data-table {
|
||
width: 100%; border-collapse: collapse; font-size: 12px;
|
||
table-layout: auto;
|
||
}
|
||
.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: clamp(72px, 8vw, 108px); white-space: nowrap; }
|
||
|
||
/* #1056: priority-based column hiding + reveal pill
|
||
* .col-hidden is toggled by JS (TableResponsive.apply) on both th and td.
|
||
* The pill is appended into the last visible th in the header row. */
|
||
.data-table th.col-hidden,
|
||
.data-table td.col-hidden { display: none !important; }
|
||
.col-hidden-pill {
|
||
display: inline-block; margin-left: 6px;
|
||
padding: 1px 7px; border-radius: 999px;
|
||
background: var(--accent-bg, rgba(96,165,250,0.18));
|
||
color: var(--accent, #60a5fa);
|
||
font-size: 10px; font-weight: 700; letter-spacing: .3px;
|
||
cursor: pointer; user-select: none; vertical-align: middle;
|
||
border: 1px solid var(--accent, #60a5fa);
|
||
text-transform: none;
|
||
}
|
||
.col-hidden-pill:hover { filter: brightness(1.15); }
|
||
.col-hidden-pill:focus-visible { outline: 2px solid var(--accent, #60a5fa); outline-offset: 2px; }
|
||
/* MAJOR-4: re-hide affordance — visually distinct from the +N reveal pill. */
|
||
.col-rehide-pill { background: transparent; color: var(--text-dim, #94a3b8); border-color: var(--border, #334155); margin-left: 4px; }
|
||
/* MAJOR-2: when user-resizable columns push total width past container,
|
||
* fall back to horizontal scroll on the wrap rather than overflowing the page. */
|
||
.table-fluid-wrap { width: 100%; overflow-x: auto; }
|
||
.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;
|
||
/* #1122: never wrap; clip so a long hop chain cannot spill into the next row */
|
||
flex-wrap: nowrap;
|
||
max-width: 100%;
|
||
max-height: 22px;
|
||
overflow: hidden;
|
||
vertical-align: middle;
|
||
}
|
||
.path-hops .hop { color: var(--accent); line-height: 18px; }
|
||
.path-hops .hop-named { color: #fff; background: var(--accent); padding: 1px 6px; border-radius: 3px; font-family: var(--font); font-weight: 600; cursor: default; flex: 0 0 auto; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 18px; }
|
||
.path-hops .arrow { color: var(--text-muted); flex: 0 0 auto; line-height: 18px; }
|
||
/* #1122/#1128: bound the row height contributed by the path column.
|
||
* `max-height` on a <td> is widely ignored by browsers (table layout
|
||
* overrides it), so use `height` — table cells respect `height` as
|
||
* min-height, locking the row to a single line of chip content
|
||
* regardless of hop count. */
|
||
.data-table td.col-path { height: 28px; line-height: 22px; overflow: hidden; }
|
||
|
||
/* === Modal === */
|
||
.modal-overlay {
|
||
position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 200;
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding: clamp(8px, 2vw, 24px); /* #1059: viewport-edge breathing room */
|
||
}
|
||
.modal {
|
||
/* #1059: fluid via min(); cap height at 90vh; internal scroll.
|
||
MINOR-6: dropped redundant `width: ...` — `max-width: min(90vw, 500px)`
|
||
alone is sufficient to constrain the box (modals are auto-width within
|
||
a flex centering container). */
|
||
background: var(--card-bg); border-radius: 12px; padding: 24px;
|
||
max-width: min(90vw, 500px);
|
||
max-height: 90vh; overflow-y: auto;
|
||
box-shadow: 0 16px 48px rgba(0,0,0,.2);
|
||
/* Sticky-positioned descendants need a positioned ancestor. */
|
||
position: relative;
|
||
}
|
||
.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;
|
||
}
|
||
|
||
/* === #1059: Sticky close buttons (modal can scroll, close stays reachable).
|
||
AC3 is a global contract — applies to ALL .modal users (BYOP, channels,
|
||
any future modal). Two patterns covered:
|
||
1. Sticky header pattern (.byop-modal): header pinned at top: 0 inside
|
||
the scrollable .modal; close is a child, no extra positioning needed.
|
||
2. Floating close pattern (.modal-close at top of .modal body):
|
||
position: sticky on the close button itself.
|
||
.ch-modal already uses position: absolute on a position: relative parent,
|
||
which keeps the close visually pinned across content scroll — that pattern
|
||
continues to satisfy AC3. */
|
||
.byop-modal .byop-header {
|
||
position: sticky; top: 0; z-index: 2;
|
||
background: var(--card-bg);
|
||
margin: -24px -24px 12px;
|
||
padding: 12px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||
}
|
||
/* MINOR-7: dropped position: sticky on .byop-x — it sits inside the
|
||
already-sticky .byop-header, so the sticky on the child was a no-op.
|
||
We only need visual styling here. */
|
||
.byop-modal .byop-x {
|
||
background: var(--card-bg); border: 1px solid var(--border);
|
||
border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 16px;
|
||
line-height: 1; color: var(--text);
|
||
}
|
||
.byop-modal .byop-x:hover { background: var(--row-hover, rgba(0,0,0,0.05)); }
|
||
|
||
/* MINOR-8: generic sticky-close for any .modal that uses a sibling .modal-close
|
||
inside the scrollable box (no enclosing sticky header). Excludes
|
||
.ch-modal-close which already uses position: absolute on a position:
|
||
relative .ch-modal — that pattern already keeps the close pinned across
|
||
internal scroll, so we don't override it. */
|
||
.modal > .modal-close:not(.ch-modal-close) {
|
||
position: sticky; top: 0; z-index: 3; float: right;
|
||
background: var(--card-bg); border: 1px solid var(--border);
|
||
border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 16px;
|
||
line-height: 1; color: var(--text);
|
||
}
|
||
.modal > .modal-close:not(.ch-modal-close):hover { background: var(--row-hover, rgba(0,0,0,0.05)); }
|
||
|
||
|
||
/* === Map Controls Panel === */
|
||
.map-controls {
|
||
/* #1059: fluid width via clamp() so controls scale with viewport but never
|
||
exceed available room or go below a usable minimum. */
|
||
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: clamp(160px, 18vw, 240px);
|
||
max-width: calc(100vw - 24px);
|
||
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 === */
|
||
/* #1057: Fluid layout. .ch-layout is a container query root so the channels
|
||
page can stack sidebar above main when the *available* layout width is
|
||
narrow (independent of the global viewport — handles cases where the
|
||
nav/side-pane consumes width). Sidebar uses clamp() for fluid sizing
|
||
between a usable min (~220px) and a comfortable max (~320px) on wide
|
||
screens; .ch-main keeps flex:1 so it absorbs remaining width including
|
||
ultrawide. */
|
||
.ch-layout {
|
||
display: flex; height: 100%; overflow: hidden;
|
||
container-type: inline-size;
|
||
container-name: chlayout;
|
||
}
|
||
.ch-sidebar {
|
||
width: clamp(220px, 22vw, 320px);
|
||
min-width: 220px;
|
||
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; }
|
||
.nav-stats { display: none; }
|
||
.brand-logo { height: 32px; width: 99px; }
|
||
.nav-link { padding: 14px 8px; font-size: 13px; }
|
||
.map-controls { width: 180px; font-size: 12px; }
|
||
}
|
||
|
||
/* #1057: Container query — stack the channels layout when the channels
|
||
page itself is narrow (e.g. on tablets, or when a side panel consumes
|
||
width). Below ~700px of *container* inline size, sidebar drops to full
|
||
width and stacks above the message area instead of squeezing it. */
|
||
@container chlayout (max-width: 700px) {
|
||
.ch-layout { flex-direction: column; }
|
||
.ch-sidebar {
|
||
width: 100%; min-width: 0;
|
||
max-height: 40%;
|
||
overflow-y: auto;
|
||
border-right: none;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.ch-sidebar-resize { display: none; }
|
||
.ch-main { width: 100%; }
|
||
}
|
||
|
||
/* === Nav Priority+ — JS-driven measurement (Issue #1102) ===
|
||
Above the mobile hamburger breakpoint we render ALL nav links inline
|
||
by default. A small JS pass in app.js (applyNavPriority) measures the
|
||
actual fit and adds .is-overflow to whichever links don't fit, then
|
||
moves them into the "More ▾" overflow menu. Pure CSS can't do this
|
||
correctly because we want every link visible at 2560px+ but only the
|
||
high-priority ones at 800px — and the breakpoint where each link
|
||
drops depends on its label width, the gutters, the active-link
|
||
padding, and the nav-stats badge. JS measurement is the only correct
|
||
answer. data-priority="high" links are pinned to the left via
|
||
order:-1 and are kept inline as long as possible (low-priority links
|
||
overflow first). Spacing/type still use #1054 fluid clamp() tokens. */
|
||
@media (min-width: 768px) {
|
||
.nav-links { display: flex !important; flex-direction: row; gap: var(--space-xs); }
|
||
.nav-more-wrap { display: flex; align-items: center; }
|
||
.hamburger { display: none; }
|
||
.nav-link { padding: 14px clamp(8px, 0.6vw + 4px, 14px); font-size: var(--fs-sm); }
|
||
.nav-links a[data-priority="high"] { order: -1; }
|
||
.nav-link.active { background: var(--nav-active-bg); border-radius: 6px; margin: 4px 0; padding: 10px clamp(8px, 0.6vw + 4px, 14px); }
|
||
/* JS-managed: links assigned .is-overflow get hidden inline; the
|
||
"More ▾" wrap is hidden when nothing overflows. */
|
||
.nav-links .nav-link.is-overflow { display: none; }
|
||
.nav-more-wrap.is-hidden { display: none !important; }
|
||
}
|
||
|
||
/* === Nav narrow-desktop tightening (Issue #1055 — 1024px overlap fix) ===
|
||
Between 768px and ~1100px, even with Priority+ collapsed to 5 high
|
||
links + "More ▾" + 4 nav-right buttons + nav-stats, the row doesn't
|
||
fit and the rightmost visible link overlaps nav-right by ~20px.
|
||
Hide nav-stats and tighten nav-link padding/gap in this band;
|
||
nav-stats reappears at ≥1101px where there is room for it.
|
||
NOTE: this block MUST follow the (min-width: 768px) Priority+ block
|
||
above so its tighter padding/gap rules win the cascade by source
|
||
order at 1024px (same specificity). */
|
||
@media (min-width: 768px) and (max-width: 1100px) {
|
||
.nav-stats { display: none; }
|
||
.nav-links { gap: 0; }
|
||
.nav-link { padding: 14px 6px; }
|
||
.nav-link.active { padding: 10px 6px; }
|
||
.nav-right { gap: 4px; }
|
||
}
|
||
|
||
/* === 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) {
|
||
.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: stack sidebar above message area on narrow viewports.
|
||
#1057 follow-up: container query alone was being overridden here by
|
||
the previous Discord-overlay rule (flex-direction:row + absolute
|
||
positioning), so the layout failed to stack at 480px. Use a plain
|
||
stacking layout that the E2E test recognizes (sidebar.bottom <=
|
||
main.top). */
|
||
.ch-layout { flex-direction: column; position: relative; }
|
||
.ch-sidebar {
|
||
width: 100%; min-width: 0; max-height: 40%; height: auto;
|
||
overflow-y: auto;
|
||
border-right: none; border-bottom: 1px solid var(--border);
|
||
background: var(--card-bg);
|
||
}
|
||
.ch-sidebar-resize { display: none; }
|
||
.ch-main {
|
||
width: 100%; flex: 1; min-height: 0;
|
||
background: var(--content-bg);
|
||
}
|
||
.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: var(--z-tooltip); 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: var(--z-dropdown);
|
||
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: var(--z-popover); 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: var(--z-dropdown); 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
|
||
* #1056 — fluid columns + +N hidden pill removes the need for a forced
|
||
* min-width; keep the wrapper for backward-compat selectors but let the
|
||
* inner table flex with the viewport. */
|
||
.obs-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||
.obs-table-scroll .obs-table { min-width: 0; }
|
||
|
||
/* #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: var(--z-dropdown);
|
||
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;
|
||
/* #1128 (Bug 3): cap trigger width so a long selection like
|
||
* "TRACE,MULTIPART,GRP_TXT" doesn't balloon the row and overlap toolbar
|
||
* buttons. The full label remains accessible via the title tooltip.
|
||
* #1131 MAJOR-4: use clamp() so the cap scales with viewport width
|
||
* instead of being a hard 180px on every screen size. */
|
||
max-width: clamp(120px, 18vw, 280px); overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.multi-select-trigger:hover { border-color: var(--accent); color: var(--accent); }
|
||
.multi-select-menu {
|
||
position: absolute; top: 100%; left: 0; z-index: var(--z-dropdown);
|
||
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: var(--z-modal);
|
||
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: var(--z-tooltip);
|
||
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 (legacy; #1056 uses fluid cols) */
|
||
.table-scroll-wrap {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
@media (max-width: 640px) {
|
||
/* #1056: do NOT enforce min-width on primary fluid tables — column hiding
|
||
* via JS keeps content readable without horizontal scroll. */
|
||
.data-table { min-width: 0; }
|
||
}
|
||
|
||
/* 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: var(--z-modal); padding: 0; }
|
||
/* #1122: when help is opened inside .modal-overlay (real modal/backdrop),
|
||
reset the absolute positioning so the flex-centered overlay places it.
|
||
Also neutralise .modal default padding so the sticky header sits flush. */
|
||
.modal-overlay > .fux-popover { position: relative; top: auto; right: auto; left: auto; bottom: auto; padding: 0; max-width: min(720px, 92vw); width: min(720px, 92vw); }
|
||
.modal.fux-popover { padding: 0; }
|
||
.fux-help-overlay { z-index: var(--z-modal); }
|
||
/* #1124: keep the help modal inside the viewport. The backdrop alone is
|
||
* enough visual separation; rows underneath stay rendered. */
|
||
.modal-overlay > .fux-popover { max-height: 80vh; overflow-y: auto; }
|
||
|
||
/* #1124 (MAJOR-1): overflow pill rendered when path chips exceed the
|
||
* row's height cap. Visual style mirrors the #1056 TableResponsive pill. */
|
||
.path-overflow-pill {
|
||
display: inline-flex; align-items: center;
|
||
flex: 0 0 auto;
|
||
margin-left: 4px;
|
||
padding: 1px 6px;
|
||
font-family: var(--mono); font-size: 10px; font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.path-overflow-pill:hover { color: var(--text); background: var(--bg-hover, var(--surface)); }
|
||
.path-popover {
|
||
position: absolute; z-index: var(--z-popover);
|
||
background: var(--surface); color: var(--text);
|
||
border: 1px solid var(--border); border-radius: 6px;
|
||
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
||
padding: 8px 10px; max-width: min(360px, calc(100vw - 32px)); max-height: 240px; overflow: auto;
|
||
font-family: var(--mono); font-size: 12px;
|
||
}
|
||
.path-popover .path-popover-title { font-family: var(--font); font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
|
||
.path-popover .hop, .path-popover .hop-named { white-space: nowrap; }
|
||
.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: var(--z-dropdown); 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: var(--z-tooltip); 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: var(--z-dropdown); 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; }
|