Files
meshcore-analyzer/public/live.css
T
Kpa-clawbot 50676d5e65 fix(live): #1110 node filter — autocomplete, theming, no reload (#1113)
## Summary

Fixes the broken **Filter by node** input on the Live page.

The previous implementation used a native `<datalist>` (no consistent
styling, no real autocomplete UX), only applied on `change` (Enter), and
mutated `location.hash` on commit — which the SPA router treated as a
navigation, triggering a full re-init.

## What changed

- **Markup** (`public/live.js`): replaces the `<datalist>` with a styled
custom `#liveNodeFilterDropdown` and adds combobox/listbox ARIA wiring.
- **Styling** (`public/live.css`): new `.live-node-filter-input` rules
use `color-mix` on `var(--text)` for the background and `var(--border)`
/ `var(--text)` for border + foreground — fully theme-aware. Dropdown
uses `var(--surface-1)` + `var(--border)`.
- **Behavior**: 200 ms debounced `/api/nodes/search` call as the user
types. Suggestions render with name + 8-char pubkey prefix. Clicking a
suggestion (`mousedown` so it beats blur) sets the filter to the pubkey.
- **No reload**: `applyFilterFromInput` and the clear button now use
`history.replaceState` instead of mutating `location.hash`, so the SPA
router never re-runs and the page never reloads. Enter is
`preventDefault`-ed and either selects the highlighted suggestion or
commits the typed text.
- **Keyboard**: ArrowUp/Down navigate suggestions, Esc closes, Enter
selects.

## TDD

Per `AGENTS.md`, the failing E2E test landed first (commit `74f3e92`),
then the fix made it green (commit `a5c5c65`).

The test file `test-1110-live-filter.js` (and an integrated block in
`test-e2e-playwright.js`) asserts:

1. The input's computed `background-color` is **not** hardcoded white
when `data-theme="dark"` is set.
2. The input is not vastly larger than the surrounding toolbar row.
3. Typing `"te"` shows a visible `#liveNodeFilterDropdown` with at least
one `.live-node-filter-option`.
4. Clicking a suggestion sets `_liveGetNodeFilterKeys()` to a non-empty
list **without** reloading the page (verified via a `window.__m` marker
that survives) and **without** navigating away from `#/live`.
5. Pressing **Enter** in the filter input never reloads or navigates.

### How to run the E2E

```
go build -o /tmp/corescope-server ./cmd/server
/tmp/corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public &
CHROMIUM_PATH=/usr/bin/chromium-browser BASE_URL=http://localhost:13581 \
  node test-1110-live-filter.js
# 4/4 passed
```

## Acceptance criteria from #1110

- [x] Filter input visually matches Live page toolbar (theme-aware bg,
border, padding)
- [x] Typing 1+ characters shows dropdown of matching node names
- [x] Selecting a suggestion filters the live feed immediately
- [x] Clearing input restores unfiltered view
- [x] No page reload on any interaction with the input
- [x] E2E test asserts: type → suggestions appear → click suggestion →
feed filters → no navigation

Fixes #1110

---------

Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com>
2026-05-05 14:28:12 -07:00

