Files
meshcore-analyzer/public/style.css
T
Kpa-clawbot cfd1903c6b fix(logo): default sage/teal brand colors, customizer mirrors accent (#1162)
## Summary

Restores sage/teal as default logo colors while preserving customizer
theming. Closes the gap from #1157 (closed) and the user's complaint
about lost two-tone.

Out-of-the-box, the navbar + hero CORE/SCOPE wordmarks now render the
brand-identity duotone — `#cfd9c9` (sage / fog) and `#2c8c8c` (teal /
water). When an operator picks a theme via the customizer (or sets a
custom accent color), the wordmark recolors to follow.

## Approach (Option C — decoupled defaults + customizer mirror)

- **`public/style.css` `:root`** — set `--logo-accent: #cfd9c9` and
`--logo-accent-hi: #2c8c8c` as literal defaults. Removes the previous
`var(--accent)` cascade so blue-by-default no longer leaks into the
brand mark.
- **`public/customize-v2.js`** — `applyTheme()`, the early-apply path,
and the live color-picker `input` handler now mirror
`themeSection.accent` → `--logo-accent` and `themeSection.accentHover` →
`--logo-accent-hi`.
- **`public/customize.js`** (legacy) — same mirroring in
`applyThemePreview()` and the early localStorage replay.
- **`.github/workflows/deploy.yml`** — adds the new e2e to the Chromium
batch.

This preserves `--accent` as the canonical app-wide accent token (no
other UI changes) while giving the logo its own brand-defaulted tokens
that the customizer still drives.

## Tests

Red → green commit pair on the branch.

- **NEW: `test-logo-default-sage-teal-e2e.js`** — gates both halves of
the contract:
1. Clean localStorage → navbar + hero CORE = `rgb(207, 217, 201)`, SCOPE
= `rgb(44, 140, 140)`.
2. Seeded `cs-theme-overrides` with red accent → navbar + hero recolor
to red.
- **UPDATED: `test-logo-theme-e2e.js`** — replaces the old "must NOT be
sage" sentinel (sage was a regression marker; it's now the brand
default) with a theme-reactivity probe that overrides `--logo-accent` /
`--logo-accent-hi` directly and asserts the wordmark fill changes.
Duotone, mobile-fit, and clip checks are unchanged.

## Verification

- Default load: sage CORE + teal SCOPE in navbar AND hero ✔ (asserted by
step 1 of the new e2e).
- Customizer override: wordmark follows `accent` / `accentHover` ✔
(asserted by step 2 of the new e2e + the theme-reactivity probe in
`test-logo-theme-e2e.js`).
- Preflight: all hard gates green (PII, branch scope, red commit,
CSS-var defined, CSS self-fallback, LIKE-on-JSON, sync migration); all
warnings green.

## Browser verified

E2E assertion added: `test-logo-default-sage-teal-e2e.js:73` (default
sage), `test-logo-default-sage-teal-e2e.js:124` (customizer override).
CI runs both via `deploy.yml:243`.

Browser verified: covered by Chromium e2e against
`http://localhost:13581` in CI; staging URL TBD on merge.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-07 08:29:02 -07:00

3241 lines
142 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* === CoreScope — style.css === */
/* Aldrich webfont — used by the navbar logo SVG (issue #1137 follow-up).
* Self-hosted woff2 (latin subset from Google Fonts, ~16KB). Only weight
* available is 400; the SVG's font-weight="700" synthesizes bold. */
@font-face {
font-family: 'Aldrich';
src: url('/fonts/aldrich-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* ============================================================
* 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 + brand-default follow-up) -----------
* 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.
* --logo-text → wordmark "CORE" / "SCOPE" / labels
* --logo-accent → primary node + left-side arcs (default sage)
* --logo-accent-hi → secondary node + right-side arcs (default teal)
* --logo-muted → tagline + sine wave
* Sage (#cfd9c9 — fog) and teal (#2c8c8c — water) are the OUT-OF-BOX
* brand identity. They are NOT cascaded from --accent any more, so
* changing the app accent color via dev tools alone will NOT recolor
* the logo. The customizer (public/customize-v2.js) mirrors --accent
* and --accent-hover into --logo-accent / --logo-accent-hi when an
* operator picks a theme, preserving full theme-driven recoloring.
* 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: #cfd9c9;
--logo-accent-hi: #2c8c8c;
--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 10801279px overflow
window AND the 12801599px 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 to match the SVG intrinsic width (#1141 followup: viewBox
widened to 150 10 970 280 to fit CORE/SCOPE wordmark; intrinsic
width 125 keeps the text legible at the same ~36px height). */
width: 125px;
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);
}
/* Mark-only navbar logo: hidden by default, swapped in for the full
wordmark logo at narrow viewports where SCOPE would otherwise clip
(#1137 mobile width pin was 99px → SCOPE→SCOF at ≤390px). */
.brand-mark-only {
display: none;
height: 32px;
width: auto;
}
.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);
}
/* #1146: "Paths Through This Node" entries render as <div> blocks, not
tables, so the rule above doesn't reach them. Without this, the path-hop
<a> elements inherit the UA-default rgb(0,0,238) blue, which on the dark
card surface (--card-bg: #1a1a2e) computes to ~1.8-3.0:1 — well below
the 4.5:1 WCAG AA body-text minimum. Use --accent so the link tracks the
active theme (and customizer overrides) like the data-table links do. */
.node-detail-section #pathsContent a,
.node-full-card #fullPathsContent a {
color: var(--accent);
}
.node-detail-section #pathsContent a:hover,
.node-full-card #fullPathsContent a:hover {
color: var(--accent-hover);
}
.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: 112px; }
.nav-link { padding: 14px 8px; font-size: 13px; }
.map-controls { width: 180px; font-size: 12px; }
}
/* === Responsive — Mobile (≤400px) ===
At narrow viewports the inline wordmark SVG (intrinsic 111px content)
clips when squeezed into the 99px tablet pin (#1137 → SCOPE→SCOF).
Swap to the dedicated .brand-mark-only inline SVG (arcs+dots only). */
@media (max-width: 400px) {
.brand-logo { display: none; }
.brand-mark-only { display: block; }
}
/* #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; }