Files
meshcore-analyzer/public/style.css
T
Kpa-clawbot cea2c70d12 feat(#1034): channel UX redesign PR1 — Add Channel modal + sectioned sidebar (#1037)
## Summary

PR 1 of 3 for #1034 — channel UX redesign. Replaces the cramped inline
"type a name or 32-hex blob" form with a clear modal dialog, and
reorganizes the sidebar into three labeled sections.

**Scope of this PR:** Modal UI + sectioned sidebar. QR generation/scan
is deferred to PR #2 (placeholders are wired and ready).
`channel-decrypt.js` crypto is untouched.

## What changed

### New modal: `[+ Add Channel]`

Triggered by the new sidebar button. Three sections:

1. **Generate PSK Channel** — name + `[Generate & Show QR]` →
`crypto.getRandomValues(16)` → hex → `ChannelDecrypt.storeKey`. QR
rendering ships in PR #2; for now `#qr-output` surfaces the hex key as
text.
2. **Add Private Channel (PSK)** — 32-hex input (regex-validated),
optional display name, `[Add]`. `[📷 Scan QR]` placeholder is present but
`disabled` (PR #2 wires it).
3. **Monitor Hashtag Channel** — non-editable `#` prefix + free text +
case-sensitivity warning + `[Monitor]`. Reuses
`ChannelDecrypt.deriveKey`.

Privacy footer: _"🔒 Keys stay in your browser. CoreScope is a passive
observer..."_

Close ✕, backdrop click, and Escape all dismiss.

### Sectioned sidebar

`renderChannelList()` rewritten to render three sections:

- **My Channels** — `userAdded` channels. ✕ always visible. Last sender
+ relative time.
- **Network** — server-known cleartext channels.
- **Encrypted (N)** — collapsed by default (toggle persists in
`localStorage`). Shows hash byte + packet count.

The legacy "🔒 No key" checkbox and `#chShowEncrypted` toggle are removed
entirely. Encrypted channels are always fetched; the renderer groups
them.

## Tests

- **Unit** — `test-channel-modal-ux.js` (33 assertions): added to
`test-all.sh`. Covers sidebar button, modal markup, three sections, QR
placeholders, privacy footer, sectioned sidebar, modal handlers (incl.
`crypto.getRandomValues(16)`).
- **E2E** — `test-channel-modal-e2e.js` (Playwright, 14 steps). Covers
modal open/close, section rendering, invalid-hex error, valid-hex
storage, encrypted-section toggle. Run with:
  ```
CHROMIUM_PATH=/usr/bin/chromium-browser BASE_URL=http://localhost:38201
node test-channel-modal-e2e.js
  ```
- `test-channel-psk-ux.js` — updated to reference `#chPskName` (was
`#chKeyLabelInput`).

### Red→green proof

- Red commit (`7ee421b`): test added with 31 expected assertion
failures, no source change.
- Green commit (`897be8f`): implementation lands, test passes 33/33.

## Browser-validated

Built `cmd/server/`, ran against `test-fixtures/e2e-fixture.db`,
exercised modal open → invalid hex → valid hex → key persisted → modal
closes → sectioned sidebar renders + Encrypted toggle expands. All 14
E2E steps pass.

## What's NOT in this PR

- QR code rendering (PR #2)
- Camera/QR scanning (PR #2)
- Migration of legacy localStorage format (PR #3, if needed — current
key format is unchanged)
- `channel-decrypt.js` changes (none — UI-only PR)

## Acceptance criteria from #1034

- [x] Modal opens on `[+ Add Channel]` click
- [x] Three sections clearly separated with labels
- [x] Add PSK: accepts 32-hex (QR scan = PR #2)
- [x] Monitor Hashtag: derives key, case-sensitivity warning shown
- [x] Privacy footer present
- [x] Sidebar: three sections (My Channels / Network / Encrypted)
- [x] ✕ button visible and functional on My Channels entries
- [x] "No key" checkbox removed
- [ ] Generate PSK QR display — text fallback only; QR is PR #2
- [ ] Old stored keys migrate seamlessly — no migration needed (storage
format unchanged)

Refs #1034

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-04 18:40:46 -07:00

2465 lines
107 KiB
CSS
Raw Blame History

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