959 lines
26 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.
/* ========== LIVE TRACE PAGE ========== */
/* Live page takes full viewport */
.live-page {
position: relative;
width: 100%;
height: 100vh;
height: 100dvh;
overflow: hidden;
background: var(--surface-0);
}
/* Override #app height constraint on live page */
#app:has(.live-page) {
height: 100vh;
height: 100dvh;
overflow: visible;
}
.live-overlay {
position: absolute;
z-index: 1000;
pointer-events: auto;
display: flex;
flex-direction: column;
}
/* ---- Panel header (non-scrolling) ---- */
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
padding: 4px 6px;
}
/* ---- Panel content (scrollable) ---- */
.panel-content {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.live-feed .panel-content {
display: flex;
flex-direction: column;
gap: 1px;
}
.live-legend .panel-content {
display: flex;
flex-direction: column;
gap: 3px;
}
/* ---- Header / Stats ---- */
.live-header {
top: 64px;
left: 12px;
display: flex;
align-items: center;
gap: 14px;
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
backdrop-filter: blur(12px);
padding: 8px 16px;
border-radius: 10px;
border: 1px solid var(--border);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255,255,255,0.04);
}
.live-title {
font-size: 14px;
font-weight: 800;
letter-spacing: 2px;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
}
.live-beacon {
width: 8px;
height: 8px;
background: var(--status-red);
border-radius: 50%;
display: inline-block;
animation: beaconPulse 1.5s ease-in-out infinite;
box-shadow: 0 0 8px #ef4444, 0 0 16px rgba(239, 68, 68, 0.4);
}
@keyframes beaconPulse {
0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 8px #ef4444, 0 0 16px rgba(239, 68, 68, 0.4); }
50% { opacity: 0.6; transform: scale(0.85); box-shadow: 0 0 4px #ef4444; }
}
.live-stats-row {
display: flex;
gap: 6px;
}
.live-stat-pill {
background: color-mix(in srgb, var(--text) 8%, transparent);
border: 1px solid var(--border);
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
}
.live-stat-pill span {
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--accent);
}
.live-stat-pill.anim-pill span { color: var(--status-yellow); }
.live-stat-pill.rate-pill span { color: var(--status-green); }
.live-sound-btn {
background: color-mix(in srgb, var(--text) 8%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 16px;
transition: background 0.2s;
}
.live-sound-btn:hover {
background: color-mix(in srgb, var(--text) 14%, transparent);
}
/* ---- Node Detail Panel ---- */
.live-node-detail {
top: 64px;
right: 12px;
width: 320px;
max-height: calc(100vh - 140px);
background: color-mix(in srgb, var(--surface-1) 95%, transparent);
backdrop-filter: blur(12px);
border-radius: 10px;
border: 1px solid var(--border);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
transition: transform 0.2s ease, opacity 0.2s ease;
}
.live-node-detail.hidden {
transform: translateX(340px);
opacity: 0;
pointer-events: none;
}
/* ---- Feed ---- */
.live-feed {
bottom: 12px;
left: 12px;
width: 360px;
max-height: 340px;
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
backdrop-filter: blur(12px);
border-radius: 10px;
border: 1px solid var(--border);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
padding: 6px;
}
.live-feed-item {
color: var(--text-muted);
font-size: 12px;
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
padding: 5px 8px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 6px;
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
overflow: hidden;
}
.live-feed-item:first-child {
background: rgba(59, 130, 246, 0.08);
border-left: 2px solid rgba(59, 130, 246, 0.4);
}
.live-feed-enter {
opacity: 0;
transform: translateX(-20px) scale(0.95);
}
.feed-icon { font-size: 14px; flex-shrink: 0; }
.feed-type { font-weight: 700; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0; }
.feed-hops {
font-size: 10px;
color: var(--text-muted);
background: color-mix(in srgb, var(--text) 8%, transparent);
padding: 1px 5px;
border-radius: 3px;
flex-shrink: 0;
}
.feed-text {
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.feed-time {
font-size: 10px;
color: var(--text-muted);
flex-shrink: 0;
margin-left: auto;
}
/* ---- Legend ---- */
.live-legend {
bottom: 58px;
right: 12px;
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
backdrop-filter: blur(12px);
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--border);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
color: var(--text-muted);
font-size: 11px;
transition: opacity 0.3s, transform 0.3s;
}
/* Collapsible legend (#279) */
.live-legend.hidden {
opacity: 0;
transform: translateX(100%);
pointer-events: none;
visibility: hidden;
}
.legend-title {
font-size: 9px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 2px;
margin-top: 0;
}
.legend-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.live-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
box-shadow: 0 0 4px currentColor;
}
/* ---- Tooltip ---- */
.live-tooltip {
background: color-mix(in srgb, var(--surface-1) 95%, transparent) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 6px !important;
font-size: 11px !important;
font-weight: 600 !important;
padding: 3px 8px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6) !important;
letter-spacing: 0.3px !important;
}
.live-tooltip::before {
border-top-color: var(--surface-1) !important;
}
/* ---- Heatmap toggle ---- */
.live-toggles {
display: flex;
gap: 10px;
font-size: 11px;
color: var(--text-muted);
align-items: center;
margin-left: 8px;
}
.live-toggles label { display: flex; align-items: center; gap: 3px; cursor: pointer; white-space: nowrap; }
.live-toggles input { margin: 0; }
/* Region filter (#1045) inline in live header toggles */
.live-toggles .live-region-filter-container { display: inline-flex; align-items: center; }
.live-toggles .live-region-filter-container .region-dropdown-trigger { font-size: inherit; padding: 2px 6px; }
/* ---- Leaflet overrides for dark theme ---- */
.live-page .leaflet-control-zoom a {
background: color-mix(in srgb, var(--surface-1) 92%, transparent) !important;
backdrop-filter: blur(12px);
color: var(--text) !important;
border-color: var(--border) !important;
}
.live-page .leaflet-control-zoom a:hover {
background: rgba(59, 130, 246, 0.2) !important;
}
/* ---- Medium breakpoint (#279) ---- */
@media (max-width: 768px) {
.live-feed { width: 280px; max-height: 200px; }
.live-node-detail { width: 260px; }
.live-legend { font-size: 10px; padding: 8px 10px; }
.live-header { gap: 8px; padding: 6px 12px; }
.live-stat-pill { font-size: 11px; padding: 2px 8px; }
.live-toggles { font-size: 10px; gap: 6px; }
}
/* ---- Responsive ---- */
@media (max-width: 640px) {
.live-feed { display: none !important; }
.feed-show-btn { display: none !important; }
.live-legend { display: none !important; }
.legend-toggle-btn { display: none !important; }
.live-header {
flex-wrap: wrap; gap: 6px; padding: 6px 10px;
top: 56px; left: 8px; right: 8px; max-width: calc(100vw - 16px);
}
.live-stats-row { flex-wrap: wrap; gap: 4px; }
.live-stat-pill { font-size: 11px; padding: 2px 7px; }
.live-toggles { font-size: 10px; gap: 6px; margin-left: 0; overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; width: 100%; min-width: 0; }
.live-title { font-size: 12px; letter-spacing: 1px; }
/* #203 — bottom-sheet node detail on mobile */
.live-node-detail { width: 100%; right: 0; left: 0; top: auto; bottom: 0; max-height: 60dvh; border-radius: 16px 16px 0 0; overflow-y: auto; z-index: 1050; }
.live-node-detail.hidden { transform: translateY(100%); }
/* Close button was unreachable: panel-header collapsed to 8px on mobile, panel-content
scroll area started at y=8, overlapping the button's 36px tap target (y=642) */
.live-node-detail .panel-header { min-height: 44px; }
.feed-detail-card {
position: fixed !important;
right: 0 !important;
left: 0 !important;
bottom: 58px !important;
top: auto !important;
transform: none !important;
width: 100% !important;
max-width: 100vw !important;
max-height: 50vh !important;
overflow-y: auto !important;
border-radius: 10px 10px 0 0 !important;
animation: slideUp 0.2s ease-out !important;
}
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
/* Touch targets */
.feed-hide-btn { width: 36px; height: 36px; font-size: 16px; }
.feed-show-btn { padding: 10px 12px; min-width: 44px; min-height: 44px; }
.legend-toggle-btn { min-width: 44px; min-height: 44px; }
/* Feed resize handle: disable on mobile (can't drag easily) */
.feed-resize-handle { display: none; }
/* Leaflet zoom controls */
.live-page .leaflet-top.leaflet-right { top: 56px; }
}
/* Feed item hover */
.live-feed-item:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
/* Feed detail card */
.feed-detail-card {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
width: 260px;
background: color-mix(in srgb, var(--surface-1) 95%, transparent);
backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
z-index: 600;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
animation: fadeSlideIn 0.15s ease-out;
font-size: .8rem;
color: var(--text);
}
@keyframes fadeSlideIn { from { opacity:0; transform: translateY(-50%) translateX(8px); } to { opacity:1; transform: translateY(-50%) translateX(0); } }
.fdc-header {
display: flex;
align-items: center;
gap: 8px;
padding-left: 8px;
margin-bottom: 8px;
}
.fdc-header strong { font-size: .85rem; color: var(--text); }
.fdc-sender { color: var(--text-muted); font-size: .75rem; }
.fdc-close {
margin-left: auto;
background: none; border: none; color: var(--text-muted); cursor: pointer;
font-size: .85rem; padding: 2px 4px; border-radius: 4px;
}
.fdc-close:hover { color: var(--text); background: color-mix(in srgb, var(--text) 12%, transparent); }
.fdc-text {
background: color-mix(in srgb, var(--text) 6%, transparent);
border-radius: 6px;
padding: 8px 10px;
margin-bottom: 8px;
line-height: 1.4;
color: var(--text-muted);
word-break: break-word;
}
.fdc-meta {
display: flex;
flex-wrap: wrap;
gap: 6px 12px;
margin-bottom: 8px;
font-size: .7rem;
color: var(--text-muted);
}
.fdc-link {
display: block;
text-align: center;
padding: 6px;
border-radius: 6px;
background: rgba(59,130,246,0.15);
color: var(--accent);
text-decoration: none;
font-size: .75rem;
font-weight: 600;
transition: background .15s;
}
.fdc-link:hover { background: rgba(59,130,246,0.25); }
.fdc-replay {
display: block; width: 100%; margin-top: 4px; padding: 6px; border: none; border-radius: 6px;
background: rgba(168,85,247,0.15); color: #c084fc; font-size: .75rem; font-weight: 600;
cursor: pointer; transition: background .15s;
}
.fdc-replay:hover { background: rgba(168,85,247,0.3); }
/* Ghost hop markers */
.ghost-hop-marker {
animation: ghostPulse 2s ease-in-out infinite;
}
@keyframes ghostPulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.6; }
}
/* Site nav auto-hide on live page — fixed overlay, no layout shift */
.nav-autohide {
opacity: 0 !important;
transform: translateY(-100%) !important;
pointer-events: none !important;
visibility: hidden;
}
.top-nav {
transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s ease;
}
/* Feed hide/show */
.live-feed { transition: opacity 0.3s, transform 0.3s; }
.live-feed.hidden { opacity: 0; transform: translateX(-100%); pointer-events: none; visibility: hidden; }
.feed-hide-btn {
position: absolute; top: 6px; right: 6px;
background: color-mix(in srgb, var(--text) 15%, transparent); border: 1px solid var(--border); color: var(--text-muted);
width: 24px; height: 24px; border-radius: 6px; cursor: pointer;
font-size: 13px; line-height: 1; display: flex; align-items: center; justify-content: center;
opacity: 0.7; transition: opacity 0.2s, background 0.2s;
z-index: 5;
}
.feed-hide-btn:hover { opacity: 1; color: #fff; background: rgba(239,68,68,0.6); }
.feed-show-btn {
position: absolute; bottom: 12px; left: 12px; z-index: 500;
background: color-mix(in srgb, var(--surface-1) 92%, transparent); backdrop-filter: blur(8px);
border: 1px solid var(--border); border-radius: 8px;
color: var(--text-muted); font-size: 18px; padding: 8px 10px;
cursor: pointer; transition: all 0.2s;
}
.feed-show-btn:hover { color: var(--text); border-color: rgba(59,130,246,0.4); }
.feed-show-btn.hidden { display: none; }
/* Push Leaflet zoom controls below nav bar */
.live-page .leaflet-top.leaflet-right {
top: 64px;
}
/* === VCR Bar === */
.vcr-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
background: color-mix(in srgb, var(--surface-1) 95%, transparent);
backdrop-filter: blur(12px);
border-top: 1px solid var(--border);
padding: 8px 12px;
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.vcr-bar { padding-bottom: calc(8px + env(safe-area-inset-bottom, 34px)); }
.live-feed { bottom: calc(78px + env(safe-area-inset-bottom, 34px)); }
.feed-show-btn { bottom: calc(88px + env(safe-area-inset-bottom, 34px)) !important; }
.live-legend { bottom: calc(78px + env(safe-area-inset-bottom, 34px)); }
}
.vcr-bar > .vcr-controls {
display: flex; align-items: center; gap: 4px; flex-shrink: 0;
}
.vcr-controls {
display: flex;
align-items: center;
gap: 8px;
}
.vcr-btn {
background: color-mix(in srgb, var(--text) 10%, transparent);
border: 1px solid var(--border);
color: var(--text);
border-radius: 6px;
padding: 6px 14px;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.15s;
}
.vcr-btn:hover { background: color-mix(in srgb, var(--text) 18%, transparent); }
/* #1110 Live page node filter — match toolbar control sizing & theme */
.live-node-filter-wrap { position: relative; display: inline-flex; align-items: center; }
.live-node-filter-input {
background: color-mix(in srgb, var(--text) 6%, transparent);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 8px;
font-size: inherit;
line-height: 1.3;
height: auto;
min-width: 140px;
outline: none;
}
.live-node-filter-input:focus {
border-color: color-mix(in srgb, var(--text) 35%, transparent);
background: color-mix(in srgb, var(--text) 10%, transparent);
}
.live-node-filter-input::placeholder { color: var(--text-muted); opacity: 0.7; }
.live-node-filter-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 2px;
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: 6px;
max-height: 240px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
min-width: 200px;
}
.live-node-filter-dropdown.hidden { display: none; }
.live-node-filter-option {
padding: 6px 10px;
cursor: pointer;
font-size: 0.85rem;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background 0.1s;
}
.live-node-filter-option:hover { background: color-mix(in srgb, var(--text) 12%, transparent); }
.live-node-filter-option.live-node-filter-active {
background: var(--accent, color-mix(in srgb, var(--text) 25%, transparent));
color: var(--text);
}
.vcr-live-btn {
background: rgba(239, 68, 68, 0.2);
color: var(--status-red);
font-weight: 700;
font-size: 0.7rem;
letter-spacing: 0.05em;
}
.vcr-live-btn:hover { background: rgba(239, 68, 68, 0.35); }
.vcr-mode {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.05em;
padding: 2px 8px;
border-radius: 4px;
margin-left: auto;
}
.vcr-mode-live { color: var(--status-green); }
.vcr-mode-paused { color: var(--status-yellow); background: rgba(251,191,36,0.1); }
.vcr-mode-replay { color: var(--accent); background: rgba(96,165,250,0.1); }
.vcr-live-dot {
display: inline-block;
width: 6px;
height: 6px;
background: var(--status-green);
border-radius: 50%;
margin-right: 4px;
animation: vcr-pulse 1.5s ease-in-out infinite;
}
@keyframes vcr-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.vcr-lcd {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
background: #1a1a0a;
border: 1px solid var(--border);
border-radius: 4px;
padding: 4px 10px;
min-width: 110px;
box-shadow: inset 0 1px 4px rgba(0,0,0,0.7), 0 0 8px rgba(0,0,0,0.3);
gap: 1px;
}
.vcr-lcd-row {
font-family: 'Courier New', 'Consolas', monospace;
letter-spacing: 2px;
line-height: 1.1;
text-align: right;
white-space: nowrap;
}
.vcr-lcd-mode {
font-size: 0.65rem;
color: var(--status-green);
text-shadow: 0 0 6px rgba(74, 222, 128, 0.6);
font-weight: 700;
}
.vcr-lcd-canvas {
width: 130px;
height: 28px;
}
.vcr-lcd-pkts {
font-size: 0.6rem;
color: var(--status-yellow);
text-shadow: 0 0 4px rgba(251, 191, 36, 0.5);
font-weight: 700;
min-height: 0.7rem;
}
.vcr-missed {
font-size: 0.7rem;
font-weight: 700;
color: var(--status-yellow);
background: rgba(251,191,36,0.15);
padding: 2px 6px;
border-radius: 4px;
animation: vcr-missed-pulse 0.6s ease;
}
@keyframes vcr-missed-pulse {
0% { transform: scale(1.3); }
100% { transform: scale(1); }
}
.vcr-scope-btns {
display: flex;
gap: 2px;
flex-shrink: 0;
}
.vcr-scope-btn {
background: none;
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.75rem;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.vcr-scope-btn.active {
background: rgba(59,130,246,0.2);
color: var(--accent);
border-color: rgba(59,130,246,0.3);
}
.vcr-timeline-container {
flex: 1;
position: relative;
height: 28px;
}
.vcr-timeline {
width: 100%;
height: 100%;
cursor: grab;
border-radius: 3px;
background: color-mix(in srgb, var(--text) 6%, transparent);
border: 1px solid color-mix(in srgb, var(--text) 10%, transparent);
touch-action: none;
}
.vcr-timeline:active, .vcr-timeline.dragging {
cursor: grabbing;
}
.vcr-playhead {
position: absolute;
top: 0;
width: 2px;
height: 100%;
background: var(--status-red);
border-radius: 1px;
pointer-events: none;
box-shadow: 0 0 4px rgba(248,113,113,0.5);
}
.vcr-prompt {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 0.78rem;
color: var(--text);
}
.vcr-prompt.hidden { display: none; }
.vcr-prompt-btn {
background: rgba(59,130,246,0.15);
border: 1px solid rgba(59,130,246,0.25);
color: var(--accent);
font-size: 0.75rem;
font-weight: 600;
padding: 4px 12px;
border-radius: 5px;
cursor: pointer;
transition: background 0.15s;
}
.vcr-prompt-btn:hover { background: rgba(59,130,246,0.3); }
/* Adjust feed position to not overlap VCR bar */
.live-feed { bottom: 68px; }
.feed-show-btn { bottom: 68px !important; }
/* Backdrop for mobile tap-outside-to-close (#797) */
.node-detail-backdrop {
display: none;
position: absolute;
inset: 0;
z-index: 1049;
background: rgba(0, 0, 0, 0.25);
}
@media (max-width: 640px) {
.node-detail-backdrop.active { display: block; }
}
/* Mobile VCR */
@media (max-width: 640px) {
/* Mobile VCR: two-row stacked layout */
.vcr-bar {
padding: 4px 8px;
padding-bottom: calc(4px + env(safe-area-inset-bottom, 20px));
flex-wrap: wrap;
gap: 4px;
overflow: visible;
}
/* Row 1: controls + scope + LCD, all in one line */
.vcr-controls { order: 1; flex-shrink: 0; gap: 4px; }
.vcr-scope-btns { order: 2; flex-shrink: 0; gap: 1px; }
.vcr-lcd { order: 3; display: flex; margin-left: auto; min-width: 90px; padding: 2px 6px; }
.vcr-lcd-canvas { width: 100px; height: 22px; }
.vcr-mode { display: none; }
/* Row 2: timeline takes full width */
.vcr-timeline-container { order: 4; width: 100%; flex: none; height: 20px; }
/* #207 — 44px touch targets for VCR buttons */
.vcr-btn { padding: 4px 8px; font-size: 0.75rem; min-height: 44px; min-width: 44px; }
.vcr-scope-btn { font-size: 0.6rem; padding: 2px 6px; min-height: 44px; min-width: 44px; }
.vcr-prompt { order: 5; width: 100%; font-size: 0.7rem; }
}
/* Timeline time tooltip */
.vcr-time-tooltip {
position: absolute;
top: -24px;
transform: translateX(-50%);
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
color: var(--text);
font-size: 0.65rem;
font-weight: 600;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
pointer-events: none;
z-index: 10;
}
.vcr-time-tooltip.hidden { display: none; }
/* Screen-reader only text */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Legend toggle button — visible at all sizes (#60, #279) */
.legend-toggle-btn {
position: absolute;
bottom: 82px;
right: 12px;
z-index: 500;
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
backdrop-filter: blur(8px);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-muted);
font-size: 18px;
padding: 8px 10px;
cursor: pointer;
transition: all 0.2s;
}
.legend-toggle-btn:hover { color: var(--text); border-color: rgba(59,130,246,0.4); }
/* Feed resize handle (#27) */
.feed-resize-handle {
position: absolute;
top: 0;
right: -4px;
width: 8px;
height: 100%;
cursor: ew-resize;
z-index: 10;
}
.feed-resize-handle::after {
content: '⋮';
position: absolute;
top: 50%;
right: 0px;
width: 10px;
height: 32px;
transform: translateY(-50%);
background: color-mix(in srgb, var(--text) 25%, transparent);
border-radius: 3px;
transition: background 0.2s;
display: flex; align-items: center; justify-content: center;
font-size: 14px; color: var(--text-muted); line-height: 32px; text-align: center;
}
.feed-resize-handle:hover::after { background: rgba(59,130,246,0.5); color: #fff; }
/* Nav pin button (#62) */
.nav-pin-btn {
background: none;
border: none;
font-size: 14px;
cursor: pointer;
padding: 4px 8px;
opacity: 0.5;
transition: opacity 0.2s;
margin-left: auto;
}
.nav-pin-btn:hover { opacity: 0.8; }
.nav-pin-btn.pinned { opacity: 1; filter: drop-shadow(0 0 4px rgba(59,130,246,0.5)); }
/* ========== Panel Corner Positioning (#608 M0) ========== */
/* Corner positions — applied via data-position attribute on .live-overlay panels */
.live-overlay[data-position="tl"] { top: 64px; left: 12px; bottom: auto; right: auto; }
.live-overlay[data-position="tr"] { top: 64px; right: 12px; bottom: auto; left: auto; }
.live-overlay[data-position="bl"] { bottom: 58px; left: 12px; top: auto; right: auto; }
.live-overlay[data-position="br"] { bottom: 58px; right: 12px; top: auto; left: auto; }
/* Override hide animations for positioned panels — slide toward nearest edge */
.live-overlay[data-position="tl"].hidden,
.live-overlay[data-position="bl"].hidden { transform: translateX(-100%); }
.live-overlay[data-position="tr"].hidden,
.live-overlay[data-position="br"].hidden { transform: translateX(100%); }
.live-overlay[data-position].hidden { opacity: 0; pointer-events: none; visibility: hidden; }
/* Corner toggle button */
.panel-corner-btn {
width: 28px;
height: 28px;
padding: 0;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
opacity: 0.6;
transition: opacity 0.15s, background 0.15s;
font-size: 14px;
line-height: 28px;
text-align: center;
flex-shrink: 0;
border-radius: 4px;
}
.panel-corner-btn:hover { opacity: 1; background: color-mix(in srgb, var(--text) 12%, transparent); }
.panel-corner-btn:focus-visible {
opacity: 1;
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 3px;
}
/* On mobile, corner toggle is not useful (panels are hidden or bottom-sheet) */
@media (max-width: 640px) {
.panel-corner-btn { display: none !important; }
.live-overlay[data-position] {
top: unset !important; bottom: unset !important;
left: unset !important; right: unset !important;
}
}
/* ── Drag Manager (#608 M1) ── */
/* Panel header as drag handle — desktop pointer devices only */
@media (pointer: fine) {
.live-overlay .panel-header {
cursor: grab;
user-select: none;
touch-action: none;
}
.live-overlay.is-dragging .panel-header {
cursor: grabbing;
}
.live-overlay .panel-header:hover {
background: var(--bg-hover, rgba(255, 255, 255, 0.03));
}
}
/* Panel during drag */
.live-overlay.is-dragging {
opacity: 0.92;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
will-change: transform;
transition: none !important;
}
/* Freely placed panel — no corner transition animations */
.live-overlay[data-dragged="true"] {
transition: none;
}
@media (prefers-reduced-motion: reduce) {
.live-overlay.is-dragging,
.live-overlay[data-dragged="true"] {
transition: none;
}
}