Files
meshcore-analyzer/public/live.css
T
Kpa-clawbot 9d1f5d2395 fix(#1061): bottom navigation for narrow viewports (#1174)
Red commit: a200704d5e (CI URL added once
Actions resolves the run)

Fixes #1061

## What
Bottom navigation at ≤768px with 5 tabs in spec order: Home, Packets,
Live, Map, Channels. Top-nav suppressed at the same breakpoint — no
duplicate nav UX.

## Files
- NEW `public/bottom-nav.js` — renders 5 tabs, syncs `.active` on
`hashchange`, reuses the existing in-app hash router (`<a
href="#/...">`). Stable selector `[data-bottom-nav-tab="<route>"]`.
Container `[data-bottom-nav]`.
- NEW `public/bottom-nav.css` — styles. Tokens reused: `--nav-bg`,
`--nav-text`, `--nav-text-muted`, `--nav-active-bg`, `--accent`,
`--border` (all global → resolve in BOTH light and dark themes).
- `public/index.html` — one `<link>` for the CSS, one `<script>` after
`app.js`. The `<nav>` is appended by JS as a sibling of `<main
id="app">` at DOMContentLoaded.
- `test-bottom-nav-1061-e2e.js` + `.github/workflows/deploy.yml` —
Playwright wiring.

## Decisions
- **Breakpoint:** `@media (max-width: 768px)`. No `@container` rules
exist anywhere in `style.css` today — media query is consistent.
- **Top-nav suppression:** `display:none` at ≤768px. Simpler than a
hamburger collapse; long-tail routes (Tools/Lab/Perf) remain reachable
by URL; "More"-tab/hamburger fallback deferred per issue body.
- **Active indicator:** `var(--nav-active-bg)` + 2px accent top-border.
No moving pill.
- **Safe-area:** `padding-bottom: env(safe-area-inset-bottom)` on nav +
reciprocal `body` reservation. `viewport-fit=cover` already in place.
- **Reduced motion:** `prefers-reduced-motion: reduce` disables the
transition.

## TDD
- Red: `a200704` — assertions fail (no bottom-nav).
- Green: `53851a1` — component + styles.

E2E assertion added: `test-bottom-nav-1061-e2e.js:71` (case (a) —
bottom-nav visible at 360x800).

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: openclaw-bot <bot@openclaw>
2026-05-09 11:00:46 -07:00

1060 lines
29 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.
* #1174 mesh-op review: subtract --bottom-nav-reserve (defined in
* bottom-nav.css; 0px at desktop, 56px+safe-area at ≤768) so the
* bottom-nav does not cover VCR controls / Leaflet zoom / live trace
* markers on phones. The 52px term accounts for the top-nav above. */
.live-page {
position: relative;
width: 100%;
height: calc(100vh - 52px - var(--bottom-nav-reserve, 0px));
height: calc(100dvh - 52px - var(--bottom-nav-reserve, 0px));
overflow: hidden;
background: var(--surface-0);
}
/* Override #app height constraint on live page */
#app:has(.live-page) {
height: calc(100vh - 52px - var(--bottom-nav-reserve, 0px));
height: calc(100dvh - 52px - var(--bottom-nav-reserve, 0px));
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: 10px;
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
backdrop-filter: blur(12px);
padding: 4px 10px;
border-radius: 8px;
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);
max-height: 40px;
box-sizing: border-box;
}
.live-header-body {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
/* Critical strip (Mesh-Operator review #1180): beacon + pkt count are
always visible even when the collapsible body is hidden at narrow
widths. This is the ingest-state cue (red beacon = WS down) + the
one number operators check while the header is otherwise collapsed. */
.live-header-critical {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* Toggle buttons (#1178, #1179) — hidden at wide viewports, visible at ≤768px.
Mesh-Operator review #1180: tap target ≥48×48 (#1060 floor + AGENTS glove
operability rule). Visible glyph stays small (decorative); transparent
padding expands the hit area without changing the visual chrome. */
.live-header-toggle,
.live-controls-toggle {
display: none;
align-items: center;
justify-content: center;
min-width: 48px;
min-height: 48px;
/* Visible chrome stays compact; padding grows the hit area. */
width: 48px;
height: 48px;
padding: 8px;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in srgb, var(--text) 8%, transparent);
color: var(--text);
font-size: 16px;
line-height: 1;
cursor: pointer;
flex-shrink: 0;
}
.live-header-toggle:hover,
.live-controls-toggle:hover {
background: color-mix(in srgb, var(--text) 14%, transparent);
}
.live-header-toggle:focus-visible,
.live-controls-toggle:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.live-title {
font-size: 12px;
font-weight: 800;
letter-spacing: 1.5px;
color: var(--text);
display: flex;
align-items: center;
gap: 6px;
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: 1px 8px;
border-radius: 16px;
font-size: 11px;
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;
flex-wrap: wrap;
}
.live-toggles label { display: flex; align-items: center; gap: 3px; cursor: pointer; white-space: nowrap; }
.live-toggles input { margin: 0; }
/* ---- Live controls cluster (#1179) ----
* Pinned to bottom-right, above the VCR bar and the global bottom-nav.
* Reserves space for both env(safe-area-inset-bottom) and the bottom-nav
* (#1061, currently in PR #1174). When the bottom-nav lands the layout
* tracks its custom property (--bottom-nav-height); otherwise the
* fallback (56px) keeps the cluster clear of the VCR bar / bottom-nav
* region.
*/
.live-controls {
position: fixed;
right: 12px;
bottom: calc(78px + var(--bottom-nav-height, 56px) + env(safe-area-inset-bottom, 0px));
z-index: 1000;
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
backdrop-filter: blur(12px);
padding: 8px 12px;
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);
max-width: min(620px, calc(100vw - 24px));
display: flex;
align-items: center;
gap: 8px;
}
.live-controls-body {
display: flex;
align-items: center;
gap: 8px;
min-width: 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) + collapse toggles (#1178, #1179) ---- */
@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: 6px; padding: 4px 8px; max-height: none; min-height: 48px; }
.live-stat-pill { font-size: 11px; padding: 1px 7px; }
.live-toggles { font-size: 10px; gap: 6px; }
/* Show toggle buttons */
.live-header-toggle,
.live-controls-toggle { display: inline-flex; }
/* When collapsed, hide the body */
.live-header.is-collapsed .live-header-body,
.live-controls.is-collapsed .live-controls-body { display: none; }
.live-header.is-collapsed { gap: 0; padding: 4px 6px; }
.live-controls.is-collapsed { padding: 6px; }
/* Expanded body on narrow: stack so it never overflows the cluster */
.live-controls.is-expanded { max-width: calc(100vw - 24px); }
.live-controls.is-expanded .live-controls-body { flex-wrap: wrap; }
.live-controls.is-expanded .live-toggles { flex-wrap: wrap; max-height: 50vh; overflow-y: auto; }
}
/* ---- 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;
}
}