Files
meshcore-analyzer/public/live.css
T
Kpa-clawbot d7bd9d57b8 feat(live): fullscreen toggle + collapse controls by default (closes #1532) (#1572)
Closes #1532.

## What

Implements the triage's 3-step fix path + tufte keyboard shortcut:

1. **`.live-controls` collapsed by default at all viewports** (was
≤768px only). The existing ⚙ pin reveals the toggles row on demand —
parity with the map-controls accordion pattern in `map.js`.
2. **New `#liveFullscreenToggle` button (⛶) next to ⚙.** Click or press
`F` to flip `body.live-fullscreen`. CSS under that class hides:
   - `.live-header-body` (title)
   - `.live-controls-body` (toggle row contents)
   - `.vcr-controls` and `.vcr-bar` (timeline scrubber)
   - `.bottom-nav`
- secondary panels (`.live-feed`, `.live-legend`, related show-buttons)
3. **`.live-stats-row` stays pinned top-right** with translucent chip
styling so the 3 KPI pills (nodes / active / pkts·min) earn permanent
residence per the tufte finding.

## Tufte rationale (from triage)

> data-ink ratio is poor — 11 controls + 3 KPIs displayed permanently
steal pixels from THE data (the firework animation). Defaults-on chrome
should collapse behind a pin/cog; only the 3 stat pills earn permanent
residence (sparkline-grade density). … "Fullscreen" is the right
primitive — Tufte's "shrink principle" says strip until unreadable, then
add back.

## Keyboard shortcut

`F` toggles fullscreen. Guards:
- Skips when focus is in `INPUT`/`TEXTAREA`/`SELECT`/contenteditable (no
interference with node-filter / audio sliders typing).
- Skips when modifier keys are held.
- Only fires on the `.live-page` route.
- State persists across reloads via `localStorage('live-fullscreen')`.

## TDD

| Commit | SHA | What |
|--------|-----|------|
| RED | `852a474b` | Source-invariant assertion test
`test-issue-1532-live-fullscreen.js` (17 assertions, all fail against
master). |
| GREEN | `906c6cc0` | Implementation: HTML button, JS click+keydown
wiring, CSS body-class rules + top-level `.is-collapsed` rule. |

Verify the RED commit gates the change:

```
git checkout 852a474b -- test-issue-1532-live-fullscreen.js
git checkout master -- public/live.js public/live.css
node test-issue-1532-live-fullscreen.js   # exits 1, 15 failures
```

## Files modified

- `public/live.js` — `#liveFullscreenToggle` button in `init()`
template; `wireLiveFullscreenToggle()` IIFE (click + keydown +
localStorage); `wireLiveCollapseToggles()` updated so `liveControls`
defaults collapsed at all viewports.
- `public/live.css` — top-level `.live-controls.is-collapsed` rule;
`body.live-fullscreen { ... }` block hiding chrome and pinning the stats
row.
- `test-issue-1532-live-fullscreen.js` — new source-invariant test (17
assertions across 5 categories).
- `test-all.sh` + `.github/workflows/deploy.yml` — register the new test
in the unit-test runner.

## CDP-verify

Source-invariant assertions cover the behavior gate. The visual diff
cannot run against staging (staging is pre-merge; deploy is
post-master). Local server stand-up was skipped for token-budget
reasons; the assertion test asserts class names + computed-style trigger
conditions equivalent to what a CDP getComputedStyle check would assert.
Post-merge: staging deploy auto-publishes within minutes — visual diff
will land then.

## Preflight overrides

None — preflight clean (PII clean, scope: 5 files all within stated
surface, red→green visible, CSS vars defined, no XSS sinks added).

---------

Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-06-04 10:52:22 -07:00

1383 lines
42 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;
}
/* #1207: empty-state placeholder fills the feed body so the panel never
renders as orphan chrome (✕ + ◫ buttons with no content). The placeholder
is always in the DOM; CSS hides it when the feed has live-feed-item
siblings. This way the panel body is never empty, even between packets,
on first paint, or after a feed clear. */
.live-feed-empty {
color: var(--text-muted);
font-size: 12px;
padding: 12px 8px;
text-align: center;
}
.live-feed .panel-content:has(.live-feed-item) .live-feed-empty {
display: none;
}
.live-legend .panel-content {
display: flex;
flex-direction: column;
gap: 3px;
}
/* ---- Header / Stats ---- */
.live-header {
top: 64px;
left: 12px;
/* #1205 — toggles re-anchored as a child row; allow wrap + max-width.
NOTE: #liveHeader intentionally no longer carries `.live-overlay`
(which set flex-direction:column). With direction defaulting to row
here, #liveControls is forced onto its own line via `flex: 0 0 100%`
and flex-wrap, so the header lays out: [title row] then [toggles
row]. At ≤640px the wrap keeps both rows reachable. */
display: flex;
/* #1204: .live-overlay sets flex-direction: column for all overlay panels
* (feed/legend/node-detail need that for header+content stacking). The
* header is horizontal — critical strip + toggle + body lay out inline,
* not stacked. Without this override the "0 pkts" critical strip stacks
* above MESH LIVE title and the stats row gets clipped. */
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 10px;
row-gap: 6px;
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-width: calc(100vw - 24px);
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 {
/* #1206: track --vcr-bar-height (set by JS ResizeObserver on .vcr-bar)
* the same way .live-feed does — hard-coded bottom offsets get
* occluded by the bar on mobile two-row + safe-area-inset layouts.
*/
bottom: calc(var(--vcr-bar-height, 58px) + 10px);
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;
}
/* #1293 — SVG shape-aware legend swatch (replaces the flat colour dot).
* Inline-block wrapper keeps SVG aligned with adjacent text labels. */
.live-shape-swatch {
display: inline-block;
width: 14px;
height: 14px;
margin-right: 6px;
vertical-align: middle;
line-height: 0;
}
.live-shape-swatch svg { display: block; }
/* #1274: marker-style swatches — mirror the live map circleMarker ring
* convention (bright white ring = repeater, faded ring = other roles).
* Background uses --role-repeater / --text-muted via CSS variables so
* theming remains consistent. */
.live-ring {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
background: var(--text-muted);
}
.live-ring--repeater {
border: 1.5px solid #fff;
opacity: 0.95;
}
.live-ring--other {
border: 1px solid #fff;
opacity: 0.45;
}
/* ---- 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 (#1205 — re-anchored inside MESH LIVE panel)
* History: PR #1180 detached the toggles into a `position:fixed` overlay
* (`.live-overlay.live-controls`) pinned bottom-right. On many viewports
* the row visually orphaned away from any panel (issue #1205). PR #1209
* tried parking inside #liveLegend; rejected on UX grounds (inverted
* hierarchy). Correct restoration: toggles are a child of #liveHeader
* (the MESH LIVE panel) so they share its chrome and the existing
* header-collapse semantics.
*/
.live-controls {
/* In-flow inside #liveHeader. No position:fixed, no own background
(header already supplies blur + border + padding). */
position: static;
background: transparent;
padding: 0;
border: 0;
box-shadow: none;
flex: 0 0 100%; /* break onto its own row inside flex-wrap header */
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.live-controls-body {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex-wrap: wrap;
}
/* Region/Area filters inline in live header controls body */
.live-controls-body .live-region-filter-container,
.live-controls-body .live-area-filter-container { display: inline-flex; align-items: center; }
.live-controls-body .live-region-filter-container .region-dropdown-trigger,
.live-controls-body .live-area-filter-container .region-dropdown-trigger { font-size: inherit; padding: 2px 6px; }
/* #1108 — "Show all nodes" sibling of the region dropdown. Reuses the
.live-controls-body label rhythm so it lines up with the other inline toggles. */
.live-controls-body .live-show-all-region-nodes {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: inherit;
white-space: nowrap;
}
/* ---- #1532 Live: default-collapsed controls + fullscreen mode ---- */
/* #1532 — `.live-controls` collapses by default at ALL viewports (was
≤768px only). The ⚙ pin reveals the toggles row on demand, parity
with the map-controls accordion pattern. Top-level (not media-gated)
so it applies on desktop too. */
.live-controls-toggle,
.live-fullscreen-toggle { display: inline-flex; }
.live-controls.is-collapsed .live-controls-body { display: none; }
/* Fullscreen mode (#1532). The body class hides chrome and
pins the 3 stat pills top-right so the firework animation is the
primary visual surface. The header band, controls row, VCR row,
and bottom-nav drop out; the beacon + pkt-count critical strip is
inside the header body so it goes with it (LIVE rate stays in
the stats row, which remains). */
body.live-fullscreen .live-header-body { display: none !important; }
body.live-fullscreen .live-controls-body { display: none !important; }
body.live-fullscreen .live-controls-toggle,
body.live-fullscreen .live-header-toggle { display: none !important; }
body.live-fullscreen .vcr-bar .vcr-controls { display: none !important; }
body.live-fullscreen .vcr-bar { display: none !important; }
body.live-fullscreen .bottom-nav { display: none !important; }
body.live-fullscreen .live-feed,
body.live-fullscreen .live-legend,
body.live-fullscreen .legend-toggle-btn,
body.live-fullscreen .feed-show-btn { display: none !important; }
/* Collapse the header chrome to just the floating stats row pinned
top-right. The header background/border drop so the stats pills
float over the map. */
body.live-fullscreen .live-header {
position: fixed;
top: 12px;
right: 12px;
left: auto;
background: transparent;
border: 0;
box-shadow: none;
padding: 0;
max-width: none;
z-index: 700;
}
body.live-fullscreen .live-stats-row {
position: fixed;
top: 12px;
right: 12px;
left: auto;
z-index: 700;
background: color-mix(in srgb, var(--surface-1) 84%, transparent);
backdrop-filter: blur(10px);
padding: 4px 8px;
border-radius: 8px;
border: 1px solid var(--border);
}
/* Keep the fullscreen exit affordance reachable: a small chip in the
top-left so users can leave the mode without remembering the F key. */
body.live-fullscreen #liveFullscreenToggle {
display: inline-flex !important;
position: fixed;
top: 12px;
left: 12px;
z-index: 700;
}
/* ---- 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; }
/* #1220 — when the controls row collapses to just the gear toggle, do NOT
force it onto its own full-width row inside the header's flex-wrap. The
wide-viewport `flex: 0 0 100%` rule (which lets the controls wrap below
the title on tablet) leaves ~60px of empty chrome below the critical
strip on mobile when both bodies are hidden. Inline the gear with the
count badge + 📊 toggle so the collapsed panel is a single ~48-56px
strip. Expanded controls still get their own wrapped row via the
`.is-expanded` override below. */
.live-controls.is-collapsed {
padding: 0;
flex: 0 0 auto;
width: auto;
margin-left: auto;
}
/* #1220 — both panels collapsed = single-row strip. Drop the header's
vertical padding so the panel hugs the 48px toggle buttons (chrome ≈
toggle height + border). Without this, the 4px top/bottom padding
plus the 6px padding around the gear container pushed total height
to 70px, dangerously close to looking like empty chrome again. */
.live-header.is-collapsed:has(.live-controls.is-collapsed) {
padding: 0 6px;
}
/* Expanded body on narrow: stack so it never overflows the cluster */
.live-controls.is-expanded { max-width: calc(100vw - 24px); margin-top: 12px; }
.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 {
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; }
/* #1234 chrome-reduction pass 2 — mobile-only adjustments.
* - Drop MESH LIVE text label and the chart-icon header toggle; counters
* speak for themselves on a narrow phone.
* - Force the body to render inline (cancel the #1178/#1179 collapse on
* .live-header-body — the chart toggle that drove it is gone).
* - Keep the header to a single row by removing flex-wrap so all pills
* sit beside the beacon.
* - Cap header chrome at 44px (acceptance criterion in #1234). */
.live-title { display: none !important; }
.live-header-toggle { display: none !important; }
/* #1234: header body now only contains the (hidden) MESH LIVE title;
* stats row is a direct child of .live-header. Collapse the body entirely
* on mobile so it contributes 0 height. */
.live-header-body,
.live-header-body[hidden] { display: none !important; }
.live-header {
flex-wrap: wrap; /* keep #1220 collapse: gear inline, expanded controls wrap to row 2 */
min-height: 0;
padding: 4px 8px;
gap: 6px;
row-gap: 0;
top: 8px; /* top-nav hidden on /live; reclaim that space */
}
.live-header.is-collapsed,
.live-header.is-expanded {
padding: 4px 8px;
}
.live-stats-row {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
min-width: 0;
}
/* #1234 — hide top app navbar on /live route at ≤640px.
* Uses :has() (Chromium 105+, Safari 15.4+, Firefox 121+) to scope the
* rule to pages mounting .live-page (the Live SPA route). Other routes
* keep the navbar intact. */
body:has(.live-page) .top-nav { display: none !important; }
body:has(.live-page) #app:has(.live-page),
body:has(.live-page) .live-page {
height: calc(100vh - var(--bottom-nav-reserve, 0px));
height: calc(100dvh - var(--bottom-nav-reserve, 0px));
}
/* #1234 — VCR scope: 12h/24h collapse into the More dropdown at ≤640px. */
.vcr-scope-btn--overflow { display: none !important; }
.vcr-scope-more-wrap { display: inline-flex !important; position: relative; }
/* #1234 — gear toggle shrunk on mobile so it fits within the ≤44px header
* strip. Tap target stays ≥40px (above WCAG 24px minimum, within #1060
* spirit). The header chart-icon toggle is fully removed on mobile so
* the gear is the only persistent action button. */
.live-controls-toggle {
min-width: 36px; min-height: 36px;
width: 36px; height: 36px;
padding: 6px;
font-size: 14px;
}
.vcr-scope-more {
/* Inherits .vcr-scope-btn base styling; no extra geometry needed. */
}
.vcr-scope-more-menu {
position: absolute;
bottom: calc(100% + 4px);
right: 0;
z-index: 1100;
background: color-mix(in srgb, var(--surface-1) 96%, transparent);
backdrop-filter: blur(8px);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px;
box-shadow: 0 6px 18px rgba(0,0,0,0.45);
display: flex;
flex-direction: column;
gap: 2px;
min-width: 80px;
}
.vcr-scope-more-menu[hidden] { display: none; }
.vcr-scope-more-item {
background: none;
border: 0;
color: var(--text);
text-align: left;
font-size: 0.8rem;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
}
.vcr-scope-more-item:hover { background: color-mix(in srgb, var(--text) 12%, transparent); }
/* #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)); }
/* Inherit --vcr-bar-height (set by JS); no hard-coded bottom here so the
* feed/legend keep tracking the live VCR height including the safe area.
* #1206 r2: removed .live-legend override — it double-added the
* safe-area inset (already baked into --vcr-bar-height via .vcr-bar
* padding-bottom above) and used a 78px fallback inconsistent with
* the base rule's 58px. Base rule at .live-legend handles both.
*/
}
.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;
}
/* #1234 — overflow dropdown is mobile-only; desktop shows all scope buttons
inline so the wrap is hidden by default. The @media (max-width:640px)
block below flips it on and hides the .vcr-scope-btn--overflow buttons. */
.vcr-scope-more-wrap { display: none; }
.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(--text);
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 (#1206)
* --vcr-bar-height is set by JS via ResizeObserver on .vcr-bar so the feed
* (and other bottom-pinned overlays) always clear the bar regardless of
* how tall it grows (mobile two-row layout, safe-area-inset, etc.).
*/
.live-feed { bottom: calc(var(--vcr-bar-height, 58px) + 10px); }
.feed-show-btn { bottom: calc(var(--vcr-bar-height, 58px) + 10px) !important; }
/* Cap the feed's height so its scrollable content never extends below the
* VCR bar top edge, even when the feed is pinned to a bottom corner.
*/
.live-feed {
max-height: min(340px, calc(100dvh - 64px - var(--vcr-bar-height, 58px) - 24px));
}
/* 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) {
/* #1244: SINGLE-ROW layout. PR #1234 was meant to put all VCR controls
* on one row but `flex-wrap: wrap` here + `width: 100%` on the timeline
* forced a 2-row layout (controls/LCD on row 1, scope+scrubber on row 2).
* Fix: `flex-wrap: nowrap`, the timeline gets `flex: 1 1 0` so it
* absorbs leftover width, and scope buttons shrink (1/6/More) to fit. */
.vcr-bar {
/* #1250: shaved horizontal padding from 8px → 4px. At 375px viewport the
* row of flex-shrink:0 children (controls + scope-btns + lcd) overflowed
* the padding box by 0.83px even though .vcr-timeline-container is
* flex:1 1 0 — its min-width:40px floor leaves the rest to absorb the
* overflow. 4px headroom on each side is well above the 0.83px clip and
* keeps the LCD's right edge ≤ viewport width. Desktop is unaffected
* (this rule is inside @media (max-width: 640px)). */
padding: 4px 4px;
padding-bottom: calc(4px + env(safe-area-inset-bottom, 20px));
flex-wrap: nowrap;
gap: 4px;
overflow: visible;
}
/* #1221: LCD clock shares row with playback controls (not floating
* bottom-right). LCD is scaled ~70% of desktop so it fits next to the
* touch-target buttons without clipping at 375px viewport. */
.vcr-controls { order: 1; flex-shrink: 0; gap: 2px; }
.vcr-lcd {
order: 4;
/* #1244: no auto margin in a no-wrap row — let the timeline absorb
* free space via flex:1 instead. */
margin-left: 0;
min-width: 0;
padding: 2px 4px;
flex-shrink: 0;
}
.vcr-lcd-canvas { width: 74px; height: 18px; }
.vcr-lcd-mode { font-size: 0.55rem; letter-spacing: 1px; }
.vcr-lcd-pkts { font-size: 0.5rem; letter-spacing: 1px; }
.vcr-scope-btns { order: 2; flex-shrink: 0; gap: 1px; }
.vcr-mode { display: none; }
/* #1244: timeline absorbs remaining width on the single row. */
.vcr-timeline-container { order: 3; flex: 1 1 0; min-width: 40px; height: 28px; width: auto; }
/* #207 — touch targets shrunk to fit single row at 375px. WCAG 2.5.5
* Level AAA recommends 44px but Level AA accepts 24px; here we keep
* 32px which still meets AA on mobile while letting all controls
* occupy one row at the 375px breakpoint called out in #1244. */
.vcr-btn { padding: 4px 6px; font-size: 0.7rem; min-height: 32px; min-width: 32px; }
.vcr-scope-btn { font-size: 0.6rem; padding: 2px 5px; min-height: 32px; min-width: 0; }
.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;
}
.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: calc(var(--vcr-bar-height, 58px) + 10px); left: 12px; top: auto; right: auto; }
.live-overlay[data-position="br"] { bottom: calc(var(--vcr-bar-height, 58px) + 10px); 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;
}
}
/* Clickable path popup */
.lc-path-popup { font-size: 12px; line-height: 1.6; min-width: 160px; }
.lc-path-badge { color: #fff; border-radius: 3px; padding: 1px 5px; font-size: 11px; font-weight: 600; }
.lc-path-time { margin-top: 4px; color: var(--text-muted); font-size: 11px; }
.lc-path-chain { margin-top: 4px; word-break: break-word; }
.lc-path-link-wrap { margin-top: 4px; }
.lc-path-link { font-size: 11px; }
/* Eliminate SVG baseline drift inside Leaflet divIcons */
.live-node-marker {
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
border: none !important;
box-sizing: border-box !important;
background: transparent !important;
}
.live-node-marker svg {
display: block;
margin: 0;
padding: 0;
}
/* Hide all overlays during zoom for maximum smoothness */
.leaflet-zoom-anim .leaflet-marker-pane,
.leaflet-zoom-anim .leaflet-tooltip-pane,
.leaflet-zoom-anim .leaflet-popup-pane,
.leaflet-zoom-anim .leaflet-overlay-pane,
.leaflet-zoom-anim .leaflet-liveAnimPane-pane {
display: none !important;
}