mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-04 01:51:20 +00:00
40aa02b438
Red commit:c0de33a952(CI: https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416117686) Green commit:c268248d— CI: https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416069319 ## What Fix #1360 regression: cluster role pills on `/map` show ONLY the role letter (R/C/M/S/O); the per-role count number that was visible pre-#1357 is gone. This PR restores the count by concatenating it after the letter inside the pill body, so each pill renders as `R60`, `C30`, `M5`, etc. - `public/map.js` `makeClusterIcon`: pill body becomes `letter + n` (was `letter`). - `aria-label` / `title` (`"60 repeaters"`) untouched — already correct. - DOM, classes, CSS, `--mc-*` constants, border-style ramp, multi-byte labels — untouched. ### Adversarial follow-up (commit on top of green) - **JS cap**: `makeClusterIcon` clamps `n > 999` → `"999+"`, so pathological clusters render as e.g. `R999+` instead of `R10000`. Pill width stays bounded. - **CSS guard** on `.mc-pill`: `max-width: 4ch; overflow: hidden; text-overflow: ellipsis;` as defense-in-depth if a render slips past the JS cap. - **+3 test assertions**: one for the JS cap, two for the CSS guard. Mutation-verified (removing the cap fails ONLY the new cap assertion). ## Why #1357 fixed WCAG 1.4.1 for cluster role pills by promoting the role letter to the pill body, but in doing so dropped the count number that sighted operators relied on for at-a-glance per-role counts. The letter is the WCAG carrier; the count is the data. Both belong in the pill body — they always did before #1357. The audit's intent was to PAIR them, not REPLACE one with the other. ## TDD red→green - **Red** (`c0de33a9`): added `test-issue-1360-pill-letter-count.js` with assertions that pill body concatenates `letter + n` and is no longer the bare `letter`. Fails by assertion against current `master`. Red CI: https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416117686 - **Green** (`c268248d`): one-line change in `public/map.js` (`letter + '</span>'` → `letter + n + '</span>'`). All assertions pass. Green CI: https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416069319 - **Follow-up** (this push): JS `"999+"` cap + CSS width guard + 3 new assertions. #1356 (40), #1293, and `marker-outline-weight` tests remain green. - New test wired into `.github/workflows/deploy.yml` right after `test-issue-1356-map-a11y.js`. ## Visual verification Open https://analyzer.00id.net/#/map after deploy and confirm cluster pills display `R<count>`, `C<count>`, `M<count>`, etc. (e.g. `R60 C30 M5`) instead of bare letters. `aria-label="60 repeaters"` remains for screen readers. For very large clusters, pills cap at `R999+` / `C999+` etc. Fixes #1360 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: CoreScope Bot <bot@corescope>
3877 lines
166 KiB
CSS
3877 lines
166 KiB
CSS
/* === 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 1080–1279px overflow
|
||
window AND the 1280–1599px overlap window where the full link strip
|
||
ran underneath .nav-right. Spacing and type use the #1054 fluid
|
||
clamp() tokens so the bar scales smoothly across viewport widths.
|
||
The hamburger and stacked mobile layout still take over at <768px. */
|
||
.top-nav {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
background: linear-gradient(135deg, var(--nav-bg) 0%, var(--nav-bg2) 100%); color: var(--nav-text);
|
||
padding: 0 var(--gutter); height: 52px;
|
||
position: sticky; top: 0; z-index: 1100;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,.3);
|
||
flex-wrap: nowrap; overflow: hidden; min-width: 0;
|
||
}
|
||
.nav-left { display: flex; align-items: center; gap: var(--space-lg); min-width: 0; flex-shrink: 1; overflow: hidden; }
|
||
.nav-brand { display: flex; align-items: center; gap: var(--space-sm); text-decoration: none; color: var(--nav-text); font-weight: 700; font-size: var(--fs-md); }
|
||
.brand-icon { font-size: 20px; }
|
||
.brand-logo {
|
||
display: block;
|
||
height: 36px;
|
||
/* Width matches the inline SVG's effective content aspect (~3.08:1)
|
||
after cropping to the wordmark+nodes region in the navbar viewBox.
|
||
/* Pinned 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;
|
||
}
|
||
/* #1173: legacy navbar `.live-dot` indicator removed — WS state is now
|
||
encoded in the brand-logo itself (.logo-disconnected + packet-driven
|
||
.logo-pulse-active). The navbar element is gone from index.html, so we
|
||
don't ship a `display: none` shell here — `live.js` reuses `.live-dot`
|
||
for legend bullets (styled by live.css) and a global no-op rule would
|
||
wipe those out. If a stale extension/customizer ever injects a navbar
|
||
`.live-dot`, it's a harmless 0×0 unstyled span. */
|
||
/* #1173: brand-logo packet-pulse animation.
|
||
- Class toggles only — pure CSS animation. JS adds/removes:
|
||
.logo-pulse-active (chained ping, source then destination)
|
||
.logo-pulse-blip (reduced-motion fallback, destination only)
|
||
.logo-disconnected (sustained desaturate while WS down)
|
||
- All color comes from --logo-accent / --logo-accent-hi tokens.
|
||
Animation only tweaks filter: brightness() + transform: scale(). */
|
||
.brand-logo circle.logo-node-a,
|
||
.brand-logo circle.logo-node-b,
|
||
.brand-mark-only circle.logo-node-a,
|
||
.brand-mark-only circle.logo-node-b {
|
||
transform-box: fill-box;
|
||
transform-origin: center;
|
||
transition: filter .12s ease-out;
|
||
will-change: filter, transform;
|
||
}
|
||
@keyframes logo-pulse-step {
|
||
0% { filter: brightness(1); transform: scale(1); }
|
||
40% { filter: brightness(1.6); transform: scale(1.18); }
|
||
100% { filter: brightness(1); transform: scale(1); }
|
||
}
|
||
.brand-logo circle.logo-pulse-active,
|
||
.brand-mark-only circle.logo-pulse-active {
|
||
animation: logo-pulse-step 80ms ease-out;
|
||
}
|
||
@keyframes logo-pulse-blip-step {
|
||
0% { opacity: 1; }
|
||
50% { opacity: 0.55; }
|
||
100% { opacity: 1; }
|
||
}
|
||
.brand-logo circle.logo-pulse-blip,
|
||
.brand-mark-only circle.logo-pulse-blip {
|
||
animation: logo-pulse-blip-step 140ms linear;
|
||
}
|
||
.brand-logo.logo-disconnected,
|
||
.brand-mark-only.logo-disconnected {
|
||
filter: grayscale(0.6) opacity(0.7);
|
||
transition: filter .25s ease-out;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.brand-logo circle.logo-pulse-active,
|
||
.brand-mark-only circle.logo-pulse-active {
|
||
/* Defensive: in case anything mistakenly toggles the chained class
|
||
under reduced-motion, neutralize the scale animation. */
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
.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); }
|
||
/* #1174 mesh-op review: subtract --bottom-nav-reserve so /map and the
|
||
* other fixed-height pages (packets, nodes, channels, audio-lab) do not
|
||
* have their last 56px covered by the bottom-nav at ≤768. The token is
|
||
* 0px at desktop so wide-viewport behavior is unchanged. */
|
||
#app.app-fixed { height: calc(100vh - 52px - var(--bottom-nav-reserve, 0px)); height: calc(100dvh - 52px - var(--bottom-nav-reserve, 0px)); 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; min-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; min-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; height: 34px; min-height: 34px; box-sizing: border-box; line-height: 1; }
|
||
.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); }
|
||
.filter-bar .col-toggle-btn { height: 34px; min-height: 34px; }
|
||
|
||
.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; }
|
||
|
||
/* === #1056 AC#4: Slide-over row-detail overlay (narrow viewports) ========
|
||
* Singleton .slide-over-backdrop + .slide-over-panel injected into <body>
|
||
* by SlideOver helper in packets.js. Used by Packets/Nodes/Observers row
|
||
* clicks at viewport <=1023 instead of routing to a separate page.
|
||
* Reuses the existing slideInRight keyframe defined later in this file.
|
||
*/
|
||
|
||
/* #1168 Munger #3: ref-counted scroll-lock. Multiple modal surfaces
|
||
* (SlideOver, ChannelColorPicker, future modals) acquire/release a
|
||
* shared count via window.__scrollLock; the class is added on first
|
||
* acquire and removed on last release. Replaces the previous
|
||
* body.style.overflow capture-and-restore string approach which
|
||
* corrupted overflow under last-writer-wins races. */
|
||
body.scroll-locked { overflow: hidden; }
|
||
.slide-over-backdrop {
|
||
position: fixed; inset: 0; z-index: 1000;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
cursor: pointer;
|
||
}
|
||
.slide-over-backdrop[hidden] { display: none; }
|
||
.slide-over-panel {
|
||
position: fixed; top: 0; right: 0; bottom: 0;
|
||
width: min(480px, 90vw);
|
||
z-index: 1001;
|
||
background: var(--detail-bg, var(--card-bg, #1f2937));
|
||
color: var(--text, inherit);
|
||
border-left: 1px solid var(--border);
|
||
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.35);
|
||
overflow-y: auto; overflow-x: hidden;
|
||
display: flex; flex-direction: column;
|
||
animation: slideInRight 200ms ease-out;
|
||
}
|
||
.slide-over-panel[hidden] { display: none; }
|
||
.slide-over-header {
|
||
position: sticky; top: 0; z-index: 2;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 8px; padding: 10px 12px;
|
||
background: var(--card-bg, var(--detail-bg));
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.slide-over-title {
|
||
margin: 0; font-size: 14px; font-weight: 600;
|
||
color: var(--text, inherit);
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.slide-over-close {
|
||
background: none; border: none; cursor: pointer;
|
||
font-size: 18px; line-height: 1;
|
||
color: var(--text-muted, #94a3b8);
|
||
padding: 4px 8px; border-radius: 4px;
|
||
/* WCAG 2.5.5 / Apple HIG — 48px tap target on touch devices. */
|
||
min-width: 44px; min-height: 44px;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
}
|
||
.slide-over-close:hover { color: var(--text, inherit); background: var(--row-hover); }
|
||
.slide-over-close:focus-visible { outline: 2px solid var(--accent, #60a5fa); outline-offset: 2px; }
|
||
.slide-over-content { flex: 1; min-height: 0; padding: 12px; }
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.slide-over-panel { animation: none; }
|
||
}
|
||
|
||
.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;
|
||
}
|
||
/* Observer IATA pill rendered inline next to observer name on packets (#1188).
|
||
* Visually similar to .badge-region but distinct so the row badge and the
|
||
* inline-with-observer badge can be styled independently in future themes. */
|
||
.badge-iata {
|
||
display: inline-block; padding: 1px 5px; border-radius: 4px;
|
||
font-size: 10px; font-weight: 700; font-family: var(--mono);
|
||
background: var(--nav-bg); color: var(--nav-text); letter-spacing: .5px;
|
||
margin-left: 4px; vertical-align: middle;
|
||
}
|
||
/* 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; }
|
||
/* #1281: 📍map link inside detail-meta — UA-default blue is unreadable on
|
||
* dark backgrounds. Force theme-aware color via --accent. */
|
||
.loc-map-link { color: var(--accent); font-size: 0.85em; text-decoration: none; }
|
||
.loc-map-link:hover { text-decoration: underline; }
|
||
.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;
|
||
/* #1236: keep the scrollbar gutter reserved so the scroll affordance is
|
||
visible even when content briefly fits — users get a consistent visual
|
||
cue that the panel is scrollable. */
|
||
scrollbar-gutter: stable;
|
||
}
|
||
/* #1236: sticky panel title so the scroll affordance is always visible at the
|
||
top of the scroll container when content overflows on small viewports. */
|
||
.map-controls h3 {
|
||
font-size: 15px;
|
||
margin: -14px -16px 10px -16px;
|
||
padding: 10px 16px;
|
||
position: sticky;
|
||
top: -14px;
|
||
background: var(--card-bg);
|
||
z-index: 2;
|
||
border-bottom: 1px solid var(--border);
|
||
border-radius: 10px 10px 0 0;
|
||
}
|
||
.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: 8px 12px; border-bottom: 1px solid var(--border);
|
||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
||
}
|
||
.ch-sidebar-title {
|
||
display: flex; align-items: center; gap: 6px;
|
||
font-size: 15px; font-weight: 700;
|
||
flex: 1 1 auto; min-width: 0;
|
||
/* Name dominates; no longer push other controls onto a new row */
|
||
margin: 0;
|
||
}
|
||
.ch-header-region { margin: 0; padding: 0; flex: 0 0 auto; }
|
||
.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);
|
||
height: 34px; min-height: 34px; box-sizing: border-box; line-height: 1;
|
||
}
|
||
|
||
/* 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 {
|
||
/* Issue #1109: was position:absolute. The containing block becomes
|
||
.top-nav (the nearest positioned ancestor: position:sticky), which
|
||
has `overflow:hidden; height:52px` since #1066 to guard against
|
||
horizontal overflow during the Priority+ measurement pass. That
|
||
guard clipped this dropdown invisibly below the navbar. Switching
|
||
to position:fixed escapes any overflow:hidden ancestor (containing
|
||
block becomes the viewport) without relaxing the #1066 desktop
|
||
overflow guard. */
|
||
display: none; position: fixed; 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; }
|
||
|
||
/* #1224 Channels mobile UX: compact header, dominant channel name,
|
||
icon-only row actions, smaller empty state. */
|
||
.ch-sidebar-header {
|
||
padding: 6px 10px; gap: 6px;
|
||
max-height: 56px;
|
||
}
|
||
.ch-sidebar-title { font-size: 14px; }
|
||
.ch-sidebar-title .ch-icon { font-size: 16px; }
|
||
.ch-add-channel-btn { padding: 4px 8px; font-size: 12px; }
|
||
.ch-analytics-link { padding: 3px 7px; font-size: 13px; }
|
||
/* Region filter shows just its compact pills/dropdown trigger in the header. */
|
||
.ch-header-region .region-filter-bar { padding: 0; gap: 4px; }
|
||
.ch-header-region .region-pill { padding: 2px 6px; font-size: 11px; }
|
||
.ch-header-region .region-dropdown-trigger { padding: 3px 6px; font-size: 11px; }
|
||
|
||
/* Channel list row: name must dominate. The inline share/remove icons
|
||
were eating >88px because of their 44x44 touch target — push them
|
||
onto a per-row reveal pattern: show a single ⋮ trigger that toggles
|
||
the action set. Until then, shrink them to icon-only and let the
|
||
name grow via flex:1. */
|
||
#chList .ch-item { padding: 10px 12px; gap: 10px; }
|
||
#chList .ch-item-name { flex: 1 1 auto; min-width: 0; }
|
||
#chList .ch-item-top { gap: 6px; }
|
||
#chList .ch-icon-btn { min-width: 32px; min-height: 32px; padding: 2px 4px; }
|
||
#chList .ch-share-btn { font-size: 0; }
|
||
#chList .ch-share-btn::before { content: '📤'; font-size: 13px; }
|
||
#chList .ch-remove-btn { font-size: 12px; padding: 2px 6px; }
|
||
/* Hide the analytics-style time on mobile rows to free width for the name. */
|
||
#chList .ch-item-time { font-size: 10px; }
|
||
|
||
/* Empty state must not dominate. */
|
||
.ch-empty { padding: 12px; font-size: 13px; max-height: 30vh; }
|
||
|
||
/* 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 — #1329: drop fixed 200px cap, use accordion sections
|
||
instead so visible content fits without internal scrolling. Panel can
|
||
grow to fill available height; max-height bound by viewport so it
|
||
never escapes the screen. */
|
||
.map-controls { width: calc(100vw - 24px); right: 12px; top: 8px; max-height: calc(100vh - 80px); font-size: 12px; padding: 10px 12px; }
|
||
/* On mobile, hide collapsed section bodies (everything inside the
|
||
fieldset except the legend). The legend remains tappable to expand. */
|
||
.map-controls fieldset.mc-section.mc-collapsed > *:not(legend) { display: none; }
|
||
.map-controls fieldset.mc-section > legend.mc-label {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 6px 0;
|
||
}
|
||
/* ▸ / ▾ indicator via ::after so we don't touch markup */
|
||
.map-controls fieldset.mc-section > legend.mc-label::after {
|
||
content: '▾';
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
margin-left: 8px;
|
||
transition: transform 0.15s;
|
||
}
|
||
.map-controls fieldset.mc-section.mc-collapsed > legend.mc-label::after {
|
||
content: '▸';
|
||
}
|
||
#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: inline-flex; align-items: center; justify-content: center;
|
||
padding: 4px 8px;
|
||
font-size: 14px;
|
||
text-decoration: none;
|
||
color: var(--text-muted);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
flex: 0 0 auto;
|
||
}
|
||
.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; }
|
||
/* #1253: trim .badge-iata h-padding + margin-left on narrow viewports so
|
||
* the observer cell + IATA pill fits within a 375px viewport. Without this
|
||
* the badge right edge overflows by ~1.25px at 375px (same class of fix
|
||
* as #1250/#1251 VCR LCD-clip). Badge stays fully visible — only padding
|
||
* and side margin shrink; font-size, colors (via vars) unchanged. */
|
||
.badge-iata { padding: 1px 3px; margin-left: 2px; }
|
||
}
|
||
|
||
/* 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; }
|
||
/* #1058 — Analytics rows: fluid + auto-stacking via CSS Grid `auto-fit`.
|
||
Cards stack automatically when the row can't fit two columns at the
|
||
400px-floor minimum (avoids cramped charts), and otherwise lay out
|
||
side-by-side. Replaces the prior `flex` + `@media (max-width:640px)`
|
||
approach so behavior is driven by available width — not viewport —
|
||
matching #1050's fluid-everywhere mandate. `min(100%, 400px)` keeps
|
||
the track from overflowing at very narrow widths. */
|
||
.analytics-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.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) {
|
||
.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; height: auto; 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; height: auto; 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; align-self: flex-start; 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: 8px; }
|
||
.node-top-row .node-qr-wrap .node-qr { margin-top: 0; }
|
||
.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;
|
||
}
|
||
/* #1243 — On mobile, mirror the desktop split-pane behavior used by the
|
||
compact node detail (.node-map-qr-wrap): overlay the QR on the map
|
||
instead of stacking a 250px-tall QR panel below it. Desktop (≥768px)
|
||
keeps the side-by-side flex layout untouched. */
|
||
.node-top-row { position: relative; }
|
||
.node-top-row .node-map-wrap { flex: 1 1 100%; }
|
||
.node-top-row .node-qr-wrap {
|
||
position: absolute;
|
||
bottom: 8px;
|
||
right: 8px;
|
||
z-index: 400;
|
||
flex: 0 0 auto;
|
||
min-width: 0;
|
||
max-width: 96px;
|
||
padding: 4px;
|
||
background: var(--card-bg-translucent, rgba(255, 255, 255, 0.85));
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
line-height: 0;
|
||
margin: 0;
|
||
}
|
||
.node-top-row .node-qr-wrap .node-qr svg { max-width: 72px; height: auto; }
|
||
/* Hide the redundant pubkey caption inside the overlay QR — it's already
|
||
shown in the card above and would push the overlay too large. */
|
||
.node-top-row .node-qr-wrap .mono { display: none; }
|
||
[data-theme="dark"] .node-top-row .node-qr-wrap {
|
||
background: var(--card-bg-translucent-dark, rgba(255, 255, 255, 0.4));
|
||
}
|
||
}
|
||
|
||
.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 / Area filter bars ─── */
|
||
.region-filter-bar { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 0; }
|
||
.area-dropdown-menu { min-width: 160px; }
|
||
.area-dropdown-item.area-item-active { color: var(--accent); font-weight: 600; }
|
||
.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; white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
.region-pill:hover { border-color: var(--accent); color: var(--accent); }
|
||
button.region-pill-active {
|
||
background: var(--accent); color: #fff; border-color: var(--accent);
|
||
}
|
||
button.region-pill-active:hover { opacity: 0.85; color: #fff; }
|
||
.region-filter-label {
|
||
font-size: 12px; font-weight: 600; color: var(--text-muted); align-self: center;
|
||
margin-right: 2px; user-select: none; white-space: nowrap; flex-shrink: 0;
|
||
}
|
||
.analytics-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 8px 0; }
|
||
.analytics-time-window-select {
|
||
height: 34px; min-height: 34px; padding: 0 10px; border-radius: 6px; box-sizing: border-box;
|
||
font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border);
|
||
background: var(--input-bg); color: var(--text); transition: border-color 0.15s;
|
||
}
|
||
.analytics-time-window-select:hover { border-color: var(--accent); }
|
||
.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 (#1205 R1) — keep matrix text/accent color
|
||
only. Background + border are owned by the parent #liveHeader (MESH
|
||
LIVE panel) now that .live-controls is in-flow and transparent;
|
||
re-painting an opaque box here would orphan a nested chrome rect
|
||
inside the header for matrix-theme users. */
|
||
.matrix-theme .live-controls {
|
||
background: transparent !important;
|
||
border: 0 !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; }
|
||
/* #1236: on mobile the path-inspector side pane was eating 32px of horizontal
|
||
width (flex:0 0 32px) inside #map-wrap, leaving an unused gutter on the
|
||
right of the leaflet canvas. Path Inspector is a desktop-only convenience;
|
||
hide the pane on narrow viewports so the map fills 100% of the viewport. */
|
||
@media (max-width: 640px) {
|
||
.map-side-pane { display: none; }
|
||
}
|
||
|
||
/* 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, a11y refit issue #1356) ──
|
||
*
|
||
* #1356 WCAG 2.2 AA refit — Tufte structural framing + audit minimal patch.
|
||
* Design source: github.com/Kpa-clawbot/CoreScope/issues/1356 (Tufte + audit comments).
|
||
*
|
||
* Carriers (NON-color) of meaning, per WCAG 1.4.1:
|
||
* - V1 cluster bubbles: size (40/48/56px) + numeral + border-style ramp
|
||
* (1.5px solid / 2.5px solid / 2px double). Fill is a single neutral.
|
||
* - V2 role pills: capital-letter prefix (R/C/M/S/O). Wong (2011) palette
|
||
* hue is secondary. Dark text (#1a1a1a) on ALL five pills (audit override
|
||
* so only ONE text-color rule is needed and every pill passes 4.5:1).
|
||
* - V3 multi-byte hash labels: unicode glyph prefix (✓/?/✗) + neutral fill
|
||
* + 3px colored left-border using the audit's high-luminance accent set
|
||
* (NOT Tol "vibrant" — those failed 3:1 vs the neutral fill).
|
||
*
|
||
* Constants are --mc-* namespaced. The reserved --info / --warning / --accent
|
||
* system vars are NOT touched (per issue scope + AGENTS.md).
|
||
*/
|
||
:root {
|
||
/* V1 — cluster bubble */
|
||
--mc-cluster-fill: rgba(33, 41, 54, 0.88);
|
||
--mc-cluster-text: #ffffff;
|
||
--mc-cluster-border: #666666; /* audit: white border = 1.05:1 vs Carto-light; #666 = 4.83:1 */
|
||
|
||
/* V2 — role pills (Wong 2011 colorblind-safe palette) */
|
||
--mc-role-repeater: #D55E00; /* vermillion */
|
||
--mc-role-companion: #56B4E9; /* sky blue */
|
||
--mc-role-room: #009E73; /* bluish-green */
|
||
--mc-role-sensor: #F0E442; /* yellow */
|
||
--mc-role-observer: #CC79A7; /* reddish-purple */
|
||
|
||
/* V3 — multi-byte hash labels (neutral fill + high-luminance accent stripes) */
|
||
--mc-mb-fill: rgba(33, 41, 54, 0.92);
|
||
--mc-mb-text: #ffffff;
|
||
--mc-mb-confirmed: #56F0A0; /* audit override of Tol vibrant for fill contrast */
|
||
--mc-mb-suspected: #FFD966;
|
||
--mc-mb-unknown: #FF8888;
|
||
}
|
||
|
||
.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);
|
||
background: var(--mc-cluster-fill);
|
||
color: var(--mc-cluster-text); text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
||
border: 2px solid var(--mc-cluster-border);
|
||
/* Dark halo + soft shadow — audit fix so the border edge is visible vs Carto-light */
|
||
box-shadow: 0 0 0 1px rgba(0,0,0,0.5), 0 1px 2px rgba(0,0,0,0.35);
|
||
cursor: pointer;
|
||
transition: transform 120ms ease;
|
||
}
|
||
.mc-cluster:hover { transform: scale(1.06); }
|
||
/* Border-style ramp is the redundant non-color carrier of the count bucket. */
|
||
.mc-cluster.mc-sm { width: 40px; height: 40px; border-width: 1.5px; border-style: solid; }
|
||
.mc-cluster.mc-md { width: 48px; height: 48px; border-width: 2.5px; border-style: solid; }
|
||
.mc-cluster.mc-lg { width: 56px; height: 56px; border-width: 2px; border-style: double; }
|
||
.mc-cluster .mc-count { font-size: 0.875rem; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
|
||
.mc-cluster.mc-lg .mc-count { font-size: 1rem; }
|
||
.mc-cluster .mc-pills {
|
||
display: flex; gap: 2px; margin-top: 3px;
|
||
}
|
||
.mc-cluster .mc-pill {
|
||
display: inline-block; min-width: 12px; padding: 1px 3px;
|
||
border-radius: 3px;
|
||
/* #1360 follow-up: defense-in-depth cap for pathological 4+ digit counts.
|
||
JS caps the rendered text at "999+" (max 4 chars); this bounds visual
|
||
width if a stray render slips through. */
|
||
max-width: 4ch; overflow: hidden; text-overflow: ellipsis;
|
||
/* Audit: bump 9px → 10px, monospace, dark text on every Wong hue.
|
||
#1a1a1a on all 5 Wong hues passes SC 1.4.3 small-text (≥4.5:1).
|
||
Sized in rem (0.625rem = 10px @ default 16px root) so user
|
||
font-size preferences scale the pill (SC 1.4.4 Resize Text 200%). */
|
||
font: 700 0.625rem/1.1 ui-monospace, "SF Mono", Consolas, monospace;
|
||
letter-spacing: 0;
|
||
color: #1a1a1a; text-align: center; text-shadow: none;
|
||
border: 1px solid rgba(0,0,0,0.25);
|
||
/* #1360: overflow:hidden + text-overflow:ellipsis above bound the pill
|
||
when counts approach the 4-char cap ("999+"). Acceptable tradeoff vs.
|
||
SC 1.4.12 letter-spacing clipping: text content is the role letter +
|
||
<=4 digits, far short of needing aggressive letter-spacing overrides. */
|
||
}
|
||
|
||
/* V3 — multi-byte hash labels: neutral fill + colored 3px left border */
|
||
.mc-mb-label {
|
||
background: var(--mc-mb-fill);
|
||
color: var(--mc-mb-text);
|
||
/* Sized in rem (0.75rem = 12px @ default root) so user font-size
|
||
preferences scale the label per SC 1.4.4 Resize Text 200%. */
|
||
font: 600 0.75rem/1.2 ui-monospace, "SF Mono", Consolas, monospace;
|
||
letter-spacing: 0.02em;
|
||
padding: 2px 5px 2px 4px;
|
||
border-left: 3px solid transparent;
|
||
border-radius: 2px;
|
||
box-shadow: 0 0 0 1px rgba(0,0,0,0.5), 0 1px 2px rgba(0,0,0,0.35);
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
line-height: 1.2;
|
||
}
|
||
.mc-mb-label.status-confirmed { border-left-color: var(--mc-mb-confirmed); }
|
||
.mc-mb-label.status-suspected { border-left-color: var(--mc-mb-suspected); }
|
||
.mc-mb-label.status-unknown { border-left-color: var(--mc-mb-unknown); }
|
||
|
||
/* Forced-colors / Windows High Contrast — degrade gracefully (audit item 7). */
|
||
@media (forced-colors: active) {
|
||
.mc-cluster, .mc-pill, .mc-mb-label {
|
||
forced-color-adjust: auto;
|
||
background: Canvas;
|
||
color: CanvasText;
|
||
border-color: CanvasText;
|
||
}
|
||
}
|
||
|
||
/* === #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; }
|
||
|
||
/* === Issue #1064 — Edge-swipe nav drawer ============================
|
||
* Slide-over from the LEFT edge. Z-index sits ABOVE bottom-nav (1200)
|
||
* but BELOW modal (var(--z-modal) = 9100). Fenced for parallel-PR
|
||
* coordination with #1062 (gesture handlers / swipe-affordance section).
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/* `pan-y` lets vertical scroll work everywhere; the drawer claims
|
||
* horizontal swipes inside our viewport. iOS browser back-swipe (system
|
||
* left-edge gesture) still works — it's at the OS layer, above the page.
|
||
*
|
||
* Mesh-Op review (PR #1184): scope this to wide viewports only. At
|
||
* ≤768px the drawer is `display:none` (Option A), so this rule does
|
||
* nothing useful and would block horizontal panning gestures the future
|
||
* gesture system (#1185) might want to claim. */
|
||
@media (min-width: 769px) {
|
||
body { touch-action: pan-y; }
|
||
}
|
||
|
||
.nav-drawer-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
z-index: 1250; /* above bottom-nav (1200), below modal-backdrop (9000) */
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 200ms ease;
|
||
}
|
||
.nav-drawer-backdrop.is-open {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.nav-drawer {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
width: min(320px, 86vw);
|
||
max-width: 360px;
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
border-right: 1px solid var(--border);
|
||
box-shadow: 4px 0 18px rgba(0, 0, 0, 0.35);
|
||
z-index: 1260; /* above its backdrop, still below modal */
|
||
transform: translateX(-100%);
|
||
transition: transform 220ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
outline: none;
|
||
}
|
||
.nav-drawer.is-open { transform: translateX(0); }
|
||
|
||
.nav-drawer-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--surface-2, var(--surface));
|
||
}
|
||
.nav-drawer-title {
|
||
font-weight: 700;
|
||
font-size: var(--fs-md);
|
||
color: var(--text);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.nav-drawer-close {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text);
|
||
font-size: 22px;
|
||
line-height: 1;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
touch-action: manipulation;
|
||
}
|
||
.nav-drawer-close:hover,
|
||
.nav-drawer-close:focus-visible {
|
||
background: var(--bg-hover, rgba(120, 160, 255, 0.12));
|
||
outline: none;
|
||
}
|
||
|
||
.nav-drawer-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 6px 0;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
.nav-drawer-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 12px 18px;
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
font-size: var(--fs-md);
|
||
min-height: 48px;
|
||
touch-action: manipulation;
|
||
}
|
||
.nav-drawer-item:hover,
|
||
.nav-drawer-item:focus-visible {
|
||
background: var(--bg-hover, rgba(120, 160, 255, 0.12));
|
||
outline: none;
|
||
}
|
||
.nav-drawer-icon {
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
width: 24px;
|
||
text-align: center;
|
||
flex: none;
|
||
}
|
||
.nav-drawer-label {
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* Option A: drawer is wide-only (>768px). At ≤768px the bottom-nav has
|
||
* the More tab (PR #1174) — drawer is hidden + script bails on open(). */
|
||
@media (max-width: 768px) {
|
||
.nav-drawer,
|
||
.nav-drawer-backdrop { display: none !important; }
|
||
}
|
||
|
||
/* Reduced motion: instant snap, no transition. */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.nav-drawer { transition: none; }
|
||
.nav-drawer-backdrop { transition: none; }
|
||
}
|
||
/* === end #1064 ====================================================== */
|
||
|
||
/* === #1062 Touch Gestures ============================================
|
||
* Visual affordances for touch-gestures.js. CSS variables only — no
|
||
* hardcoded colors. body owns vertical scroll natively (touch-action: pan-y);
|
||
* the bottom-nav opts out so we manage horizontal swipes on it.
|
||
* ==================================================================== */
|
||
body { touch-action: pan-y; }
|
||
[data-bottom-nav] { touch-action: none; }
|
||
|
||
.row-swiping { transition: transform 180ms ease-out; }
|
||
.row-action-overlay {
|
||
position: fixed;
|
||
z-index: 1500;
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 0;
|
||
background: var(--card-bg, #1a1a1a);
|
||
border: 1px solid var(--border, #333);
|
||
border-radius: 6px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||
overflow: hidden;
|
||
opacity: 0;
|
||
transform: translateX(40px);
|
||
transition: opacity 180ms ease-out, transform 180ms ease-out;
|
||
pointer-events: auto;
|
||
}
|
||
.row-action-overlay.row-action-overlay-open {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
.row-action-overlay[hidden] { display: none; }
|
||
.row-action-btn {
|
||
flex: 1 1 auto;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text, #e7e7e7);
|
||
padding: 0 12px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
min-height: 48px;
|
||
border-right: 1px solid var(--border, #333);
|
||
}
|
||
.row-action-btn:last-child { border-right: none; }
|
||
.row-action-btn:hover { background: var(--bg-hover, rgba(120, 160, 255, 0.12)); }
|
||
.row-action-btn:active { background: var(--accent-bg, rgba(0, 122, 255, 0.18)); }
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.row-swiping,
|
||
.row-action-overlay { transition: none !important; }
|
||
}
|
||
/* === end #1062 ====================================================== */ (feat(#1062): green — implement gesture system)
|
||
|
||
/* === Issue #1065 — Gesture discoverability hints =================== */
|
||
.gesture-hint {
|
||
position: fixed;
|
||
z-index: 9999;
|
||
max-width: 360px;
|
||
/* !important guards against any layered cascade enabling pointer-events
|
||
* (e.g., a future overlay class with higher specificity). The hint
|
||
* wrapper MUST never capture clicks — only its inner button does. */
|
||
pointer-events: none !important;
|
||
opacity: 1;
|
||
animation-name: gesture-hint-slide-in;
|
||
animation-duration: 240ms;
|
||
animation-timing-function: ease-out;
|
||
animation-fill-mode: both;
|
||
transition: opacity 320ms ease-out;
|
||
}
|
||
.gesture-hint-bottom {
|
||
left: 50%;
|
||
bottom: 80px;
|
||
transform: translateX(-50%);
|
||
}
|
||
.gesture-hint-top {
|
||
left: 50%;
|
||
top: 16px;
|
||
transform: translateX(-50%);
|
||
}
|
||
.gesture-hint-top-left {
|
||
left: 16px;
|
||
top: 80px;
|
||
}
|
||
.gesture-hint-inner {
|
||
pointer-events: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
background: var(--surface-2, #1a1a1a);
|
||
color: var(--text, #e7e7e7);
|
||
border: 1px solid var(--border, #333);
|
||
border-radius: 999px;
|
||
padding: 8px 8px 8px 16px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
||
font-size: 13px;
|
||
line-height: 1.3;
|
||
}
|
||
.gesture-hint-text {
|
||
white-space: normal;
|
||
}
|
||
.gesture-hint-dismiss {
|
||
pointer-events: auto;
|
||
background: var(--accent, #4a9eff);
|
||
color: var(--accent-fg, #fff);
|
||
border: none;
|
||
border-radius: 999px;
|
||
padding: 6px 14px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
flex: 0 0 auto;
|
||
}
|
||
.gesture-hint-dismiss:hover { filter: brightness(1.1); }
|
||
.gesture-hint-fading { opacity: 0; }
|
||
|
||
@keyframes gesture-hint-slide-in {
|
||
from { opacity: 0; transform: translateX(-50%) translateY(8px); }
|
||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
}
|
||
.gesture-hint-top-left { animation-name: gesture-hint-fade-in; }
|
||
@keyframes gesture-hint-fade-in {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.gesture-hint {
|
||
animation-name: none !important;
|
||
animation-duration: 0s !important;
|
||
}
|
||
}
|
||
/* === end #1065 ====================================================== */
|