Use native control templates and share duplicated view styling

This commit is contained in:
n30nex
2026-10-11 22:37:33 -04:00
parent 8bfc45d487
commit 034cc8f71a
9 changed files with 152 additions and 356 deletions
+5 -1
View File
@@ -40,7 +40,8 @@ test('spacing and dragging survive refresh while live packets keep their routes
const stage = page.locator('#netgraph-stage');
await expect(stage).toHaveAttribute('data-render-state', 'idle');
await layoutPanel(page);
await page.getByLabel('Region spacing', { exact: true }).fill('2');
await page.getByLabel('Region spacing', { exact: true }).focus(); await page.keyboard.press('Home');
for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowRight');
await expect(stage).toHaveAttribute('data-region-spacing', '2');
await expect(stage).toHaveAttribute('data-visible-routes', '2');
await page.getByRole('button', { name: 'Move region right', exact: true }).click();
@@ -76,6 +77,8 @@ test('spacing and dragging survive refresh while live packets keep their routes
await expect(stage).toHaveAttribute('data-last-packet-hops', '1');
await expect.poll(() => stage.getAttribute('data-active-packets').then(Number)).toBeGreaterThan(0);
await page.screenshot({ path: info.outputPath('netgraph-arranged-live-only.png') });
await expect(stage).toHaveAttribute('data-active-packets', '0', { timeout: 12_000 });
await expect(stage).toHaveAttribute('data-residue-count', '0', { timeout: 5_000 });
await page.reload(); await expect(stage).toHaveAttribute('data-render-state', 'idle');
expect(await saved(page)).toEqual({ ...moved, routes: false });
await expect(stage).toHaveAttribute('data-region-spacing', '2');
@@ -88,6 +91,7 @@ test('spacing and dragging survive refresh while live packets keep their routes
await page.locator('#region-filter > summary').click();
const box = (await page.locator('.region-options').boundingBox())!;
expect(box.x).toBeGreaterThanOrEqual(0); expect(box.x + box.width).toBeLessThanOrEqual(page.viewportSize()!.width);
expect(box.y + box.height).toBeLessThan((await page.locator('.controls').boundingBox())!.y);
await page.keyboard.press('Escape'); await expect(page.locator('#region-filter')).not.toHaveAttribute('open', '');
await expect(page.locator('#region-filter > summary')).toBeFocused();
});
+3
View File
@@ -93,6 +93,9 @@
<a class="control-button" href="">Try again</a>
</p>
</main>
<template id="graph-camera-ui"><div class="graph-camera" role="group" aria-label="3D camera controls" hidden><strong>3D camera</strong><div><button type="button" data-left aria-label="Rotate topology left" title="Rotate left">↶</button><button type="button" data-right aria-label="Rotate topology right" title="Rotate right">↷</button><button type="button" data-flat>Top-down</button></div><label>Tilt<input type="range" min="8" max="65" step="1" value="45" aria-label="Topology tilt"><output>45°</output></label><small>Drag to pan. Shift-drag or right-drag to rotate. Pinch to zoom and twist to rotate. Height is schematic.</small></div></template>
<template id="graph-regions-ui"><details id="region-filter" class="region-filter"><summary aria-label="Filter regions">Regions · All</summary><div id="region-options" class="region-options" hidden><input type="search" aria-label="Search regions" placeholder="IATA / region"><div class="region-actions"><button type="button" data-all>All</button><button type="button" data-none>None</button></div><div class="region-checks" role="group" aria-label="Regions"></div></div></details></template>
<template id="graph-layout-ui"><details class="graph-layout"><summary>Region layout</summary><label>Spacing<input type="range" min="1" max="4" step="0.1" aria-label="Region spacing"><output></output></label><label>Move region<select aria-label="Region to move"></select></label><div class="region-actions"><button type="button" aria-label="Move region left" data-move="-40,0">←</button><button type="button" aria-label="Move region up" data-move="0,-40">↑</button><button type="button" aria-label="Move region down" data-move="0,40">↓</button><button type="button" aria-label="Move region right" data-move="40,0">→</button></div><button type="button" data-reset>Reset region layout</button><small>Drag a region label to move its whole bubble. Saved on this device.</small></details></template>
<script type="module" src="/src/netgraph/main.ts"></script>
</body>
</html>
+1
View File
@@ -1,3 +1,4 @@
import './networkControls.css';
import { mountSoundPreview } from './soundPreview';
import { requestedNode } from './selection';
import { mountCinematicDock, mountLayerCombinations } from './cinematicChrome';
+5 -3
View File
@@ -7,9 +7,7 @@ export function mountGraphCamera(graph: NetgraphRenderer): () => void {
button.textContent = '3D'; button.setAttribute('aria-label', '3D Netgraph'); button.setAttribute('aria-pressed', 'false');
button.title = 'Tilt and rotate the topology';
controls.insertBefore(button, controls.querySelector('.display-disclosure'));
const panel = document.createElement('div'); panel.className = 'graph-camera'; panel.hidden = true;
panel.setAttribute('role', 'group'); panel.setAttribute('aria-label', '3D camera controls');
panel.innerHTML = '<strong>3D camera</strong><div><button type="button" data-left aria-label="Rotate topology left" title="Rotate left">↶</button><button type="button" data-right aria-label="Rotate topology right" title="Rotate right">↷</button><button type="button" data-flat>Top-down</button></div><label>Tilt<input type="range" min="8" max="65" step="1" value="45" aria-label="Topology tilt"><output>45°</output></label><small>Drag to pan. Shift-drag or right-drag to rotate. Pinch to zoom and twist to rotate. Height is schematic.</small>';
const panel = graphTemplate<HTMLElement>('graph-camera-ui');
document.getElementById('display-panel')!.append(panel);
const tilt = panel.querySelector('input')!, output = panel.querySelector('output')!;
const navigate = () => document.getElementById('netgraph-stage')!.dispatchEvent(new Event('graphnavigate'));
@@ -28,3 +26,7 @@ export function mountGraphCamera(graph: NetgraphRenderer): () => void {
};
sync(); return sync;
}
export function graphTemplate<T extends HTMLElement>(id: string): T {
return (document.getElementById(id) as HTMLTemplateElement).content.firstElementChild!.cloneNode(true) as T;
}
+1
View File
@@ -1,3 +1,4 @@
import '../networkControls.css';
import { mountRegionControls } from './regionControls';
import { mountGraphFollow } from './follow';
import { mountGraphCamera } from './cameraControls';
+3 -4
View File
@@ -1,16 +1,15 @@
import { graphTemplate } from './cameraControls';
import type { NetgraphRenderer } from './renderer';
export function mountRegionControls(graph: NetgraphRenderer, host: HTMLElement, changed: () => void, closePanels: () => void) {
const stage = document.getElementById('netgraph-stage')!;
const navigate = () => stage.dispatchEvent(new Event('graphnavigate'));
const menu = document.createElement('details'); menu.id = 'region-filter'; menu.className = 'region-filter';
menu.innerHTML = '<summary aria-label="Filter regions">Regions · All</summary><div id="region-options" class="region-options" hidden><input type="search" aria-label="Search regions" placeholder="IATA / region"><div class="region-actions"><button type="button" data-all>All</button><button type="button" data-none>None</button></div><div class="region-checks" role="group" aria-label="Regions"></div></div>';
const menu = graphTemplate<HTMLDetailsElement>('graph-regions-ui');
const summary = menu.querySelector('summary')!, search = menu.querySelector('input')!, list = menu.querySelector<HTMLElement>('.region-checks')!;
const routes = document.createElement('button'); routes.type = 'button'; routes.id = 'graph-routes-button'; routes.textContent = 'Routes';
routes.setAttribute('aria-label', 'Show historical routes'); routes.title = 'Hide standing lines; live packets still light their routes briefly';
host.append(menu, routes);
const layout = document.createElement('details'); layout.className = 'graph-layout';
layout.innerHTML = '<summary>Region layout</summary><label>Spacing<input type="range" min="1" max="4" step="0.1" aria-label="Region spacing"><output></output></label><label>Move region<select aria-label="Region to move"></select></label><div class="region-actions"><button type="button" aria-label="Move region left" data-move="-40,0">←</button><button type="button" aria-label="Move region up" data-move="0,-40">↑</button><button type="button" aria-label="Move region down" data-move="0,40">↓</button><button type="button" aria-label="Move region right" data-move="40,0">→</button></div><button type="button" data-reset>Reset region layout</button><small>Drag a region label to move its whole bubble. Saved on this device.</small>';
const layout = graphTemplate<HTMLDetailsElement>('graph-layout-ui');
document.getElementById('display-panel')!.append(layout);
const spacing = layout.querySelector('input')!, output = layout.querySelector('output')!, move = layout.querySelector('select')!;
let lastAreas: ReturnType<NetgraphRenderer['regions']> | undefined, lastRegions: string[] | null | undefined, signature = '';
+36 -128
View File
@@ -71,14 +71,7 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible
box-shadow: inset 0 0 110px rgba(0, 4, 8, 0.28);
}
.glass {
z-index: 5;
border: 1px solid rgba(151, 220, 224, 0.16);
background: linear-gradient(145deg, rgba(8, 20, 26, 0.9), rgba(2, 9, 14, 0.82));
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34), inset 0 1px rgba(219, 255, 255, 0.04);
-webkit-backdrop-filter: blur(14px) saturate(1.18);
backdrop-filter: blur(14px) saturate(1.18);
}
.glass { border: 1px solid rgba(151, 220, 224, 0.16); background: linear-gradient(145deg, rgba(8, 20, 26, 0.9), rgba(2, 9, 14, 0.82)); box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34), inset 0 1px rgba(219, 255, 255, 0.04); }
/* Live canvases need not be blurred again underneath every Android control. */
@media (pointer: coarse), (max-width: 700px) {
@@ -89,78 +82,33 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible
}
}
.topbar {
position: absolute;
top: max(12px, env(safe-area-inset-top));
left: max(12px, env(safe-area-inset-left));
display: flex;
min-height: 46px;
align-items: center;
gap: 15px;
padding: 0 12px;
border-radius: 13px;
}
.topbar { position: absolute; top: max(12px, env(safe-area-inset-top)); left: max(12px, env(safe-area-inset-left)); min-height: 46px; gap: 15px; border-radius: 13px; }
.brand, .workspace-link, .view-title, .status { display: inline-flex; min-height: 44px; align-items: center; gap: 7px; }
.brand, .workspace-link { color: #effdff; text-decoration: none; }
.brand { font-size: 13px; font-weight: 760; letter-spacing: 0.02em; }
.brand-mark { width: 9px; height: 9px; border: 2px solid #35cdbc; border-radius: 50%; box-shadow: 0 0 11px #36d7c2; }
.brand { font-weight: 760; }
.view-title { color: #bfe6e3; font-size: 11px; font-weight: 760; letter-spacing: 0.055em; text-transform: uppercase; }
.view-title > span:first-child, .workspace-link > span:first-child { color: #63d9cc; text-shadow: 0 0 10px rgba(99, 217, 204, 0.68); }
.workspace-link { color: #9ccbc8; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; }
.brand:hover, .workspace-link:hover { color: #fff; }
.status { color: #aebdcb; font-size: 11px; font-weight: 650; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #86919b; }
.status[data-state="active"] .status-dot { background: #45df9a; box-shadow: 0 0 8px #45df9a; }
.status[data-state="quiet"] .status-dot { background: #70a99f; }
.status[data-state="reconnecting"] .status-dot { background: #e9c35b; animation: status-pulse 1.1s ease-in-out infinite; }
.status[data-state="offline"] .status-dot { background: #ed7283; }
.topbar.traffic-pulse .brand-mark { animation: traffic-brand 680ms ease-out; }
.traffic-meter { display: inline-flex; height: 12px; align-items: flex-end; gap: 2px; }
.traffic-meter i { width: 2px; height: 2px; border-radius: 2px; background: rgba(var(--traffic-rgb), 0.3); transition: height 160ms ease, background 160ms ease, box-shadow 160ms ease; }
.status { font-weight: 650; }
.traffic-meter i { background: rgba(var(--traffic-rgb), 0.3); transition: height 160ms ease, background 160ms ease, box-shadow 160ms ease; }
.traffic-meter[data-level="1"] i:nth-child(-n+1),
.traffic-meter[data-level="2"] i:nth-child(-n+2),
.traffic-meter[data-level="3"] i:nth-child(-n+3),
.traffic-meter[data-level="4"] i:nth-child(-n+4),
.traffic-meter[data-level="5"] i:nth-child(-n+5) { height: 10px; background: rgba(var(--traffic-rgb), 0.92); box-shadow: 0 0 7px rgba(var(--traffic-rgb), 0.62); }
.traffic-meter[data-level="5"] i:nth-child(-n+5) { background: rgba(var(--traffic-rgb), 0.92); }
.traffic-meter i:nth-child(2), .traffic-meter i:nth-child(4) { max-height: 7px; }
.traffic-meter i:nth-child(3) { max-height: 12px; }
.controls {
z-index: 8;
position: absolute;
top: max(12px, env(safe-area-inset-top));
right: max(12px, env(safe-area-inset-right));
display: flex;
min-height: 46px;
overflow: visible;
border-radius: 13px;
}
.controls { z-index: 8; position: absolute; top: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-right)); min-height: 46px; border-radius: 13px; }
.popover-control { position: relative; display: flex; }
.control-button {
display: inline-flex;
min-width: 44px;
min-height: 44px;
align-items: center;
justify-content: center;
gap: 7px;
border: 0;
border-right: 1px solid rgba(139, 205, 214, 0.12);
padding: 0 11px;
color: #bacbd5;
background: transparent;
cursor: pointer;
font-size: 12px;
font-weight: 680;
}
.control-button:hover, .control-button:focus-visible, .control-button.selected { color: #efffff; background: rgba(41, 184, 169, 0.14); outline-offset: -3px; }
.control-button { min-width: 44px; justify-content: center; gap: 7px; color: #bacbd5; font-weight: 680; }
.control-button:hover, .control-button:focus-visible, .control-button.selected { color: #efffff; outline-offset: -3px; }
.control-button svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
.control-button small { color: #8195a0; font-size: 9px; font-weight: 760; }
.control-button[data-sound-state="on"] small { color: #6ce2c5; }
.control-button[data-sound-state="resume"] small { color: #f0c36a; }
.control-button.sounding svg, .sound-activity.active { animation: sound-hop 680ms ease-out; }
.reset-button { border-right: 0; }
.route-window { display: inline-flex; min-height: 44px; align-items: center; gap: 7px; border-right: 1px solid rgba(139, 205, 214, 0.12); padding: 0 8px 0 11px; color: #869aa6; font-size: 10px; font-weight: 700; }
.route-window { gap: 7px; border-right: 1px solid rgba(139, 205, 214, 0.12); padding: 0 8px 0 11px; color: #869aa6; font-size: 10px; font-weight: 700; }
.route-window select, .scene-control select {
min-height: 32px;
border: 1px solid rgba(127, 200, 204, 0.2);
@@ -173,38 +121,23 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible
.find-panel, .sound-panel { position: absolute; top: calc(100% + 8px); z-index: 9; display: grid; border-radius: 13px; }
.find-panel[hidden], .sound-panel[hidden] { display: none; }
.find-panel { right: auto; left: 0; width: min(292px, calc(100vw - 24px)); gap: 8px; padding: 12px; }
.find-panel > label { color: #dff8f5; font-size: 11px; font-weight: 760; }
.find-panel input {
width: 100%;
min-height: 44px;
border: 1px solid rgba(99, 217, 204, 0.26);
border-radius: 8px;
padding: 0 11px;
color: #efffff;
background: rgba(4, 15, 20, 0.96);
}
.node-search-results { display: grid; max-height: min(330px, 52vh); overflow-y: auto; overscroll-behavior: contain; }
.node-search-results > p { margin: 7px 2px; color: #8297a4; font-size: 11px; }
.node-search-result { display: grid; min-height: 50px; gap: 2px; border: 0; border-top: 1px solid rgba(139, 205, 214, 0.1); padding: 7px 8px; color: #eaffff; background: transparent; text-align: left; cursor: pointer; }
.node-search-result strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.node-search-result span { color: #8298a5; font-size: 9px; text-transform: capitalize; }
.find-panel { right: auto; }
.find-panel > label { color: #dff8f5; font-weight: 760; }
.find-panel input { color: #efffff; }
.node-search-results > p { margin: 7px 2px; }
.node-search-result { min-height: 50px; color: #eaffff; }
.node-search-result strong { font-size: 11px; }
.node-search-result span { color: #8298a5; font-size: 9px; }
.node-search-result:hover, .node-search-result:focus-visible, .node-search-result[aria-selected="true"] { background: rgba(41, 184, 169, 0.13); outline-offset: -2px; }
.sound-panel { right: 0; width: 250px; gap: 11px; padding: 13px; }
.sound-panel header { display: flex; align-items: center; justify-content: space-between; color: #efffff; font-size: 12px; }
.sound-panel header span { color: #899da8; font-size: 11px; font-weight: 750; }
.sound-panel { width: 250px; }
.sound-panel header { color: #efffff; }
.sound-panel[data-sound-state="on"] header span { color: #6ce2c5; }
.sound-panel[data-sound-state="resume"] header span { color: #f0c36a; }
.sound-toggle { min-height: 44px; border: 1px solid rgba(99, 217, 204, 0.25); border-radius: 9px; color: #eaffff; background: rgba(38, 166, 155, 0.14); cursor: pointer; font-size: 12px; font-weight: 720; }
.sound-toggle:hover, .sound-toggle:focus-visible { background: rgba(38, 166, 155, 0.24); outline-offset: 1px; }
.sound-toggle { color: #eaffff; font-weight: 720; }
.scene-control, .volume-control { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 6px 10px; color: #afc0c9; font-size: 11px; font-weight: 650; }
.volume-control output { color: #e9ffff; font-variant-numeric: tabular-nums; }
.volume-control input { grid-column: 1 / -1; width: 100%; min-height: 40px; accent-color: #4dcfbe; cursor: pointer; }
.sound-panel p { display: flex; align-items: center; gap: 7px; margin: 0; color: #859aa5; font-size: 10px; }
.sound-activity { width: 7px; height: 7px; flex: none; border-radius: 50%; background: #41545c; }
.sound-activity.active { background: #6ce2c5; box-shadow: 0 0 9px rgba(108, 226, 197, 0.9); }
.volume-control output { color: #e9ffff; }
.volume-control input { min-height: 40px; }
.graph-summary {
position: absolute;
top: max(72px, calc(env(safe-area-inset-top) + 72px));
@@ -231,18 +164,7 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible
.zoom-controls .control-button { width: 44px; height: 44px; padding: 0; font-size: 23px; touch-action: manipulation; }
.zoom-controls .control-button + .control-button { border-top: 1px solid rgba(151, 220, 224, 0.16); }
.legend {
position: absolute;
right: max(12px, env(safe-area-inset-right));
bottom: max(12px, env(safe-area-inset-bottom));
display: grid;
gap: 7px;
padding: 9px 11px;
border-radius: 11px;
color: #93aab4;
font-size: 9px;
pointer-events: none;
}
.legend { right: max(12px, env(safe-area-inset-right)); bottom: max(12px, env(safe-area-inset-bottom)); display: grid; gap: 7px; padding: 9px 11px; border-radius: 11px; color: #93aab4; font-size: 9px; pointer-events: none; }
.role-legend, .packet-legend { display: flex; flex-wrap: wrap; gap: 6px 11px; }
.role-legend span, .packet-legend span { display: inline-flex; align-items: center; gap: 5px; }
.role-legend i { width: 7px; height: 7px; border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 50%; background: #67ead2; }
@@ -256,34 +178,20 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible
.packet-legend i[data-kind="Control"] { background: #a78bfa; }
.packet-legend i[data-kind="inter-area"] { background: transparent; border-top: 1px dashed #76a8af; box-shadow: none; }
.tooltip { position: absolute; z-index: 7; max-width: min(260px, calc(100vw - 24px)); padding: 7px 9px; border-radius: 8px; color: #efffff; font-size: 10px; pointer-events: none; transform: translate(10px, calc(-100% - 10px)); }
.tooltip[hidden] { display: none; }
.tooltip strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tooltip span { color: #8da3af; font-size: 9px; text-transform: capitalize; }
.tooltip { z-index: 7; max-width: min(260px, calc(100vw - 24px)); padding: 7px 9px; color: #efffff; font-size: 10px; transform: translate(10px, calc(-100% - 10px)); }
.tooltip strong { display: block; }
.tooltip span { color: #8da3af; }
.node-inspector-sheet { position: absolute; z-index: 7; top: max(76px, calc(env(safe-area-inset-top) + 76px)); right: max(12px, env(safe-area-inset-right)); width: min(350px, calc(100vw - 24px)); max-height: min(620px, calc(100vh - 164px)); overflow: hidden; border-radius: 14px; }
.node-inspector-sheet[hidden] { display: none; }
.node-inspector { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); max-height: min(620px, calc(100vh - 164px)); }
.node-inspector > header { display: flex; min-height: 54px; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 8px 9px 13px; border-bottom: 1px solid rgba(139, 205, 214, 0.11); }
.node-inspector > header > div { display: grid; min-width: 0; gap: 2px; }
.node-inspector > header strong { overflow: hidden; color: #f0ffff; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.node-inspector > header span { color: #8ca2af; font-size: 10px; text-transform: capitalize; }
.node-inspector-close { width: 44px; height: 44px; flex: none; border: 0; border-radius: 9px; color: #a9bdc8; background: transparent; cursor: pointer; font-size: 24px; }
.node-inspector-close:hover, .node-inspector-close:focus-visible { color: #efffff; background: rgba(41, 184, 169, 0.16); outline-offset: -3px; }
.node-inspector dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; padding: 10px 13px; }
.node-inspector dl div { display: grid; min-width: 0; gap: 2px; }
.node-inspector dt { color: #708895; font-size: 9px; font-weight: 650; text-transform: uppercase; }
.node-inspector dd { overflow: hidden; margin: 0; color: #d9e9ef; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.node-inspector h3 { margin: 0; padding: 6px 13px 7px; color: #8da3ae; font-size: 10px; font-weight: 700; }
.neighbor-list { overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid rgba(139, 205, 214, 0.09); }
.neighbor-row { display: grid; width: 100%; min-height: 54px; gap: 1px; border: 0; border-bottom: 1px solid rgba(139, 205, 214, 0.08); padding: 7px 13px; color: #eaffff; background: transparent; text-align: left; cursor: pointer; }
.neighbor-row strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.neighbor-row span { color: #8298a5; font-size: 9px; text-transform: capitalize; }
.neighbor-row:hover, .neighbor-row:focus-visible { background: rgba(41, 184, 169, 0.13); outline-offset: -2px; }
.node-inspector-sheet { top: max(76px, calc(env(safe-area-inset-top) + 76px)); right: max(12px, env(safe-area-inset-right)); width: min(350px, calc(100vw - 24px)); max-height: min(620px, calc(100vh - 164px)); border-radius: 14px; }
.node-inspector { max-height: min(620px, calc(100vh - 164px)); }
.node-inspector > header { min-height: 54px; padding: 9px 8px 9px 13px; }
.node-inspector-close:hover, .node-inspector-close:focus-visible { color: #efffff; }
.neighbor-row { color: #eaffff; }
.neighbor-row:hover, .neighbor-row:focus-visible { outline-offset: -2px; }
.empty-state, .fatal { position: absolute; z-index: 8; top: 50%; left: 50%; margin: 0; border-radius: 12px; padding: 12px 15px; color: #b8cbd3; font-size: 12px; transform: translate(-50%, -50%); }
.empty-state[hidden], .fatal[hidden] { display: none; }
.fatal { color: #ffd2d8; border-color: rgba(237, 114, 131, 0.35); }
.fatal { color: #ffd2d8; }
@keyframes status-pulse { 50% { opacity: 0.42; transform: scale(0.78); } }
@keyframes traffic-brand { 45% { border-color: rgb(var(--traffic-rgb)); box-shadow: 0 0 15px rgba(var(--traffic-rgb), 0.9); transform: scale(1.25); } }
@@ -377,9 +285,9 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible
.region-filter summary, #graph-routes-button { cursor:pointer; white-space:nowrap; padding:9px 6px; font:inherit; color:var(--ink-strong); }
#graph-routes-button { border:1px solid var(--panel-border); border-radius:7px; background:var(--field-bg); }
#graph-routes-button.selected { background:var(--selected); }
.region-options { position:absolute; top:calc(100% + 6px); left:0; width:min(300px,calc(100vw - 16px)); padding:10px; border:1px solid var(--panel-border); border-radius:10px; background:var(--panel-solid); box-shadow:0 8px 25px #0003; }
.region-options { max-height:min(420px,calc(100dvh - var(--dock-height,54px) - var(--dock-bottom,12px) - 176px)); overflow:auto; overscroll-behavior:contain; position:absolute; top:calc(100% + 6px); left:0; width:min(300px,calc(100vw - 16px)); padding:10px; border:1px solid var(--panel-border); border-radius:10px; background:var(--panel-solid); box-shadow:0 8px 25px #0003; }
.region-options > input { width:100%; min-height:40px; }
.region-checks { max-height:min(280px,45dvh); overflow:auto; overscroll-behavior:contain; }
.region-checks label { display:flex; align-items:center; gap:8px; min-height:44px; cursor:pointer; }
.region-checks label[hidden] { display:none; }
.region-checks input { accent-color:var(--focus-ring); }
@@ -388,7 +296,7 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible
.graph-layout { padding-block:10px; border-top:1px solid var(--panel-border); }
.graph-layout summary { cursor:pointer; padding-block:8px; font-weight:700; }
.graph-layout label { display:flex; align-items:center; gap:8px; margin-block:8px; }
.graph-layout input { min-width:0; flex:1; accent-color:var(--focus-ring); }
.graph-layout input { min-height:44px; min-width:0; flex:1; accent-color:var(--focus-ring); }
.graph-layout select { min-width:0; max-width:66%; min-height:44px; color:var(--ink-strong); background:var(--field-bg); }
.graph-layout small { display:block; margin-top:8px; line-height:1.4; color:var(--muted); }
#netgraph-stage.region-hover { cursor:grab; }
+63
View File
@@ -0,0 +1,63 @@
/* Shared declarations only. View-specific layout and responsive overrides stay in each view. */
:where(#app, #netgraph-app) .glass { z-index: 5; -webkit-backdrop-filter: blur(14px) saturate(1.18); backdrop-filter: blur(14px) saturate(1.18); }
:where(#app, #netgraph-app) .topbar { display: flex; align-items: center; padding: 0 12px; }
:where(#app, #netgraph-app) .brand { font-size: 13px; letter-spacing: 0.02em; }
:where(#app, #netgraph-app) .brand-mark { width: 9px; height: 9px; border: 2px solid #35cdbc; border-radius: 50%; box-shadow: 0 0 11px #36d7c2; }
:where(#app, #netgraph-app) .status { color: #aebdcb; font-size: 11px; }
:where(#app, #netgraph-app) .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #86919b; }
:where(#app, #netgraph-app) .status[data-state="active"] .status-dot { background: #45df9a; box-shadow: 0 0 8px #45df9a; }
:where(#app, #netgraph-app) .status[data-state="quiet"] .status-dot { background: #70a99f; }
:where(#app, #netgraph-app) .status[data-state="reconnecting"] .status-dot { background: #e9c35b; animation: status-pulse 1.1s ease-in-out infinite; }
:where(#app, #netgraph-app) .status[data-state="offline"] .status-dot { background: #ed7283; }
:where(#app, #netgraph-app) .topbar.traffic-pulse .brand-mark { animation: traffic-brand 680ms ease-out; }
:where(#app, #netgraph-app) .traffic-meter { display: inline-flex; height: 12px; align-items: flex-end; gap: 2px; }
:where(#app, #netgraph-app) .traffic-meter i { width: 2px; height: 2px; border-radius: 2px; }
:where(#app, #netgraph-app) .traffic-meter[data-level="1"] i:nth-child(-n+1), :where(#app, #netgraph-app) .traffic-meter[data-level="2"] i:nth-child(-n+2), :where(#app, #netgraph-app) .traffic-meter[data-level="3"] i:nth-child(-n+3), :where(#app, #netgraph-app) .traffic-meter[data-level="4"] i:nth-child(-n+4), :where(#app, #netgraph-app) .traffic-meter[data-level="5"] i:nth-child(-n+5) { height: 10px; box-shadow: 0 0 7px rgba(var(--traffic-rgb), 0.62); }
:where(#app, #netgraph-app) .traffic-meter i:nth-child(3) { max-height: 12px; }
:where(#app, #netgraph-app) .controls { display: flex; overflow: visible; }
:where(#app, #netgraph-app) .control-button { display: inline-flex; align-items: center; min-height: 44px; border: 0; border-right: 1px solid rgba(139, 205, 214, 0.12); padding: 0 11px; background: transparent; cursor: pointer; font-size: 12px; }
:where(#app, #netgraph-app) .control-button:hover, :where(#app, #netgraph-app) .control-button:focus-visible, :where(#app, #netgraph-app) .control-button.selected { background: rgba(41, 184, 169, 0.14); }
:where(#app, #netgraph-app) .sound-panel { right: 0; gap: 11px; padding: 13px; }
:where(#app, #netgraph-app) .sound-panel header { display: flex; align-items: center; justify-content: space-between; font-size: 12px; }
:where(#app, #netgraph-app) .sound-panel header span { color: #899da8; font-size: 11px; font-weight: 750; }
:where(#app, #netgraph-app) .sound-toggle { min-height: 44px; border: 1px solid rgba(99, 217, 204, 0.25); border-radius: 9px; background: rgba(38, 166, 155, 0.14); cursor: pointer; font-size: 12px; }
:where(#app, #netgraph-app) .sound-toggle:hover, :where(#app, #netgraph-app) .sound-toggle:focus-visible { background: rgba(38, 166, 155, 0.24); outline-offset: 1px; }
:where(#app, #netgraph-app) .volume-control output { font-variant-numeric: tabular-nums; }
:where(#app, #netgraph-app) .volume-control input { grid-column: 1 / -1; width: 100%; accent-color: #4dcfbe; cursor: pointer; }
:where(#app, #netgraph-app) .sound-panel p { display: flex; align-items: center; gap: 7px; margin: 0; color: #859aa5; font-size: 10px; }
:where(#app, #netgraph-app) .sound-activity { width: 7px; height: 7px; flex: none; border-radius: 50%; background: #41545c; }
:where(#app, #netgraph-app) .sound-activity.active { background: #6ce2c5; box-shadow: 0 0 9px rgba(108, 226, 197, 0.9); }
:where(#app, #netgraph-app) .find-panel { left: 0; width: min(292px, calc(100vw - 24px)); gap: 8px; padding: 12px; }
:where(#app, #netgraph-app) .find-panel > label { font-size: 11px; }
:where(#app, #netgraph-app) .find-panel input { min-height: 44px; width: 100%; border: 1px solid rgba(99, 217, 204, 0.26); border-radius: 8px; padding: 0 11px; background: rgba(4, 15, 20, 0.96); }
:where(#app, #netgraph-app) .node-search-results { display: grid; max-height: min(330px, 52vh); overflow-y: auto; overscroll-behavior: contain; }
:where(#app, #netgraph-app) .node-search-results > p { color: #8297a4; font-size: 11px; }
:where(#app, #netgraph-app) .node-search-result { display: grid; gap: 2px; border: 0; border-top: 1px solid rgba(139, 205, 214, 0.1); padding: 7px 8px; background: transparent; text-align: left; cursor: pointer; }
:where(#app, #netgraph-app) .node-search-result strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
:where(#app, #netgraph-app) .node-search-result span { text-transform: capitalize; }
:where(#app, #netgraph-app) .route-window { display: inline-flex; align-items: center; min-height: 44px; }
:where(#app, #netgraph-app) .legend { position: absolute; }
:where(#app, #netgraph-app) .tooltip { position: absolute; border-radius: 8px; pointer-events: none; }
:where(#app, #netgraph-app) .tooltip strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
:where(#app, #netgraph-app) .tooltip span { font-size: 9px; text-transform: capitalize; }
:where(#app, #netgraph-app) .tooltip[hidden] { display: none; }
:where(#app, #netgraph-app) .node-inspector-sheet { position: absolute; z-index: 7; overflow: hidden; }
:where(#app, #netgraph-app) .node-inspector-sheet[hidden] { display: none; }
:where(#app, #netgraph-app) .node-inspector { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); }
:where(#app, #netgraph-app) .node-inspector > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(139, 205, 214, 0.11); }
:where(#app, #netgraph-app) .node-inspector > header > div { display: grid; min-width: 0; gap: 2px; }
:where(#app, #netgraph-app) .node-inspector > header strong { overflow: hidden; color: #f0ffff; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
:where(#app, #netgraph-app) .node-inspector > header span { color: #8ca2af; font-size: 10px; text-transform: capitalize; }
:where(#app, #netgraph-app) .node-inspector-close { width: 44px; height: 44px; flex: none; border: 0; border-radius: 9px; color: #a9bdc8; background: transparent; cursor: pointer; font-size: 24px; }
:where(#app, #netgraph-app) .node-inspector-close:hover, :where(#app, #netgraph-app) .node-inspector-close:focus-visible { background: rgba(41, 184, 169, 0.16); outline-offset: -3px; }
:where(#app, #netgraph-app) .node-inspector dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; padding: 10px 13px; }
:where(#app, #netgraph-app) .node-inspector dl div { display: grid; min-width: 0; gap: 2px; }
:where(#app, #netgraph-app) .node-inspector dt { color: #708895; font-size: 9px; font-weight: 650; text-transform: uppercase; }
:where(#app, #netgraph-app) .node-inspector dd { overflow: hidden; margin: 0; color: #d9e9ef; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
:where(#app, #netgraph-app) .node-inspector h3 { margin: 0; padding: 6px 13px 7px; color: #8da3ae; font-size: 10px; font-weight: 700; }
:where(#app, #netgraph-app) .neighbor-list { overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid rgba(139, 205, 214, 0.09); }
:where(#app, #netgraph-app) .neighbor-row { display: grid; width: 100%; min-height: 54px; gap: 1px; border: 0; border-bottom: 1px solid rgba(139, 205, 214, 0.08); padding: 7px 13px; background: transparent; text-align: left; cursor: pointer; }
:where(#app, #netgraph-app) .neighbor-row strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
:where(#app, #netgraph-app) .neighbor-row span { color: #8298a5; font-size: 9px; text-transform: capitalize; }
:where(#app, #netgraph-app) .neighbor-row:hover, :where(#app, #netgraph-app) .neighbor-row:focus-visible { background: rgba(41, 184, 169, 0.13); }
:where(#app, #netgraph-app) .fatal { border-color: rgba(237, 114, 131, 0.35); }
+35 -220
View File
@@ -75,15 +75,7 @@ button { font: inherit; }
#packet-canvas { z-index: 4; }
.glass {
z-index: 5;
border: 1px solid var(--panel-border);
background: var(--panel-background);
box-shadow: var(--panel-shadow);
-webkit-backdrop-filter: blur(14px) saturate(1.18);
backdrop-filter: blur(14px) saturate(1.18);
transition: border-color 180ms ease, background-color 180ms ease, box-shadow 180ms ease;
}
.glass { border: 1px solid var(--panel-border); background: var(--panel-background); box-shadow: var(--panel-shadow); transition: border-color 180ms ease, background-color 180ms ease, box-shadow 180ms ease; }
@media (pointer: coarse), (max-width: 700px) {
.glass {
@@ -108,68 +100,31 @@ button { font: inherit; }
}
.toolbar > section { flex-shrink: 0; pointer-events: auto; }
.topbar {
display: flex;
align-items: center;
gap: 18px;
min-height: 44px;
padding: 0 12px;
border-radius: 12px;
}
.topbar { gap: 18px; min-height: 44px; border-radius: 12px; }
.brand, .netgraph-link, .status { display: flex; align-items: center; gap: 7px; }
.brand { min-height: 44px; border: 0; padding: 0 2px; color: #effdff; background: transparent; cursor: pointer; font-size: 13px; font-weight: 720; letter-spacing: 0.02em; }
.brand { min-height: 44px; border: 0; padding: 0 2px; color: #effdff; background: transparent; cursor: pointer; font-weight: 720; }
.brand:hover, .brand:focus-visible { color: #ffffff; outline: none; }
.brand:focus-visible { border-radius: 6px; box-shadow: 0 0 0 2px #63d9cc; }
.brand-mark { width: 9px; height: 9px; border: 2px solid #35cdbc; border-radius: 50%; box-shadow: 0 0 11px #36d7c2; }
.netgraph-link { min-height: 44px; padding: 0 3px; color: #9ccbc8; text-decoration: none; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; }
.netgraph-link:hover, .netgraph-link:focus-visible { color: var(--ink-strong); outline: none; }
.netgraph-link:focus-visible { border-radius: 6px; box-shadow: 0 0 0 2px #63d9cc; }
.netgraph-link > span:first-child { color: #63d9cc; text-shadow: 0 0 10px rgba(99, 217, 204, 0.68); }
.status { color: #aebdcb; font-size: 11px; font-weight: 600; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #86919b; }
.status[data-state="active"] .status-dot { background: #45df9a; box-shadow: 0 0 8px #45df9a; }
.status[data-state="quiet"] .status-dot { background: #70a99f; }
.status[data-state="reconnecting"] .status-dot { background: #e9c35b; animation: status-pulse 1.1s ease-in-out infinite; }
.status[data-state="offline"] .status-dot { background: #ed7283; }
.topbar.traffic-pulse .brand-mark { animation: traffic-brand 680ms ease-out; }
.status { font-weight: 600; }
.topbar.traffic-pulse .status-dot { animation: traffic-status 680ms ease-out; }
.traffic-meter {
display: inline-flex;
height: 12px;
align-items: flex-end;
gap: 2px;
margin-left: 2px;
}
.traffic-meter i {
width: 2px;
height: 2px;
border-radius: 2px;
background: rgba(var(--traffic-rgb), 0.32);
box-shadow: 0 0 0 rgba(var(--traffic-rgb), 0);
transition: height 180ms ease, background-color 180ms ease, box-shadow 180ms ease;
}
.traffic-meter { margin-left: 2px; }
.traffic-meter i { background: rgba(var(--traffic-rgb), 0.32); box-shadow: 0 0 0 rgba(var(--traffic-rgb), 0); transition: height 180ms ease, background-color 180ms ease, box-shadow 180ms ease; }
.traffic-meter[data-level="1"] i:nth-child(-n+1),
.traffic-meter[data-level="2"] i:nth-child(-n+2),
.traffic-meter[data-level="3"] i:nth-child(-n+3),
.traffic-meter[data-level="4"] i:nth-child(-n+4),
.traffic-meter[data-level="5"] i:nth-child(-n+5) {
height: 10px;
background: rgba(var(--traffic-rgb), 0.9);
box-shadow: 0 0 7px rgba(var(--traffic-rgb), 0.62);
}
.traffic-meter[data-level="5"] i:nth-child(-n+5) { background: rgba(var(--traffic-rgb), 0.9); }
.traffic-meter i:nth-child(2) { max-height: 8px; }
.traffic-meter i:nth-child(3) { max-height: 12px; }
.traffic-meter i:nth-child(4) { max-height: 7px; }
.traffic-meter i:nth-child(5) { max-height: 10px; }
.controls {
margin-left: auto;
display: flex;
overflow: visible;
border-radius: 12px;
}
.controls { margin-left: auto; border-radius: 12px; }
.layers-disclosure { position: relative; }
.layers-panel[hidden],
@@ -177,23 +132,9 @@ button { font: inherit; }
.sound-control { position: relative; display: flex; }
.find-control { position: relative; display: flex; }
.control-button {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 44px;
border: 0;
border-right: 1px solid rgba(139, 205, 214, 0.12);
padding: 0 11px;
color: #b9cad7;
background: transparent;
cursor: pointer;
font-size: 12px;
font-weight: 650;
transition: color 150ms ease, background-color 150ms ease, box-shadow 180ms ease, transform 150ms ease;
}
.control-button { gap: 6px; color: #b9cad7; font-weight: 650; transition: color 150ms ease, background-color 150ms ease, box-shadow 180ms ease, transform 150ms ease; }
.icon-button { border-right: 0; }
.control-button:hover, .control-button:focus-visible, .control-button.selected { color: var(--ink-strong); background: rgba(41, 184, 169, 0.14); outline: none; }
.control-button:hover, .control-button:focus-visible, .control-button.selected { color: var(--ink-strong); outline: none; }
#follow-button[data-mode="director"] { color: var(--ink-strong); box-shadow: inset 0 -1px rgba(99, 217, 204, 0.45), 0 0 18px rgba(50, 200, 187, 0.08); }
#app.traffic-awake #follow-button[data-mode="director"] > span:first-child { animation: director-lock 880ms ease-out; }
.control-button:focus-visible { outline: 2px solid #63d9cc; outline-offset: -3px; }
@@ -208,31 +149,11 @@ button { font: inherit; }
.control-button[data-sound-state="resume"] .sound-state,
.sound-panel[data-sound-state="resume"] header span { color: #f0c36a; }
.sound-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 9;
display: grid;
width: 244px;
gap: 11px;
padding: 13px;
border-radius: 12px;
}
.sound-panel { position: absolute; top: calc(100% + 8px); z-index: 9; display: grid; width: 244px; border-radius: 12px; }
.sound-panel[hidden] { display: none; }
.sound-panel header { display: flex; align-items: center; justify-content: space-between; color: var(--ink-strong); font-size: 12px; }
.sound-panel header span { color: #899da8; font-size: 11px; font-weight: 750; }
.sound-toggle {
min-height: 44px;
border: 1px solid rgba(99, 217, 204, 0.25);
border-radius: 9px;
color: var(--ink-strong);
background: rgba(38, 166, 155, 0.14);
cursor: pointer;
font-size: 12px;
font-weight: 700;
}
.sound-toggle:hover, .sound-toggle:focus-visible { background: rgba(38, 166, 155, 0.24); outline: 2px solid #63d9cc; outline-offset: 1px; }
.sound-panel header { color: var(--ink-strong); }
.sound-toggle { color: var(--ink-strong); font-weight: 700; }
.sound-toggle:hover, .sound-toggle:focus-visible { outline: 2px solid #63d9cc; }
.scene-control { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px; color: var(--muted); font-size: 11px; font-weight: 650; }
.scene-control select {
min-height: 36px;
@@ -245,52 +166,17 @@ button { font: inherit; }
}
.scene-control select:focus-visible { outline: 2px solid #63d9cc; outline-offset: 1px; }
.volume-control { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 5px 10px; color: var(--muted); font-size: 11px; font-weight: 650; }
.volume-control output { color: var(--ink-strong); font-variant-numeric: tabular-nums; }
.volume-control input { grid-column: 1 / -1; width: 100%; min-height: 44px; accent-color: #4dcfbe; cursor: pointer; }
.sound-panel p { display: flex; align-items: center; gap: 7px; margin: 0; color: #859aa5; font-size: 10px; }
.sound-activity { width: 7px; height: 7px; flex: none; border-radius: 50%; background: #41545c; }
.sound-activity.active { background: #6ce2c5; box-shadow: 0 0 9px rgba(108, 226, 197, 0.9); }
.find-panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 9;
display: grid;
width: min(292px, calc(100vw - 24px));
gap: 8px;
padding: 12px;
border-radius: 12px;
}
.volume-control output { color: var(--ink-strong); }
.volume-control input { min-height: 44px; }
.find-panel { position: absolute; top: calc(100% + 8px); z-index: 9; display: grid; border-radius: 12px; }
.find-panel[hidden] { display: none; }
.find-panel > label { color: var(--ink-strong); font-size: 11px; font-weight: 750; }
.find-panel input {
min-height: 44px;
width: 100%;
border: 1px solid rgba(99, 217, 204, 0.26);
border-radius: 8px;
padding: 0 11px;
color: var(--ink-strong);
background: rgba(4, 15, 20, 0.96);
font: inherit;
}
.find-panel > label { color: var(--ink-strong); font-weight: 750; }
.find-panel input { color: var(--ink-strong); font: inherit; }
.find-panel input:focus-visible { outline: 2px solid #63d9cc; outline-offset: 1px; }
.node-search-results { display: grid; max-height: min(330px, 52vh); overflow-y: auto; overscroll-behavior: contain; }
.node-search-results > p { margin: 6px 2px; color: #8297a4; font-size: 11px; }
.node-search-result {
display: grid;
min-height: 48px;
gap: 2px;
border: 0;
border-top: 1px solid rgba(139, 205, 214, 0.1);
padding: 7px 8px;
color: var(--ink-strong);
background: transparent;
text-align: left;
cursor: pointer;
}
.node-search-result strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.node-search-result span { color: #8197a5; font-size: 10px; text-transform: capitalize; }
.node-search-results > p { margin: 6px 2px; }
.node-search-result { min-height: 48px; color: var(--ink-strong); }
.node-search-result strong { font-size: 12px; }
.node-search-result span { color: #8197a5; font-size: 10px; }
.node-search-result:hover, .node-search-result:focus-visible, .node-search-result[aria-selected="true"] { background: rgba(41, 184, 169, 0.14); outline: none; }
.node-search-result:focus-visible { box-shadow: inset 0 0 0 2px #63d9cc; }
@@ -300,19 +186,7 @@ button { font: inherit; }
100% { filter: drop-shadow(0 0 0 rgba(99, 217, 204, 0)); transform: scale(1); }
}
.route-window {
position: static;
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 44px;
padding: 5px 8px 5px 10px;
border: 0;
border-radius: 0;
color: var(--muted);
font-size: 11px;
font-weight: 650;
}
.route-window { position: static; gap: 6px; padding: 5px 8px 5px 10px; border: 0; border-radius: 0; color: var(--muted); font-size: 11px; font-weight: 650; }
.route-window select {
min-height: 30px;
border: 1px solid rgba(139, 205, 214, 0.18);
@@ -325,18 +199,7 @@ button { font: inherit; }
}
.route-window select:focus-visible { outline: 2px solid #63d9cc; outline-offset: 1px; }
.legend {
position: absolute;
bottom: max(13px, env(safe-area-inset-bottom));
left: max(12px, env(safe-area-inset-left));
display: flex;
align-items: center;
padding: 8px 10px;
border-radius: 10px;
color: #93a5b6;
font-size: 11px;
font-weight: 600;
}
.legend { bottom: max(13px, env(safe-area-inset-bottom)); left: max(12px, env(safe-area-inset-left)); display: flex; align-items: center; padding: 8px 10px; border-radius: 10px; color: #93a5b6; font-size: 11px; font-weight: 600; }
.legend-items { display: flex; align-items: center; gap: 13px; }
.legend-items span, .focus-chip { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.legend i.node { width: 6px; height: 6px; border-radius: 50%; box-shadow: 0 0 5px currentColor; }
@@ -389,23 +252,9 @@ button { font: inherit; }
box-shadow: 0 0 6px var(--route-color);
}
.tooltip {
position: absolute;
z-index: 6;
display: grid;
gap: 3px;
min-width: 110px;
max-width: 220px;
padding: 8px 10px;
border-radius: 8px;
transform: translateX(-50%);
pointer-events: none;
}
.tooltip strong { overflow: hidden; color: #f3ffff; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.tooltip span { color: #92a6b7; font-size: 9px; text-transform: capitalize; }
.tooltip[hidden] { display: none; }
.tooltip { z-index: 6; display: grid; gap: 3px; min-width: 110px; max-width: 220px; padding: 8px 10px; transform: translateX(-50%); }
.tooltip strong { color: #f3ffff; font-size: 11px; }
.tooltip span { color: #92a6b7; }
/* Raise the popup wrapper's stacking context above traffic (4), below controls (8). */
.node-inspector-popup { z-index: 7; }
.node-inspector-popup .maplibregl-popup-content {
@@ -429,49 +278,15 @@ button { font: inherit; }
}
.node-inspector-popup .maplibregl-popup-close-button:hover,
.node-inspector-popup .maplibregl-popup-close-button:focus-visible { color: var(--ink-strong); background: rgba(41, 184, 169, 0.16); outline: 2px solid #63d9cc; outline-offset: -3px; }
.node-inspector-sheet {
position: absolute;
z-index: 7;
right: 8px;
bottom: max(8px, env(safe-area-inset-bottom));
left: 8px;
max-height: min(48vh, 420px);
overflow: hidden;
border-radius: 15px;
}
.node-inspector-sheet[hidden] { display: none; }
.node-inspector { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); min-width: 300px; max-height: min(430px, 65vh); }
.node-inspector > header { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 46px 9px 13px; border-bottom: 1px solid rgba(139, 205, 214, 0.11); }
.node-inspector > header > div { display: grid; min-width: 0; gap: 2px; }
.node-inspector > header strong { overflow: hidden; color: #f0ffff; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.node-inspector > header span { color: #8ca2af; font-size: 10px; text-transform: capitalize; }
.node-inspector-close { width: 44px; height: 44px; flex: none; border: 0; border-radius: 9px; color: #a9bdc8; background: transparent; cursor: pointer; font-size: 24px; }
.node-inspector-close:hover, .node-inspector-close:focus-visible { color: var(--ink-strong); background: rgba(41, 184, 169, 0.16); outline: 2px solid #63d9cc; outline-offset: -3px; }
.node-inspector dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; padding: 10px 13px; }
.node-inspector dl div { display: grid; min-width: 0; gap: 2px; }
.node-inspector dt { color: #708895; font-size: 9px; font-weight: 650; text-transform: uppercase; }
.node-inspector dd { overflow: hidden; margin: 0; color: #d9e9ef; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.node-inspector h3 { margin: 0; padding: 6px 13px 7px; color: #8da3ae; font-size: 10px; font-weight: 700; }
.neighbor-list { overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid rgba(139, 205, 214, 0.09); }
.neighbor-row { display: grid; width: 100%; min-height: 54px; gap: 1px; border: 0; border-bottom: 1px solid rgba(139, 205, 214, 0.08); padding: 7px 13px; color: var(--ink-strong); background: transparent; text-align: left; cursor: pointer; }
.neighbor-row strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.neighbor-row span { color: #8298a5; font-size: 9px; text-transform: capitalize; }
.neighbor-row:hover, .neighbor-row:focus-visible { background: rgba(41, 184, 169, 0.13); outline: none; }
.node-inspector-sheet { right: 8px; bottom: max(8px, env(safe-area-inset-bottom)); left: 8px; max-height: min(48vh, 420px); border-radius: 15px; }
.node-inspector { min-width: 300px; max-height: min(430px, 65vh); }
.node-inspector > header { min-height: 52px; padding: 10px 46px 9px 13px; }
.node-inspector-close:hover, .node-inspector-close:focus-visible { color: var(--ink-strong); outline: 2px solid #63d9cc; }
.neighbor-row { color: var(--ink-strong); }
.neighbor-row:hover, .neighbor-row:focus-visible { outline: none; }
.neighbor-row:focus-visible { box-shadow: inset 0 0 0 2px #63d9cc; }
.fatal {
position: absolute;
z-index: 8;
top: 50%;
left: 50%;
max-width: min(420px, calc(100% - 32px));
padding: 14px 18px;
border-color: rgba(237, 114, 131, 0.35);
border-radius: 12px;
color: #ffd7dc;
font-size: 12px;
transform: translate(-50%, -50%);
}
.fatal { position: absolute; z-index: 8; top: 50%; left: 50%; max-width: min(420px, calc(100% - 32px)); padding: 14px 18px; border-radius: 12px; color: #ffd7dc; font-size: 12px; transform: translate(-50%, -50%); }
.fatal[hidden] { display: none; }
.about-dialog {