From 03ad8142e65f1aff7baa5dac5466e26d3d423db2 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 13 Sep 2026 04:26:11 -0700 Subject: [PATCH] style: format the UI shell and component styles --- scripts/format-scope.json | 35 +- scripts/package-boundaries.json | 12 +- scripts/qa-scene-controls.mjs | 6 +- src/splitFlap.js | 47 +- src/standalone/ui.js | 156 +- src/testSupport/readStylesheet.mjs | 11 +- src/ui/applicationShell.js | 2178 +++++++++++++++++------- src/ui/panelLayoutController.js | 172 +- src/ui/panelPositionControls.js | 56 +- src/ui/recordingControls.js | 39 +- src/ui/shellElements.js | 72 +- src/ui/shellFeedback.js | 70 +- src/ui/shellLifecycle.test.mjs | 205 ++- src/ui/styles/cctv.css | 23 +- src/ui/styles/cockpit.css | 811 ++++++--- src/ui/styles/command-dock-compact.css | 55 +- src/ui/styles/command-dock-sliding.css | 139 +- src/ui/styles/command-dock-trays.css | 64 +- src/ui/styles/command-dock.css | 14 +- src/ui/styles/controls.css | 98 +- src/ui/styles/first-run.css | 124 +- src/ui/styles/foundation.css | 37 +- src/ui/styles/layers.css | 572 +++++-- src/ui/styles/location.css | 22 +- src/ui/styles/overlays.css | 21 +- src/ui/styles/provider-settings.css | 72 +- src/ui/styles/radio.css | 402 ++++- src/ui/styles/recording.css | 1 - src/ui/styles/responsive.css | 1 - src/ui/styles/scenes.css | 50 +- src/ui/styles/status.css | 102 +- src/ui/styles/voice-cost.css | 5 +- 32 files changed, 4126 insertions(+), 1546 deletions(-) diff --git a/scripts/format-scope.json b/scripts/format-scope.json index e375bee6d..eac00e93a 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -213,5 +213,38 @@ "src/ui/sceneControls.js", "src/ui/scenePresentation.js", "src/ui/sceneControls.test.mjs", - "scripts/qa-scene-controls.mjs" + "scripts/qa-scene-controls.mjs", + "src/ui.js", + "src/standalone/ui.js", + "src/ui/applicationShell.js", + "src/ui/panelLayoutController.js", + "src/ui/panelPositionControls.js", + "src/ui/recordingControls.js", + "src/ui/shellElements.js", + "src/ui/shellFeedback.js", + "src/ui/uiLifetime.js", + "src/ui/styles.js", + "src/ui/shellLifecycle.test.mjs", + "src/testSupport/readStylesheet.mjs", + "src/splitFlap.js", + "style.css", + "src/ui/styles/overlays.css", + "src/ui/styles/command-dock-trays.css", + "src/ui/styles/location.css", + "src/ui/styles/recording.css", + "src/ui/styles/status.css", + "src/ui/styles/layers.css", + "src/ui/styles/controls.css", + "src/ui/styles/cctv.css", + "src/ui/styles/foundation.css", + "src/ui/styles/first-run.css", + "src/ui/styles/scenes.css", + "src/ui/styles/command-dock.css", + "src/ui/styles/command-dock-sliding.css", + "src/ui/styles/provider-settings.css", + "src/ui/styles/radio.css", + "src/ui/styles/responsive.css", + "src/ui/styles/voice-cost.css", + "src/ui/styles/cockpit.css", + "src/ui/styles/command-dock-compact.css" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 0cc639119..f1ef8b5fe 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -425,9 +425,7 @@ "external": [] }, "ui-shell": { - "exports": [ - "./ui/shell" - ], + "exports": ["./ui/shell"], "modules": [ "src/bloom.js", "src/cctvFocusPolicy.js", @@ -522,14 +520,10 @@ "src/worldFocus.js", "src/ui/uiLifetime.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "ui-styles": { - "exports": [ - "./ui/styles" - ], + "exports": ["./ui/styles"], "modules": [ "src/ui/styles.js", "style.css", diff --git a/scripts/qa-scene-controls.mjs b/scripts/qa-scene-controls.mjs index 916561b40..e34bb03c7 100644 --- a/scripts/qa-scene-controls.mjs +++ b/scripts/qa-scene-controls.mjs @@ -210,7 +210,11 @@ try { ); // Escape can also collapse the Scene accordion. Reopen it through its // installed disclosure before testing the next visible user action. - if (await page.$eval('#scene-panel', (panel) => panel.classList.contains('collapsed'))) + if ( + await page.$eval('#scene-panel', (panel) => + panel.classList.contains('collapsed'), + ) + ) await page.click('[data-collapse-target="scene-panel"]'); await page.waitForSelector('#scene-download-btn', { visible: true }); await page.click('#scene-download-btn'); diff --git a/src/splitFlap.js b/src/splitFlap.js index cdd84dfae..17d677999 100644 --- a/src/splitFlap.js +++ b/src/splitFlap.js @@ -107,7 +107,8 @@ function positiveNumber(value, fallback) { } function nowMs() { - return typeof performance !== 'undefined' && typeof performance.now === 'function' + return typeof performance !== 'undefined' && + typeof performance.now === 'function' ? performance.now() : Date.now(); } @@ -235,7 +236,9 @@ export function visibleGlyphs(plan, elapsedMs, options = {}) { const turnRatio = Number.isFinite(Number(options.turnRatio)) ? Number(options.turnRatio) : FLAP_TURN_RATIO; - const elapsed = Number.isFinite(Number(elapsedMs)) ? Math.max(0, Number(elapsedMs)) : 0; + const elapsed = Number.isFinite(Number(elapsedMs)) + ? Math.max(0, Number(elapsedMs)) + : 0; const turn = charMs * turnRatio; let out = ''; for (const cell of plan?.cells || []) { @@ -246,9 +249,11 @@ export function visibleGlyphs(plan, elapsedMs, options = {}) { /** Whether the viewer asked for reduced motion. */ function prefersReducedMotion() { - return typeof window !== 'undefined' - && typeof window.matchMedia === 'function' - && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + return ( + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches + ); } /** @@ -274,7 +279,11 @@ function isVisible(element) { if (element.getClientRects().length === 0) return false; const view = element.ownerDocument?.defaultView; if (typeof view?.getComputedStyle !== 'function') return true; - for (let node = element; node && node.nodeType === 1; node = node.parentElement) { + for ( + let node = element; + node && node.nodeType === 1; + node = node.parentElement + ) { const style = view.getComputedStyle(node); if (style.visibility === 'hidden' || style.display === 'none') return false; if (Number(style.opacity) === 0) return false; @@ -301,10 +310,12 @@ function ensureHost(element) { // nothing changed. const text = element.firstElementChild; const cells = text?.nextElementSibling; - if (text?.classList?.contains(TEXT_CLASS) - && text.firstChild?.nodeType === 3 - && cells?.classList?.contains(CELLS_CLASS) - && !cells.nextElementSibling) { + if ( + text?.classList?.contains(TEXT_CLASS) && + text.firstChild?.nodeType === 3 && + cells?.classList?.contains(CELLS_CLASS) && + !cells.nextElementSibling + ) { return { text, cells }; } @@ -363,7 +374,8 @@ function easeWidth(element, fromWidth, toWidth, durationMs) { element.style.width = `${toWidth}px`; const finish = (event) => { - if (event && (event.target !== element || event.propertyName !== 'width')) return; + if (event && (event.target !== element || event.propertyName !== 'width')) + return; cancelWidthEase(element); clearSizing(element); }; @@ -449,16 +461,19 @@ export function setSplitFlapText(element, text, options = {}) { // previous label's glyphs, and those are what must flap away. const state = flapStates.get(element); const displayed = state - ? visibleGlyphs(state.plan, nowMs() - state.startedAt, { charMs: state.charMs }) + ? visibleGlyphs(state.plan, nowMs() - state.startedAt, { + charMs: state.charMs, + }) : settled; clearFlapTimer(element); const beforeWidth = measureWidth(element); - const animate = SPLIT_FLAP_ENABLED - && options.immediate !== true - && !prefersReducedMotion() - && isVisible(element); + const animate = + SPLIT_FLAP_ENABLED && + options.immediate !== true && + !prefersReducedMotion() && + isVisible(element); const plan = animate ? planSplitFlap(displayed, next, options) : null; diff --git a/src/standalone/ui.js b/src/standalone/ui.js index 56f989a69..7f46543bb 100644 --- a/src/standalone/ui.js +++ b/src/standalone/ui.js @@ -1,7 +1,14 @@ /** Compose UI controls with the application's existing engines and layer instances. */ import { StyleManager as ApplicationShell } from '../ui/applicationShell.js'; import { LocationSearch } from '../ui/location.js'; -import { CITY_POIS, GLOBE_VIEW, flyToGlobeView, flyToPresetLocation, flyToPOI, searchAndFlyTo } from '../locations.js'; +import { + CITY_POIS, + GLOBE_VIEW, + flyToGlobeView, + flyToPresetLocation, + flyToPOI, + searchAndFlyTo, +} from '../locations.js'; import { interruptCameraMotion } from '../cameraVerbs.js'; import { IntelHUD } from '../hud.js'; import { ShareLinkManager } from '../sharelink.js'; @@ -12,8 +19,14 @@ import { isCelestialRingStyleSupported, setKeyholeFadeTuning, } from '../celestialRing.js'; -import { destroyTrackedReadout, initTrackedReadout } from '../data/trackedReadout.js'; -import { destroyWorldOverlay, initWorldOverlay } from '../overlays/worldOverlay.js'; +import { + destroyTrackedReadout, + initTrackedReadout, +} from '../data/trackedReadout.js'; +import { + destroyWorldOverlay, + initWorldOverlay, +} from '../overlays/worldOverlay.js'; import { destroyDetection, initDetection, @@ -47,7 +60,11 @@ import { warmGroundFloor, } from '../data/groundFloor.js'; import { sampleMeshFloorCells } from '../data/meshFloorSampler.js'; -import { holdContinuousRender, releaseContinuousRender, governorRequestRender } from '../renderGovernor.js'; +import { + holdContinuousRender, + releaseContinuousRender, + governorRequestRender, +} from '../renderGovernor.js'; import { setScopeMaskEnabled, isScopeMaskEnabled, @@ -65,69 +82,72 @@ import { export class StyleManager extends ApplicationShell { constructor(viewer, options = {}) { - super(viewer, { ...options, services: { - CITY_POIS, - GLOBE_VIEW, - flyToGlobeView, - flyToPresetLocation, - flyToPOI, - searchAndFlyTo, - interruptCameraMotion, - IntelHUD, - ShareLinkManager, - OrbitController, - CelestialRing, - getKeyholeFadeTuning, - isCelestialRingStyleSupported, - setKeyholeFadeTuning, - destroyTrackedReadout, - initTrackedReadout, - destroyWorldOverlay, - initWorldOverlay, - destroyDetection, - initDetection, - cycleDetectionMode, - readDetectionDiagnostics, - getDetectionTuning, - getDetectionMode, - setDetectionModeByLabel, - suspendDetection, - resumeDetection, - setDetectionStyle, - setDetectionTuning, - trafficLayer, - flightsLayer, - militaryFlightsLayer, - isTr3b, - toggleTr3b, - satellitesLayer, - cctvLayer, - radioLayer, - bikeshareLayer, - aisLiveVesselsLayer, - militaryAwarenessLayer, - militaryInstallationsLayer, - rocketLaunchesLayer, - cachedGroundFloor, - cachedMeshFloor, - GROUND_FLOOR_LIFT_M, - meshFloorPreferred, - warmGroundFloor, - sampleMeshFloorCells, - holdContinuousRender, - releaseContinuousRender, - governorRequestRender, - setScopeMaskEnabled, - isScopeMaskEnabled, - setScopeMaskFeather, - getScopeMaskFeather, - setScopeTerminusOverride, - getScopeTerminusOverride, - clampScopeTerminusPct, - fetchRegionalBrief, - regionalDistanceM, - weatherCodeLabel, - LocationSearch - } }); + super(viewer, { + ...options, + services: { + CITY_POIS, + GLOBE_VIEW, + flyToGlobeView, + flyToPresetLocation, + flyToPOI, + searchAndFlyTo, + interruptCameraMotion, + IntelHUD, + ShareLinkManager, + OrbitController, + CelestialRing, + getKeyholeFadeTuning, + isCelestialRingStyleSupported, + setKeyholeFadeTuning, + destroyTrackedReadout, + initTrackedReadout, + destroyWorldOverlay, + initWorldOverlay, + destroyDetection, + initDetection, + cycleDetectionMode, + readDetectionDiagnostics, + getDetectionTuning, + getDetectionMode, + setDetectionModeByLabel, + suspendDetection, + resumeDetection, + setDetectionStyle, + setDetectionTuning, + trafficLayer, + flightsLayer, + militaryFlightsLayer, + isTr3b, + toggleTr3b, + satellitesLayer, + cctvLayer, + radioLayer, + bikeshareLayer, + aisLiveVesselsLayer, + militaryAwarenessLayer, + militaryInstallationsLayer, + rocketLaunchesLayer, + cachedGroundFloor, + cachedMeshFloor, + GROUND_FLOOR_LIFT_M, + meshFloorPreferred, + warmGroundFloor, + sampleMeshFloorCells, + holdContinuousRender, + releaseContinuousRender, + governorRequestRender, + setScopeMaskEnabled, + isScopeMaskEnabled, + setScopeMaskFeather, + getScopeMaskFeather, + setScopeTerminusOverride, + getScopeTerminusOverride, + clampScopeTerminusPct, + fetchRegionalBrief, + regionalDistanceM, + weatherCodeLabel, + LocationSearch, + }, + }); } } diff --git a/src/testSupport/readStylesheet.mjs b/src/testSupport/readStylesheet.mjs index d826bb30a..6ce02400e 100644 --- a/src/testSupport/readStylesheet.mjs +++ b/src/testSupport/readStylesheet.mjs @@ -3,9 +3,14 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; export function readStylesheet(file, ancestors = new Set()) { - const resolved = path.resolve(file instanceof URL ? fileURLToPath(file) : file); + const resolved = path.resolve( + file instanceof URL ? fileURLToPath(file) : file, + ); if (ancestors.has(resolved)) throw new Error('Circular stylesheet import'); const next = new Set(ancestors).add(resolved); - return readFileSync(resolved, 'utf8').replace(/@import ['"]([^'"]+)['"];\s*/g, - (_, imported) => readStylesheet(path.resolve(path.dirname(resolved), imported), next)); + return readFileSync(resolved, 'utf8').replace( + /@import ['"]([^'"]+)['"];\s*/g, + (_, imported) => + readStylesheet(path.resolve(path.dirname(resolved), imported), next), + ); } diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index b84da86f2..ab10cbdaf 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -8,13 +8,30 @@ import { RadioControls } from './radio.js'; import { LocationControls } from './location.js'; import { bindClearLayersControl } from './layers.js'; import { createMapSourceControls } from './mapSource.js'; -import { VisualEffects, STYLES, GLOBAL_POST_DEFAULTS, STYLE_PRESET_DEFAULTS, MILITARY_DETECTION_PRESET } from './effects.js'; +import { + VisualEffects, + STYLES, + GLOBAL_POST_DEFAULTS, + STYLE_PRESET_DEFAULTS, + MILITARY_DETECTION_PRESET, +} from './effects.js'; import { bindDisplayControls } from './displayControls.js'; -import { bindApplicationShortcuts, createStyleParameters } from './visualInput.js'; +import { + bindApplicationShortcuts, + createStyleParameters, +} from './visualInput.js'; import { PanelLayoutController } from './panelLayoutController.js'; -import { bindPanelDisclosure, collapsePanelOnEscape, createHoverDisclosure } from './panelDisclosure.js'; +import { + bindPanelDisclosure, + collapsePanelOnEscape, + createHoverDisclosure, +} from './panelDisclosure.js'; import * as Cesium from 'cesium'; -import { BLOOM_SCALE_VERSION, clampBloomIntensity, decodeBloomIntensity } from '../bloom.js'; +import { + BLOOM_SCALE_VERSION, + clampBloomIntensity, + decodeBloomIntensity, +} from '../bloom.js'; import { aircraftTrackingTarget, @@ -40,7 +57,11 @@ import { ShellFeedback } from './shellFeedback.js'; import { PanelPositionControls } from './panelPositionControls.js'; import { cockpitEntryAllowed } from '../contextModePolicy.js'; -import { applyCockpitVisionStageIntensities, captureCockpitVisionBaseline, normalizeCockpitVisionMode } from '../cockpitVisionPolicy.js'; +import { + applyCockpitVisionStageIntensities, + captureCockpitVisionBaseline, + normalizeCockpitVisionMode, +} from '../cockpitVisionPolicy.js'; import { applyContactsDetection, shareCacheNeedsHeal, @@ -145,11 +166,51 @@ export class StyleManager { * @param {Cesium.Viewer} viewer - The CesiumJS viewer instance. * @param {object} [options] */ - constructor(viewer, { mapStackController = null, placeSearch, services } = {}) { - const { IntelHUD, ShareLinkManager, OrbitController, CelestialRing, initTrackedReadout, initWorldOverlay, initDetection, setDetectionStyle, trafficLayer, flightsLayer, militaryFlightsLayer, isTr3b, toggleTr3b, satellitesLayer, cctvLayer, bikeshareLayer, aisLiveVesselsLayer, militaryAwarenessLayer, cachedGroundFloor, cachedMeshFloor, GROUND_FLOOR_LIFT_M, meshFloorPreferred, warmGroundFloor, sampleMeshFloorCells, holdContinuousRender, releaseContinuousRender, governorRequestRender, setScopeMaskEnabled, setScopeMaskFeather, setScopeTerminusOverride, clampScopeTerminusPct, fetchRegionalBrief, regionalDistanceM, weatherCodeLabel } = services; + constructor( + viewer, + { mapStackController = null, placeSearch, services } = {}, + ) { + const { + IntelHUD, + ShareLinkManager, + OrbitController, + CelestialRing, + initTrackedReadout, + initWorldOverlay, + initDetection, + setDetectionStyle, + trafficLayer, + flightsLayer, + militaryFlightsLayer, + isTr3b, + toggleTr3b, + satellitesLayer, + cctvLayer, + bikeshareLayer, + aisLiveVesselsLayer, + militaryAwarenessLayer, + cachedGroundFloor, + cachedMeshFloor, + GROUND_FLOOR_LIFT_M, + meshFloorPreferred, + warmGroundFloor, + sampleMeshFloorCells, + holdContinuousRender, + releaseContinuousRender, + governorRequestRender, + setScopeMaskEnabled, + setScopeMaskFeather, + setScopeTerminusOverride, + clampScopeTerminusPct, + fetchRegionalBrief, + regionalDistanceM, + weatherCodeLabel, + } = services; this.services = services; this._lifetime = new UiLifetime(); - this._recording = new RecordingControls({ syncShareState: () => this._syncShareState() }); + this._recording = new RecordingControls({ + syncShareState: () => this._syncShareState(), + }); Object.assign(this, readShellElements()); this._panelPosition = new PanelPositionControls({ syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel), @@ -157,13 +218,20 @@ export class StyleManager { syncCctvPanelViewport: () => this._syncCctvPanelViewport(), showToast: (message) => this._showToast(message), }); - this._feedback = new ShellFeedback({ readLayers: () => this._dataManager?.getAll?.() || [] }); + this._feedback = new ShellFeedback({ + readLayers: () => this._dataManager?.getAll?.() || [], + }); this._panelLayout = new PanelLayoutController({ - readHud: () => ({ visible: this.hud.visible, variant: this.hud.getVariant() }), + readHud: () => ({ + visible: this.hud.visible, + variant: this.hud.getVariant(), + }), scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(), syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel), - readDisplayScrollTop: () => this._displayPortalScrollRestoreOwner === 'standard' - ? this._standardDisplayScrollTop : (this._ppToggles?.scrollTop || 0), + readDisplayScrollTop: () => + this._displayPortalScrollRestoreOwner === 'standard' + ? this._standardDisplayScrollTop + : this._ppToggles?.scrollTop || 0, }); this.viewer = viewer; this.mapStackController = mapStackController; @@ -187,7 +255,6 @@ export class StyleManager { this._globeResetPromise = null; this._dataManager = null; - this._windowResizeHandler = null; this._cctvRequestFocusHandler = null; this._removeCctvRequestFocusListener = null; @@ -215,9 +282,14 @@ export class StyleManager { let storedDetectionAllocation = 'ELASTIC'; try { - storedDetectionAllocation = localStorage.getItem(DETECTION_ALLOCATION_STORAGE_KEY) || 'ELASTIC'; - } catch { /* storage can be unavailable in privacy/test contexts */ } - this._detectionAllocationPreference = normalizeAllocationStrategy(storedDetectionAllocation); + storedDetectionAllocation = + localStorage.getItem(DETECTION_ALLOCATION_STORAGE_KEY) || 'ELASTIC'; + } catch { + /* storage can be unavailable in privacy/test contexts */ + } + this._detectionAllocationPreference = normalizeAllocationStrategy( + storedDetectionAllocation, + ); this._mapStackChangeHandler = null; @@ -228,7 +300,7 @@ export class StyleManager { this._expandedCityId = null; this._activePoiIndex = null; this._currentTarget = null; // Cesium.Cartesian3 of current POI target - this._currentPoi = null; // Current POI data object + this._currentPoi = null; // Current POI data object // Formatted address of the last free-text geocode search. Preset pills set // _activeLocationId instead; a search has no preset record, so this is the // only thing the mini-status can report for it. @@ -272,18 +344,20 @@ export class StyleManager { regionalDistanceM, weatherCodeLabel, }, - onVisionChange: (mode, active, options) => this._setCockpitVision(mode, active, options), - onCameraTakeover: () => this._stampNavigation({ cancelPendingSelection: false }), - getInheritedVisionLabel: () => ( - STYLE_STATUS_LABELS[this.activeStyle] - || String(this.activeStyle || 'normal').toUpperCase() - ), - isEntryAllowed: () => cockpitEntryAllowed({ - contextMode: this._contextMode, - contextModeChanging: this._contextModeChanging, - flightsEnabled: !!this._dataManager?.isEnabled('flights'), - militaryEnabled: !!this._dataManager?.isEnabled('military'), - }), + onVisionChange: (mode, active, options) => + this._setCockpitVision(mode, active, options), + onCameraTakeover: () => + this._stampNavigation({ cancelPendingSelection: false }), + getInheritedVisionLabel: () => + STYLE_STATUS_LABELS[this.activeStyle] || + String(this.activeStyle || 'normal').toUpperCase(), + isEntryAllowed: () => + cockpitEntryAllowed({ + contextMode: this._contextMode, + contextModeChanging: this._contextModeChanging, + flightsEnabled: !!this._dataManager?.isEnabled('flights'), + militaryEnabled: !!this._dataManager?.isEnabled('military'), + }), onEntered: () => { // A new Cockpit session owns both side rails. Clear standard map-view // panels once on entry; NEXT/PREVIOUS never reaches this callback, so @@ -293,7 +367,10 @@ export class StyleManager { for (const panelId of COCKPIT_ENTRY_COLLAPSE_PANEL_IDS) { const panel = document.getElementById(panelId); if (panel) { - this._cockpitPanelRestore.set(panelId, panel.classList.contains('collapsed')); + this._cockpitPanelRestore.set( + panelId, + panel.classList.contains('collapsed'), + ); } this.setPanelCollapsed(panelId, true, { persist: false, @@ -316,11 +393,15 @@ export class StyleManager { } }, restoreTrackingFrame: (entity) => { - const [layerId, ...idParts] = String(entity?.gevTrackedId || '').split(':'); + const [layerId, ...idParts] = String(entity?.gevTrackedId || '').split( + ':', + ); const trackedId = idParts.join(':'); if (!trackedId) return false; - if (layerId === 'flights') return flightsLayer.refocusTrackedById?.(trackedId) === true; - if (layerId === 'military') return militaryFlightsLayer.refocusTrackedById?.(trackedId) === true; + if (layerId === 'flights') + return flightsLayer.refocusTrackedById?.(trackedId) === true; + if (layerId === 'military') + return militaryFlightsLayer.refocusTrackedById?.(trackedId) === true; return false; }, }); @@ -329,10 +410,11 @@ export class StyleManager { // Cesium canvas but below the HUD/detection/readout z ladder. this.celestialRing = new CelestialRing(viewer, { enabled: false, - onAutoDisable: () => this.setCelestialRingEnabled(false, { - syncShare: !!this.shareLinkManager, - focus: false, - }), + onAutoDisable: () => + this.setCelestialRingEnabled(false, { + syncShare: !!this.shareLinkManager, + focus: false, + }), }); // Share Link Manager @@ -362,10 +444,21 @@ export class StyleManager { } = state || {}; // Ignore the retired 'ai-edit' style from older share links. if (style && style !== 'normal' && style !== 'ai-edit') { - this.setStyle(style, { applyPreset: true, revealParameters: false, restore: true }); + this.setStyle(style, { + applyPreset: true, + revealParameters: false, + restore: true, + }); } - if (styleParams && style && this.stages[style] && STYLES[style]?.uniforms) { - for (const [uniformName, uniformValue] of Object.entries(styleParams)) { + if ( + styleParams && + style && + this.stages[style] && + STYLES[style]?.uniforms + ) { + for (const [uniformName, uniformValue] of Object.entries( + styleParams, + )) { if (!Object.hasOwn(STYLES[style].uniforms, uniformName)) continue; this.stages[style].uniforms[uniformName] = uniformValue; } @@ -388,25 +481,39 @@ export class StyleManager { this.hud.setMode(hudVisible ? 'on' : 'off'); this._updateHudButtonState(); } - if (typeof detectionDensity === 'number' && this._detectionDensitySlider) { + if ( + typeof detectionDensity === 'number' && + this._detectionDensitySlider + ) { const pct = canonicalizeDensity(detectionDensity); this._detectionDensitySlider.value = String(pct); this._detectionDensityValue.textContent = `${pct}%`; this._applyDetectionDensityFromUi(); } if (detectionAllocation) { - this._setDetectionAllocation(detectionAllocation, { syncShare: false, persist: false }); + this._setDetectionAllocation(detectionAllocation, { + syncShare: false, + persist: false, + }); } if (typeof detectionFadePct === 'number' && this._detectionFadeSlider) { this._detectionFadeSlider.value = String(detectionFadePct); } - if (typeof detectionOutsideOpacityPct === 'number' && this._detectionOpacitySlider) { - this._detectionOpacitySlider.value = String(detectionOutsideOpacityPct); + if ( + typeof detectionOutsideOpacityPct === 'number' && + this._detectionOpacitySlider + ) { + this._detectionOpacitySlider.value = String( + detectionOutsideOpacityPct, + ); } this._applyDetectionFadeFromUi(); if (detectionMode) this._setDetectionMode(detectionMode); if (typeof celestialRing === 'boolean') { - this.setCelestialRingEnabled(celestialRing, { syncShare: false, focus: false }); + this.setCelestialRingEnabled(celestialRing, { + syncShare: false, + focus: false, + }); } if (typeof scopeEnabled === 'boolean') { setScopeMaskEnabled(scopeEnabled); @@ -416,7 +523,8 @@ export class StyleManager { if (typeof scopeFeatherPct === 'number' && this._scopeFeatherSlider) { const pct = Math.max(0, Math.min(100, Math.round(scopeFeatherPct))); this._scopeFeatherSlider.value = String(pct); - if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`; + if (this._scopeFeatherValue) + this._scopeFeatherValue.textContent = `${pct}%`; setScopeMaskFeather(pct / 100); } // null restores the altitude-adaptive ramp; a number pins the terminus @@ -433,16 +541,22 @@ export class StyleManager { await mapStackRestore; this._syncShareState(); }, - isNavigationCurrent: (generation) => generation === this._navigationGeneration, + isNavigationCurrent: (generation) => + generation === this._navigationGeneration, cancelOwnedNavigation: () => this.viewer.camera.cancelFlight(), }); - this.shareLinkManager.setPanelStateProvider(() => this._buildSharePanelState()); + this.shareLinkManager.setPanelStateProvider(() => + this._buildSharePanelState(), + ); this.shareLinkManager.setStyleParamStateProvider((styleName) => { const shader = STYLES[styleName]; const stage = this.stages[styleName]; if (!shader?.uniforms || !stage) return null; return Object.fromEntries( - Object.keys(shader.uniforms).map((uniformName) => [uniformName, stage.uniforms[uniformName]]), + Object.keys(shader.uniforms).map((uniformName) => [ + uniformName, + stage.uniforms[uniformName], + ]), ); }); // Parse before panel chrome initializes so every valid share URL starts @@ -470,9 +584,21 @@ export class StyleManager { // Initialize detection overlay BEFORE style stages so the composite // stage is first in the post-process pipeline - initDetection(viewer, [trafficLayer, flightsLayer, militaryFlightsLayer, satellitesLayer, cctvLayer, bikeshareLayer, aisLiveVesselsLayer], (modeLabel) => { - this._updateDetectionButton(modeLabel); - }); + initDetection( + viewer, + [ + trafficLayer, + flightsLayer, + militaryFlightsLayer, + satellitesLayer, + cctvLayer, + bikeshareLayer, + aisLiveVesselsLayer, + ], + (modeLabel) => { + this._updateDetectionButton(modeLabel); + }, + ); initTrackedReadout(viewer); setDetectionStyle(this.activeStyle); this._applyDetectionDensityFromUi(); @@ -503,7 +629,9 @@ export class StyleManager { // Restore from URL hash if present const savedState = this._initialShareState; this._initialShareRestorePromise = savedState - ? new Promise((resolve) => { this._resolveInitialShareRestore = resolve; }) + ? new Promise((resolve) => { + this._resolveInitialShareRestore = resolve; + }) : Promise.resolve({ status: 'not-requested', share: null, layers: [] }); if (savedState) { this._hasShareState = true; @@ -517,26 +645,35 @@ export class StyleManager { this._initialShareRestoreTimeout = null; if (this._disposed) return; const generation = this._initialShareNavigationGeneration; - const applyCamera = Number.isInteger(generation) - && this._reassertNavigationHandoff(generation); + const applyCamera = + Number.isInteger(generation) && + this._reassertNavigationHandoff(generation); void (async () => { try { const share = await this.shareLinkManager.applyState(savedState, { applyCamera, navigationToken: generation, }); - const layers = await (this._layerStateRestorePromise || Promise.resolve([])); - const tracking = share.camera === 'applied' - ? await this._layerStateCoordinator?.restoreShareTrackingSelection?.() - : { - status: 'superseded', - cleared: this._layerStateCoordinator?.cancelPendingShareTracking?.( - 'shared-camera-superseded', - { clearSelection: true }, - ) === true, - }; + const layers = await (this._layerStateRestorePromise || + Promise.resolve([])); + const tracking = + share.camera === 'applied' + ? await this._layerStateCoordinator?.restoreShareTrackingSelection?.() + : { + status: 'superseded', + cleared: + this._layerStateCoordinator?.cancelPendingShareTracking?.( + 'shared-camera-superseded', + { clearSelection: true }, + ) === true, + }; this.shareLinkManager.completeInitialRestore(); - this._settleInitialShareRestore({ status: 'settled', share, layers, tracking }); + this._settleInitialShareRestore({ + status: 'settled', + share, + layers, + tracking, + }); } catch (error) { this.shareLinkManager.completeInitialRestore(); this._settleInitialShareRestore({ @@ -556,18 +693,27 @@ export class StyleManager { // and selected-subject Follow so delayed work cannot seize navigation. this._initialShareGestureHandler = () => { if ( - this._disposed - || !this._hasShareState - || !this._resolveInitialShareRestore - ) return; + this._disposed || + !this._hasShareState || + !this._resolveInitialShareRestore + ) + return; stampInitialShareGesture((options) => this._stampNavigation(options)); }; - this.viewer?.canvas?.addEventListener('pointerdown', this._initialShareGestureHandler, { - passive: true, - }); - this.viewer?.canvas?.addEventListener('wheel', this._initialShareGestureHandler, { - passive: true, - }); + this.viewer?.canvas?.addEventListener( + 'pointerdown', + this._initialShareGestureHandler, + { + passive: true, + }, + ); + this.viewer?.canvas?.addEventListener( + 'wheel', + this._initialShareGestureHandler, + { + passive: true, + }, + ); // Keep the parameter panel from overlapping toggle controls. this._layoutRightPanels(); @@ -583,51 +729,70 @@ export class StyleManager { // return so the time-driven reducer catches up on real elapsed time — and // re-arms its own ticker if the batch is still running. this._feedback.observeVisibility(); - this._cctvRequestFocusHandler = (event) => routeCctvFocusRequest( - event, - (activate, focus) => this._runExplicitCctvFocus(activate, focus), - (cameraId, durationSec) => cctvLayer.focusCamera(cameraId, durationSec), - ); + this._cctvRequestFocusHandler = (event) => + routeCctvFocusRequest( + event, + (activate, focus) => this._runExplicitCctvFocus(activate, focus), + (cameraId, durationSec) => cctvLayer.focusCamera(cameraId, durationSec), + ); this._removeCctvRequestFocusListener = registerCctvFocusRequestListener( window, this._cctvRequestFocusHandler, ); - this._worldRequestFocusHandler = (event) => routeWorldFocusRequest( - event, - (detail, fly) => this._runExplicitWorldFocus(detail, fly), - (detail) => flyToWorldTarget(this.viewer, detail), - ); + this._worldRequestFocusHandler = (event) => + routeWorldFocusRequest( + event, + (detail, fly) => this._runExplicitWorldFocus(detail, fly), + (detail) => flyToWorldTarget(this.viewer, detail), + ); this._removeWorldRequestFocusListener = registerWorldFocusRequestListener( window, this._worldRequestFocusHandler, ); - this._navigationOwnerChangedRemover = viewer.trackedEntityChanged.addEventListener((entity) => { - if (entity && !this._disposed) this._stampNavigation({ cancelPendingSelection: false }); - }); + this._navigationOwnerChangedRemover = + viewer.trackedEntityChanged.addEventListener((entity) => { + if (entity && !this._disposed) + this._stampNavigation({ cancelPendingSelection: false }); + }); // Vessel/installation focus flies without ever assigning a tracked entity, // so it cannot reach the listener above. It announces instead. - this._removeNavigationAuthorityListener = registerNavigationAuthorityListener( - window, - (event) => { + this._removeNavigationAuthorityListener = + registerNavigationAuthorityListener(window, (event) => { if (this._disposed) return; this._stampNavigation({ - cancelPendingSelection: event?.detail?.cancelPendingSelection !== false, + cancelPendingSelection: + event?.detail?.cancelPendingSelection !== false, }); - }, - ); + }); } // Compatibility reads for existing controls, scene snapshots and Cockpit. - get stages() { return this._visualEffects.stages; } - get transitions() { return this._visualEffects.transitions; } - get bloomEnabled() { return this._visualEffects.bloomEnabled; } - get sharpenEnabled() { return this._visualEffects.sharpenEnabled; } - get _bloomStage() { return this._visualEffects.bloomStage; } - get _sharpenStage() { return this._visualEffects.sharpenStage; } + get stages() { + return this._visualEffects.stages; + } + get transitions() { + return this._visualEffects.transitions; + } + get bloomEnabled() { + return this._visualEffects.bloomEnabled; + } + get sharpenEnabled() { + return this._visualEffects.sharpenEnabled; + } + get _bloomStage() { + return this._visualEffects.bloomStage; + } + get _sharpenStage() { + return this._visualEffects.sharpenStage; + } /** Advance camera authority and settle any older search UI immediately. */ - _stampNavigation({ cancelPendingSelection = true, clearSearchedLocation = true } = {}) { - const { flightsLayer, militaryFlightsLayer, satellitesLayer } = this.services; + _stampNavigation({ + cancelPendingSelection = true, + clearSearchedLocation = true, + } = {}) { + const { flightsLayer, militaryFlightsLayer, satellitesLayer } = + this.services; this._navigationGeneration += 1; // A newer destination owns the camera, so the last free-text search is no // longer where we are. DEFERRED navigation opts out here and clears at the @@ -635,33 +800,67 @@ export class StyleManager { // a lookup that fails must not blank a readout that is still true. if (clearSearchedLocation) this.clearSearchedLocation(); if (cancelPendingSelection) { - if (this._hasShareState && this._resolveInitialShareRestore && !this._layerStateCoordinator) { + if ( + this._hasShareState && + this._resolveInitialShareRestore && + !this._layerStateCoordinator + ) { this._initialShareSelectionSuperseded = true; } - const passivelyClearedShareSelection = this._layerStateCoordinator - ?.cancelPendingShareTracking?.( + const passivelyClearedShareSelection = + this._layerStateCoordinator?.cancelPendingShareTracking?.( 'superseded-by-explicit-navigation', { clearSelection: true }, ) === true; - try { flightsLayer.cancelPendingTrackingRestore?.(); } catch { /* best effort */ } - try { militaryFlightsLayer.cancelPendingTrackingRestore?.(); } catch { /* best effort */ } - try { satellitesLayer.cancelPendingTrackingRestore?.(); } catch { /* best effort */ } + try { + flightsLayer.cancelPendingTrackingRestore?.(); + } catch { + /* best effort */ + } + try { + militaryFlightsLayer.cancelPendingTrackingRestore?.(); + } catch { + /* best effort */ + } + try { + satellitesLayer.cancelPendingTrackingRestore?.(); + } catch { + /* best effort */ + } // A deliberate destination supersedes share-selected entities that have // not arrived yet. Active owners publish their clear when released. if (!passivelyClearedShareSelection && !flightsLayer.getTrackedInfo?.()) { - this._dataManager?.setLayerParams('flights', { - selectedFlightsTrackingId: null, - }, { origin: 'tool' }); + this._dataManager?.setLayerParams( + 'flights', + { + selectedFlightsTrackingId: null, + }, + { origin: 'tool' }, + ); } - if (!passivelyClearedShareSelection && !militaryFlightsLayer.getTrackedInfo?.()) { - this._dataManager?.setLayerParams('military', { - selectedMilitaryTrackingId: null, - }, { origin: 'tool' }); + if ( + !passivelyClearedShareSelection && + !militaryFlightsLayer.getTrackedInfo?.() + ) { + this._dataManager?.setLayerParams( + 'military', + { + selectedMilitaryTrackingId: null, + }, + { origin: 'tool' }, + ); } - if (!passivelyClearedShareSelection && !satellitesLayer.getTrackedInfo?.()) { - this._dataManager?.setLayerParams('satellites', { - selectedSatTrackingId: null, - }, { origin: 'tool' }); + if ( + !passivelyClearedShareSelection && + !satellitesLayer.getTrackedInfo?.() + ) { + this._dataManager?.setLayerParams( + 'satellites', + { + selectedSatTrackingId: null, + }, + { origin: 'tool' }, + ); } } if (this._activeLocationSearchGeneration !== null) { @@ -685,7 +884,15 @@ export class StyleManager { preserveCameraFlight = false, trackingOrigin = 'tool', } = {}) { - const { interruptCameraMotion, flightsLayer, militaryFlightsLayer, satellitesLayer, aisLiveVesselsLayer, militaryAwarenessLayer, rocketLaunchesLayer } = this.services; + const { + interruptCameraMotion, + flightsLayer, + militaryFlightsLayer, + satellitesLayer, + aisLiveVesselsLayer, + militaryAwarenessLayer, + rocketLaunchesLayer, + } = this.services; let contactSelected = false; try { contactSelected = Boolean( @@ -695,21 +902,43 @@ export class StyleManager { }), ); } catch { - try { flightsLayer.stopTracking?.({ origin: trackingOrigin }); } catch { /* best-effort release */ } - try { militaryFlightsLayer.stopTracking?.({ origin: trackingOrigin }); } catch { /* best-effort release */ } + try { + flightsLayer.stopTracking?.({ origin: trackingOrigin }); + } catch { + /* best-effort release */ + } + try { + militaryFlightsLayer.stopTracking?.({ origin: trackingOrigin }); + } catch { + /* best-effort release */ + } if (!preserveVesselSelection) { - try { aisLiveVesselsLayer.clearSelection?.(); } catch { /* best-effort release */ } + try { + aisLiveVesselsLayer.clearSelection?.(); + } catch { + /* best-effort release */ + } } } - try { satellitesLayer.stopTracking?.({ origin: trackingOrigin }); } catch { /* best-effort release */ } - try { rocketLaunchesLayer.releaseCameraOwnership?.(); } catch { /* best-effort release */ } + try { + satellitesLayer.stopTracking?.({ origin: trackingOrigin }); + } catch { + /* best-effort release */ + } + try { + rocketLaunchesLayer.releaseCameraOwnership?.(); + } catch { + /* best-effort release */ + } this.viewer.trackedEntity = undefined; interruptCameraMotion('explicit-navigation'); this._stopOrbit(); if (!preserveCameraFlight) this.viewer.camera.cancelFlight(); try { this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); - } catch { /* teardown race */ } + } catch { + /* teardown race */ + } return contactSelected; } @@ -727,7 +956,10 @@ export class StyleManager { } /** Accept a delayed lookup without releasing its current camera owner. */ - _beginDeferredNavigation(noun = 'location', { cancelPendingSelection = true } = {}) { + _beginDeferredNavigation( + noun = 'location', + { cancelPendingSelection = true } = {}, + ) { return beginDeferredNavigation({ disposed: this._disposed, cockpitActive: !!this.cockpitView?.active, @@ -735,7 +967,11 @@ export class StyleManager { showToast: (text) => this._showToast(text), // The searched-location readout survives the STAMP; only a flight that // actually starts invalidates it (see the release hook below). - stamp: () => this._stampNavigation({ cancelPendingSelection, clearSearchedLocation: false }), + stamp: () => + this._stampNavigation({ + cancelPendingSelection, + clearSearchedLocation: false, + }), }); } @@ -798,7 +1034,9 @@ export class StyleManager { * clamp its top. No-op until the panel has been positioned (explicit inline top). * @returns {void} */ - _reclampDraggablePanels() { return this._panelPosition._reclampDraggablePanels(); } + _reclampDraggablePanels() { + return this._panelPosition._reclampDraggablePanels(); + } /** * Creates one CesiumJS PostProcessStage per visual style and registers @@ -862,8 +1100,9 @@ export class StyleManager { restore: this._contactsDetectionRestore, // A map style picked DURING the session owns detection on the way out — // its auto-enable preset is younger than the entry snapshot. - styleOwnsDetection: !this._detectionUserOverridden - && Boolean(STYLE_PRESET_DEFAULTS[this.activeStyle]?.detection), + styleOwnsDetection: + !this._detectionUserOverridden && + Boolean(STYLE_PRESET_DEFAULTS[this.activeStyle]?.detection), // The snapshot must cover everything activation mutates — the preset // writes DENSITY as well as mode, so a mode-only snapshot returned // OFF @ 25% as OFF @ 75% and the next manual enable came back Dense. @@ -889,11 +1128,14 @@ export class StyleManager { // style's preset already matches), and returning early there left a copied // link claiming the operator's pre-Contacts values while the map showed // Dense @ 75%. - if (!shareCacheNeedsHeal({ - changed: result.changed, - hadOwnership, - hasOwnership: Boolean(result.restore), - })) return; + if ( + !shareCacheNeedsHeal({ + changed: result.changed, + hadOwnership, + hasOwnership: Boolean(result.restore), + }) + ) + return; if (result.changed) this._syncDetectionUiFromEngine(); this._syncShareState(); } @@ -904,8 +1146,11 @@ export class StyleManager { if (!this.stages) return; if (!active) { if (this._cockpitVisionRestore) { - for (const [name, intensity] of Object.entries(this._cockpitVisionRestore)) { - if (this.stages[name]) this._setStageIntensity(this.stages[name], intensity); + for (const [name, intensity] of Object.entries( + this._cockpitVisionRestore, + )) { + if (this.stages[name]) + this._setStageIntensity(this.stages[name], intensity); } } this._cockpitVisionRestore = null; @@ -916,10 +1161,17 @@ export class StyleManager { return; } if (!this._cockpitVisionRestore) { - this._cockpitVisionRestore = captureCockpitVisionBaseline(this.stages, this.transitions); + this._cockpitVisionRestore = captureCockpitVisionBaseline( + this.stages, + this.transitions, + ); } if (next === 'optical') { - applyCockpitVisionStageIntensities(this.stages, next, this._cockpitVisionRestore); + applyCockpitVisionStageIntensities( + this.stages, + next, + this._cockpitVisionRestore, + ); this._syncStagesEnabledFromIntensity(); this._cockpitVisionMode = next; this._syncIrBoost(); @@ -927,7 +1179,11 @@ export class StyleManager { this._revealCockpitStyleParameters({ openDisplay: revealParameters }); return; } - const target = applyCockpitVisionStageIntensities(this.stages, next, this._cockpitVisionRestore); + const target = applyCockpitVisionStageIntensities( + this.stages, + next, + this._cockpitVisionRestore, + ); this._syncStagesEnabledFromIntensity(); this._cockpitVisionMode = next; this._syncIrBoost(); // Cockpit vision override ('nvg'/'thermal' boost; CRT/NOIR clear) @@ -942,9 +1198,15 @@ export class StyleManager { * ('nvg'/'thermal', which can differ from the map preset in BOTH * directions), otherwise the map preset ('surveillance'/'thermal'). */ _syncIrBoost() { - const cockpitMode = this.cockpitView?.active ? this._cockpitVisionMode : null; - const effective = cockpitMode && cockpitMode !== 'optical' ? cockpitMode : this.activeStyle; - const irBoost = effective === 'surveillance' || effective === 'thermal' || effective === 'nvg'; + const cockpitMode = this.cockpitView?.active + ? this._cockpitVisionMode + : null; + const effective = + cockpitMode && cockpitMode !== 'optical' ? cockpitMode : this.activeStyle; + const irBoost = + effective === 'surveillance' || + effective === 'thermal' || + effective === 'nvg'; this._dataManager?.setLayerParams('flights', { irBoost }); this._dataManager?.setLayerParams('military', { irBoost }); // Fog blends distant geometry toward an effectively-BLACK color in this @@ -971,7 +1233,10 @@ export class StyleManager { _syncCockpitInheritedStyle() { if (!this.cockpitView?.active || !this.stages) return; this._cockpitVisionRestore = Object.fromEntries( - Object.keys(this.stages).map((name) => [name, name === this.activeStyle ? 1 : 0]), + Object.keys(this.stages).map((name) => [ + name, + name === this.activeStyle ? 1 : 0, + ]), ); for (const name of Object.keys(this.stages)) this.transitions.delete(name); this.cockpitView.setVisionMode(this.cockpitView.visionMode); @@ -979,17 +1244,27 @@ export class StyleManager { /** Reveal shared style parameters, optionally opening Cockpit Display first. */ _revealCockpitStyleParameters({ openDisplay = false } = {}) { - if (!this.cockpitView?.active || !this._sliderPanel?.classList.contains('active')) return; - if (openDisplay && this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') !== 'true') { + if ( + !this.cockpitView?.active || + !this._sliderPanel?.classList.contains('active') + ) + return; + if ( + openDisplay && + this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') !== 'true' + ) { this._setCockpitDisclosure?.('display', true); return; } - if (this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') !== 'true') return; + if (this._cockpitDisplayToggleBtn?.getAttribute('aria-expanded') !== 'true') + return; this._sliderPanel.classList.remove('collapsed'); this._syncPanelCollapseButton(this._sliderPanel); - this._lifetime.frame(() => this._lifetime.frame(() => { - this._sliderPanel?.scrollIntoView?.({ block: 'nearest' }); - })); + this._lifetime.frame(() => + this._lifetime.frame(() => { + this._sliderPanel?.scrollIntoView?.({ block: 'nearest' }); + }), + ); } /** @@ -998,7 +1273,9 @@ export class StyleManager { * @returns {void} */ _initBloomSharpen() { - this._visualEffects.initPostProcess(this._sharpenSlider ? parseInt(this._sharpenSlider.value, 10) / 100 : 0.6); + this._visualEffects.initPostProcess( + this._sharpenSlider ? parseInt(this._sharpenSlider.value, 10) / 100 : 0.6, + ); } /** @@ -1030,7 +1307,8 @@ export class StyleManager { governorRequestRender('bloom'); const clamped = clampBloomIntensity(intensity); if (this._bloomSlider) this._bloomSlider.value = String(clamped); - if (this._bloomSliderValue) this._bloomSliderValue.textContent = `${clamped}%`; + if (this._bloomSliderValue) + this._bloomSliderValue.textContent = `${clamped}%`; this._applyBloomIntensity(clamped); if (syncShare) this._syncShareState(); } @@ -1089,7 +1367,9 @@ export class StyleManager { this._sharpenSliderRow.classList.toggle('visible', this.sharpenEnabled); } if (this.sharpenEnabled && this._sharpenSlider) { - this._applySharpenIntensity(parseInt(this._sharpenSlider.value, 10) / 100); + this._applySharpenIntensity( + parseInt(this._sharpenSlider.value, 10) / 100, + ); } this._syncShareState(); this._layoutRightPanels(); @@ -1103,7 +1383,12 @@ export class StyleManager { * @returns {void} */ _initUI() { - const { cycleDetectionMode, setScopeMaskEnabled, isScopeMaskEnabled, setScopeMaskFeather } = this.services; + const { + cycleDetectionMode, + setScopeMaskEnabled, + isScopeMaskEnabled, + setScopeMaskFeather, + } = this.services; this._applicationShortcuts?.destroy(); this._applicationShortcuts = bindApplicationShortcuts({ documentRef: document, @@ -1125,7 +1410,8 @@ export class StyleManager { }, toggleOrbit: () => this._toggleOrbit(), toggleCleanView: () => this.toggleCleanView(), - toggleLayers: () => document.getElementById('data-panel').classList.toggle('active'), + toggleLayers: () => + document.getElementById('data-panel').classList.toggle('active'), cycleDetection: () => { this.shareLinkManager?.claimRestoreLane?.('visual'); this._detectionUserOverridden = true; @@ -1140,12 +1426,18 @@ export class StyleManager { this._displayControls = bindDisplayControls({ elements: { styleButtons: document.querySelectorAll('.style-btn'), - bloomButton: this._bloomBtn, bloomSlider: this._bloomSlider, - sharpenButton: this._sharpenBtn, sharpenSlider: this._sharpenSlider, - scopeButton: this._scopeBtn, scopeFeatherSlider: this._scopeFeatherSlider, - hudLayout: this._hudLayoutSelect, hudButton: this._hudBtn, - cleanViewButton: this._cleanViewBtn, cleanViewExitButton: this._cleanViewExitBtn, - densitySlider: this._detectionDensitySlider, detectionButton: this._detectionBtn, + bloomButton: this._bloomBtn, + bloomSlider: this._bloomSlider, + sharpenButton: this._sharpenBtn, + sharpenSlider: this._sharpenSlider, + scopeButton: this._scopeBtn, + scopeFeatherSlider: this._scopeFeatherSlider, + hudLayout: this._hudLayoutSelect, + hudButton: this._hudBtn, + cleanViewButton: this._cleanViewBtn, + cleanViewExitButton: this._cleanViewExitBtn, + densitySlider: this._detectionDensitySlider, + detectionButton: this._detectionBtn, allocationButtons: this._detectionAllocationBtns, fadeSliders: [this._detectionFadeSlider, this._detectionOpacitySlider], celestialButton: this._celestialBtn, @@ -1177,13 +1469,15 @@ export class StyleManager { setScopeFeather: (value) => { this.shareLinkManager?.claimRestoreLane?.('visual'); const pct = Math.max(0, Math.min(100, value || 0)); - if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`; + if (this._scopeFeatherValue) + this._scopeFeatherValue.textContent = `${pct}%`; setScopeMaskFeather(pct / 100); this._syncShareState(); }, setSharpenIntensity: (pct) => { this.shareLinkManager?.claimRestoreLane?.('visual'); - if (this._sharpenSliderValue) this._sharpenSliderValue.textContent = `${pct}%`; + if (this._sharpenSliderValue) + this._sharpenSliderValue.textContent = `${pct}%`; this._applySharpenIntensity(pct / 100); this._syncShareState(); }, @@ -1198,7 +1492,8 @@ export class StyleManager { this._detectionUserOverridden = true; const pct = canonicalizeDensity(value); this._detectionDensitySlider.value = String(pct); - if (this._detectionDensityValue) this._detectionDensityValue.textContent = `${pct}%`; + if (this._detectionDensityValue) + this._detectionDensityValue.textContent = `${pct}%`; this._applyDetectionDensityFromUi(); this._syncShareState(); }, @@ -1256,7 +1551,8 @@ export class StyleManager { controller: this.mapStackController, subscribe: (onChange) => { window.addEventListener('gev:map-stack-changed', onChange); - return () => window.removeEventListener('gev:map-stack-changed', onChange); + return () => + window.removeEventListener('gev:map-stack-changed', onChange); }, claimSelection: () => this.shareLinkManager?.claimRestoreLane?.('map'), onStateChanged: () => this._syncShareState(), @@ -1297,7 +1593,8 @@ export class StyleManager { if (!this._detectionDensitySlider) return; const pct = canonicalizeDensity(this._detectionDensitySlider.value); this._detectionDensitySlider.value = String(pct); - if (this._detectionDensityValue) this._detectionDensityValue.textContent = `${pct}%`; + if (this._detectionDensityValue) + this._detectionDensityValue.textContent = `${pct}%`; setDetectionTuning({ densityPct: pct }); this._updateDetectionButton(getDetectionMode()); } @@ -1305,16 +1602,28 @@ export class StyleManager { /** Apply responsive keyhole fade controls from normalized UI percentages. */ _applyDetectionFadeFromUi() { const { setKeyholeFadeTuning } = this.services; - const fadePct = Math.max(0, Math.min(40, Math.round(Number(this._detectionFadeSlider?.value) || 0))); + const fadePct = Math.max( + 0, + Math.min(40, Math.round(Number(this._detectionFadeSlider?.value) || 0)), + ); const outsideOpacityValue = this._detectionOpacitySlider?.value; const outsideOpacityPct = Math.max( 0, - Math.min(100, Math.round(outsideOpacityValue == null ? 3 : Number(outsideOpacityValue) || 0)), + Math.min( + 100, + Math.round( + outsideOpacityValue == null ? 3 : Number(outsideOpacityValue) || 0, + ), + ), ); - if (this._detectionFadeSlider) this._detectionFadeSlider.value = String(fadePct); - if (this._detectionFadeValue) this._detectionFadeValue.textContent = `${fadePct}%`; - if (this._detectionOpacitySlider) this._detectionOpacitySlider.value = String(outsideOpacityPct); - if (this._detectionOpacityValue) this._detectionOpacityValue.textContent = `${outsideOpacityPct}%`; + if (this._detectionFadeSlider) + this._detectionFadeSlider.value = String(fadePct); + if (this._detectionFadeValue) + this._detectionFadeValue.textContent = `${fadePct}%`; + if (this._detectionOpacitySlider) + this._detectionOpacitySlider.value = String(outsideOpacityPct); + if (this._detectionOpacityValue) + this._detectionOpacityValue.textContent = `${outsideOpacityPct}%`; setKeyholeFadeTuning({ fadeRatio: fadePct / 100, outsideOpacity: outsideOpacityPct / 100, @@ -1324,7 +1633,9 @@ export class StyleManager { _setDetectionAllocation(strategy, { syncShare = true, persist = true } = {}) { const { setDetectionTuning } = this.services; - const raw = String(strategy || '').trim().toUpperCase(); + const raw = String(strategy || '') + .trim() + .toUpperCase(); if (!ALLOCATION_STRATEGIES.includes(raw)) return false; const normalized = normalizeAllocationStrategy(raw); this._detectionAllocationPreference = normalized; @@ -1335,22 +1646,37 @@ export class StyleManager { button.setAttribute('aria-checked', String(active)); } if (persist) { - try { localStorage.setItem(DETECTION_ALLOCATION_STORAGE_KEY, normalized); } catch { /* best effort */ } + try { + localStorage.setItem(DETECTION_ALLOCATION_STORAGE_KEY, normalized); + } catch { + /* best effort */ + } } if (syncShare) this._syncShareState(); return true; } _syncDetectionUiFromEngine() { - const { getKeyholeFadeTuning, getDetectionTuning, getDetectionMode } = this.services; + const { getKeyholeFadeTuning, getDetectionTuning, getDetectionMode } = + this.services; const tuning = getDetectionTuning(); - if (this._detectionDensitySlider) this._detectionDensitySlider.value = String(tuning.densityPct); - if (this._detectionDensityValue) this._detectionDensityValue.textContent = `${tuning.densityPct}%`; - this._setDetectionAllocation(tuning.allocationStrategy, { syncShare: false, persist: false }); + if (this._detectionDensitySlider) + this._detectionDensitySlider.value = String(tuning.densityPct); + if (this._detectionDensityValue) + this._detectionDensityValue.textContent = `${tuning.densityPct}%`; + this._setDetectionAllocation(tuning.allocationStrategy, { + syncShare: false, + persist: false, + }); const fadeTuning = getKeyholeFadeTuning(); - if (this._detectionFadeSlider) this._detectionFadeSlider.value = String(Math.round(fadeTuning.fadeRatio * 100)); + if (this._detectionFadeSlider) + this._detectionFadeSlider.value = String( + Math.round(fadeTuning.fadeRatio * 100), + ); if (this._detectionOpacitySlider) { - this._detectionOpacitySlider.value = String(Math.round(fadeTuning.outsideOpacity * 100)); + this._detectionOpacitySlider.value = String( + Math.round(fadeTuning.outsideOpacity * 100), + ); } this._applyDetectionFadeFromUi(); this._updateDetectionButton(getDetectionMode()); @@ -1378,7 +1704,10 @@ export class StyleManager { _setHudVariant(variantName) { if (!variantName) return; this.hud.setVariant(variantName); - if (this._hudLayoutSelect && this._hudLayoutSelect.value !== this.hud.getVariant()) { + if ( + this._hudLayoutSelect && + this._hudLayoutSelect.value !== this.hud.getVariant() + ) { this._hudLayoutSelect.value = this.hud.getVariant(); } this._syncShareState(); @@ -1392,7 +1721,9 @@ export class StyleManager { * @param {{settle?: boolean}} [options] Whether to remeasure after transitions. * @returns {void} */ - _scheduleAdaptivePanelLayout(options0) { return this._panelLayout._scheduleAdaptivePanelLayout(options0); } + _scheduleAdaptivePanelLayout(options0) { + return this._panelLayout._scheduleAdaptivePanelLayout(options0); + } /** * Applies preset defaults (bloom, sharpen, shader uniforms, HUD variant) @@ -1420,7 +1751,9 @@ export class StyleManager { const bloomInput = preset.bloom || {}; if (typeof bloomInput.intensity === 'number' && this._bloomSlider) { - this._setBloomIntensity(clampBloomIntensity(bloomInput.intensity), { syncShare: false }); + this._setBloomIntensity(clampBloomIntensity(bloomInput.intensity), { + syncShare: false, + }); } if (typeof bloomInput.enabled === 'boolean') { this._setBloomEnabled(bloomInput.enabled); @@ -1428,7 +1761,10 @@ export class StyleManager { const sharpenInput = preset.sharpen || {}; if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) { - const sharpenPct = Math.max(0, Math.min(100, Math.round(sharpenInput.intensity))); + const sharpenPct = Math.max( + 0, + Math.min(100, Math.round(sharpenInput.intensity)), + ); this._sharpenSlider.value = String(sharpenPct); this._sharpenSliderValue.textContent = `${sharpenPct}%`; this._applySharpenIntensity(sharpenPct / 100); @@ -1469,7 +1805,8 @@ export class StyleManager { if (typeof det.densityPct === 'number' && this._detectionDensitySlider) { const pct = canonicalizeDensity(det.densityPct); this._detectionDensitySlider.value = String(pct); - if (this._detectionDensityValue) this._detectionDensityValue.textContent = `${pct}%`; + if (this._detectionDensityValue) + this._detectionDensityValue.textContent = `${pct}%`; this._applyDetectionDensityFromUi(); } if (det.mode) this._setDetectionMode(String(det.mode).toUpperCase()); @@ -1484,14 +1821,22 @@ export class StyleManager { _applyGlobalPostDefaults() { const defaults = GLOBAL_POST_DEFAULTS; if (typeof defaults.bloom?.intensity === 'number' && this._bloomSlider) { - this._setBloomIntensity(clampBloomIntensity(defaults.bloom.intensity), { syncShare: false }); + this._setBloomIntensity(clampBloomIntensity(defaults.bloom.intensity), { + syncShare: false, + }); } if (typeof defaults.bloom?.enabled === 'boolean') { this._setBloomEnabled(defaults.bloom.enabled); } - if (typeof defaults.sharpen?.intensity === 'number' && this._sharpenSlider) { - const sharpenPct = Math.max(0, Math.min(100, Math.round(defaults.sharpen.intensity))); + if ( + typeof defaults.sharpen?.intensity === 'number' && + this._sharpenSlider + ) { + const sharpenPct = Math.max( + 0, + Math.min(100, Math.round(defaults.sharpen.intensity)), + ); this._sharpenSlider.value = String(sharpenPct); this._sharpenSliderValue.textContent = `${sharpenPct}%`; this._applySharpenIntensity(sharpenPct / 100); @@ -1511,25 +1856,35 @@ export class StyleManager { if (defaults.detectionMode) { this._setDetectionMode(defaults.detectionMode); } - if (typeof defaults.detectionDensity === 'number' && this._detectionDensitySlider) { + if ( + typeof defaults.detectionDensity === 'number' && + this._detectionDensitySlider + ) { const density = canonicalizeDensity(defaults.detectionDensity); this._detectionDensitySlider.value = String(density); this._detectionDensityValue.textContent = `${density}%`; this._applyDetectionDensityFromUi(); } this._setDetectionAllocation( - this._detectionAllocationPreference || defaults.detectionAllocation || 'ELASTIC', + this._detectionAllocationPreference || + defaults.detectionAllocation || + 'ELASTIC', { syncShare: false, persist: false }, ); if (this._detectionFadeSlider) { this._detectionFadeSlider.value = String(defaults.detectionFadePct ?? 7); } if (this._detectionOpacitySlider) { - this._detectionOpacitySlider.value = String(defaults.detectionOutsideOpacityPct ?? 1); + this._detectionOpacitySlider.value = String( + defaults.detectionOutsideOpacityPct ?? 1, + ); } this._applyDetectionFadeFromUi(); if (typeof defaults.celestialRing === 'boolean') { - this.setCelestialRingEnabled(defaults.celestialRing, { syncShare: false, focus: false }); + this.setCelestialRingEnabled(defaults.celestialRing, { + syncShare: false, + focus: false, + }); } } @@ -1562,28 +1917,41 @@ export class StyleManager { _syncShareState() { if (this._disposed) return; - const { getDetectionTuning, isScopeMaskEnabled, getScopeMaskFeather, getScopeTerminusOverride } = this.services; + const { + getDetectionTuning, + isScopeMaskEnabled, + getScopeMaskFeather, + getScopeTerminusOverride, + } = this.services; const detection = this._shareableDetectionState(); - this.shareLinkManager.onToggleChange(this.bloomEnabled, this.sharpenEnabled, { - bloomIntensity: this._getBloomIntensity(), - bloomVersion: BLOOM_SCALE_VERSION, - sharpenIntensity: parseInt(this._sharpenSlider?.value || '49', 10), - hudVariant: this.hud.getVariant(), - hudVisible: this.hud.visible, - detectionMode: detection.mode, - detectionDensity: detection.densityPct, - detectionAllocation: getDetectionTuning().allocationStrategy, - detectionFadePct: parseInt(this._detectionFadeSlider?.value || '7', 10), - detectionOutsideOpacityPct: parseInt(this._detectionOpacitySlider?.value || '1', 10), - celestialRingEnabled: this.celestialRingEnabled, - scopeEnabled: isScopeMaskEnabled(), - scopeFeatherPct: Math.round(getScopeMaskFeather() * 100), - // null when adaptive — the share layer omits `sce` entirely in that case. - scopeTerminusPct: getScopeTerminusOverride() == null - ? null - : Math.round(getScopeTerminusOverride() * 100), - mapStack: this.mapStackController?.getActiveId?.() || 'photoreal', - }); + this.shareLinkManager.onToggleChange( + this.bloomEnabled, + this.sharpenEnabled, + { + bloomIntensity: this._getBloomIntensity(), + bloomVersion: BLOOM_SCALE_VERSION, + sharpenIntensity: parseInt(this._sharpenSlider?.value || '49', 10), + hudVariant: this.hud.getVariant(), + hudVisible: this.hud.visible, + detectionMode: detection.mode, + detectionDensity: detection.densityPct, + detectionAllocation: getDetectionTuning().allocationStrategy, + detectionFadePct: parseInt(this._detectionFadeSlider?.value || '7', 10), + detectionOutsideOpacityPct: parseInt( + this._detectionOpacitySlider?.value || '1', + 10, + ), + celestialRingEnabled: this.celestialRingEnabled, + scopeEnabled: isScopeMaskEnabled(), + scopeFeatherPct: Math.round(getScopeMaskFeather() * 100), + // null when adaptive — the share layer omits `sce` entirely in that case. + scopeTerminusPct: + getScopeTerminusOverride() == null + ? null + : Math.round(getScopeTerminusOverride() * 100), + mapStack: this.mapStackController?.getActiveId?.() || 'photoreal', + }, + ); } /** @@ -1592,7 +1960,9 @@ export class StyleManager { * @param {boolean} [forceShow=false] - Force the chip visible regardless of busy state. * @returns {void} */ - _updateTrafficSyncChip(forceShow, now) { return this._feedback._updateTrafficSyncChip(forceShow, now); } + _updateTrafficSyncChip(forceShow, now) { + return this._feedback._updateTrafficSyncChip(forceShow, now); + } /** * Initializes panel collapse buttons and restores persisted collapsed state. @@ -1600,34 +1970,52 @@ export class StyleManager { * @returns {void} */ _initPanelChrome() { - for (const control of this._panelDisclosureControls || []) control.destroy(); + for (const control of this._panelDisclosureControls || []) + control.destroy(); this._panelDisclosureControls = []; const targets = new Map(); - document.querySelectorAll('.panel-collapse-btn[data-collapse-target]').forEach((button) => { - const targetId = button.dataset.collapseTarget; - if (!targetId) return; - if (!targets.has(targetId)) targets.set(targetId, []); - targets.get(targetId).push(button); - }); + document + .querySelectorAll('.panel-collapse-btn[data-collapse-target]') + .forEach((button) => { + const targetId = button.dataset.collapseTarget; + if (!targetId) return; + if (!targets.has(targetId)) targets.set(targetId, []); + targets.get(targetId).push(button); + }); for (const [targetId, buttons] of targets) { const panel = document.getElementById(targetId); if (!panel) continue; - this._panelDisclosureControls.push(bindPanelDisclosure({ - panel, - buttons, - onChange: (collapsed, options) => this.setPanelCollapsed(targetId, collapsed, options), - onEscape: (event) => this._collapsePanelOnEscape(event, targetId), - })); + this._panelDisclosureControls.push( + bindPanelDisclosure({ + panel, + buttons, + onChange: (collapsed, options) => + this.setPanelCollapsed(targetId, collapsed, options), + onEscape: (event) => this._collapsePanelOnEscape(event, targetId), + }), + ); this._restorePanelCollapsedState(targetId, { allowStored: !this._initialShareState, }); } // The command dock always starts compact; either wing reveals on hover, // focus, or click and collapses again after the interaction moves away. - this.setPanelCollapsed('control-panel', true, { syncShare: false, persist: false }); - this.setPanelCollapsed('location-bar', true, { syncShare: false, persist: false }); - this._initAutoHoverPanel('control-panel', { openDelayMs: 140, closeDelayMs: 420 }); - this._initAutoHoverPanel('location-bar', { openDelayMs: 140, closeDelayMs: 420 }); + this.setPanelCollapsed('control-panel', true, { + syncShare: false, + persist: false, + }); + this.setPanelCollapsed('location-bar', true, { + syncShare: false, + persist: false, + }); + this._initAutoHoverPanel('control-panel', { + openDelayMs: 140, + closeDelayMs: 420, + }); + this._initAutoHoverPanel('location-bar', { + openDelayMs: 140, + closeDelayMs: 420, + }); this._initCommandDockPins(); this._initCommandDockTrayMetrics(); this._maybeNotifyLayoutReset(); @@ -1646,7 +2034,8 @@ export class StyleManager { _collapsePanelOnEscape(event, panelId) { return collapsePanelOnEscape(event, { panel: document.getElementById(panelId), - onChange: (collapsed, options) => this.setPanelCollapsed(panelId, collapsed, options), + onChange: (collapsed, options) => + this.setPanelCollapsed(panelId, collapsed, options), beforeCollapse: () => { if (panelId !== 'location-bar' || !this._locationSearch) return; this._locationSearch.classList.remove('expanded'); @@ -1662,31 +2051,38 @@ export class StyleManager { * @returns {void} */ _initCommandDockPins() { - document.querySelectorAll('.dock-pin-btn[data-pin-target]').forEach((button) => { - this._lifetime.listen(button, 'click', (event) => { - event.stopPropagation(); - const panelId = button.dataset.pinTarget; - this._setCommandDockPanelPinState(panelId); + document + .querySelectorAll('.dock-pin-btn[data-pin-target]') + .forEach((button) => { + this._lifetime.listen(button, 'click', (event) => { + event.stopPropagation(); + const panelId = button.dataset.pinTarget; + this._setCommandDockPanelPinState(panelId); + }); }); - }); } - _setCommandDockPanelPinState(panelId, pin, { - restore = false, - persist = true, - syncShare = true, - } = {}) { + _setCommandDockPanelPinState( + panelId, + pin, + { restore = false, persist = true, syncShare = true } = {}, + ) { const panelEl = document.getElementById(panelId); - const button = document.querySelector(`.dock-pin-btn[data-pin-target="${panelId}"]`); + const button = document.querySelector( + `.dock-pin-btn[data-pin-target="${panelId}"]`, + ); if (!panelEl || !button) return undefined; - const shouldPin = typeof pin === 'boolean' - ? pin - : !panelEl.classList.contains('dock-pinned'); + const shouldPin = + typeof pin === 'boolean' + ? pin + : !panelEl.classList.contains('dock-pinned'); panelEl.classList.toggle('dock-pinned', shouldPin); button.setAttribute('aria-pressed', String(shouldPin)); - document.querySelectorAll('#command-dock .dock-pinned-top').forEach((pinnedPanel) => { - pinnedPanel.classList.remove('dock-pinned-top'); - }); + document + .querySelectorAll('#command-dock .dock-pinned-top') + .forEach((pinnedPanel) => { + pinnedPanel.classList.remove('dock-pinned-top'); + }); if (shouldPin) { panelEl.classList.add('dock-pinned-top'); this.setPanelCollapsed(panelId, false, { @@ -1696,7 +2092,9 @@ export class StyleManager { syncShare: false, }); } else { - const remainingPinnedPanel = document.querySelector('#command-dock .dock-pinned'); + const remainingPinnedPanel = document.querySelector( + '#command-dock .dock-pinned', + ); remainingPinnedPanel?.classList.add('dock-pinned-top'); if (!restore && !panelEl.matches(':hover')) { this.setPanelCollapsed(panelId, true, { @@ -1719,21 +2117,27 @@ export class StyleManager { * without hardcoded content dimensions. * @returns {void} */ - _initCommandDockTrayMetrics() { return this._panelLayout._initCommandDockTrayMetrics(); } + _initCommandDockTrayMetrics() { + return this._panelLayout._initCommandDockTrayMetrics(); + } /** * Writes each pinned tray height and their combined stack height as CSS * variables. The most recently pinned tray forms the upper level. * @returns {void} */ - _updateCommandDockTrayStack() { return this._panelLayout._updateCommandDockTrayStack(); } + _updateCommandDockTrayStack() { + return this._panelLayout._updateCommandDockTrayStack(); + } /** * One-time toast when stored v6 panel positions are superseded by the v7 * layout defaults (positions reset; collapsed states are preserved). * @returns {void} */ - _maybeNotifyLayoutReset() { return this._panelPosition._maybeNotifyLayoutReset(); } + _maybeNotifyLayoutReset() { + return this._panelPosition._maybeNotifyLayoutReset(); + } /** * Configures intentional hover-expand / leave-collapse behavior on a panel. @@ -1746,7 +2150,10 @@ export class StyleManager { * @param {number} [options.closeDelayMs=1000] - Delay after pointer leaves before collapsing. * @returns {void} */ - _initAutoHoverPanel(panelId, { openDelayMs = 850, closeDelayMs = 1000 } = {}) { + _initAutoHoverPanel( + panelId, + { openDelayMs = 850, closeDelayMs = 1000 } = {}, + ) { const panel = document.getElementById(panelId); if (!panel) return; this._hoverPanelControls ??= new Map(); @@ -1758,11 +2165,15 @@ export class StyleManager { openDelayMs, closeDelayMs, isActive: () => !this._disposed, - onChange: (collapsed, options) => this.setPanelCollapsed(panelId, collapsed, options), + onChange: (collapsed, options) => + this.setPanelCollapsed(panelId, collapsed, options), onEscape: (event) => this._collapsePanelOnEscape(event, panelId), - focusTarget: panelId === 'control-panel' ? () => ( - panel.querySelector('.map-stack-chip.active') || panel.querySelector('.map-stack-chip') - ) : null, + focusTarget: + panelId === 'control-panel' + ? () => + panel.querySelector('.map-stack-chip.active') || + panel.querySelector('.map-stack-chip') + : null, }); this._hoverPanelControls.set(panelId, controller); if (panelId === 'control-panel') { @@ -1776,7 +2187,9 @@ export class StyleManager { * and left accordion remain fixed so their HUD alignment is deterministic. * @returns {void} */ - _initPanelDrag() { return this._panelPosition._initPanelDrag(); } + _initPanelDrag() { + return this._panelPosition._initPanelDrag(); + } _persistAwarenessSelection(event, cleared = false) { if (!this._dataManager) return; @@ -1786,26 +2199,38 @@ export class StyleManager { const config = { flights: { key: 'selectedFlightsTrackingId', - normalize: (value) => String(value ?? '').trim().toLowerCase() || null, + normalize: (value) => + String(value ?? '') + .trim() + .toLowerCase() || null, }, military: { key: 'selectedMilitaryTrackingId', - normalize: (value) => String(value ?? '').trim().toLowerCase() || null, + normalize: (value) => + String(value ?? '') + .trim() + .toLowerCase() || null, }, satellites: { key: 'selectedSatTrackingId', normalize: (value) => { const candidate = Number(value); - return Number.isFinite(candidate) && candidate > 0 ? Math.trunc(candidate) : null; + return Number.isFinite(candidate) && candidate > 0 + ? Math.trunc(candidate) + : null; }, }, }[layerId]; if (!config) return; const selectedValue = cleared ? null : config.normalize(event?.detail?.id); if (cleared || selectedValue === null) { - this._dataManager.adoptLayerParams?.(layerId, { - [config.key]: selectedValue, - }, { origin }); + this._dataManager.adoptLayerParams?.( + layerId, + { + [config.key]: selectedValue, + }, + { origin }, + ); return; } // A direct selection promotes a Context-owned tracker dependency into @@ -1826,11 +2251,19 @@ export class StyleManager { ['satellites', 'selectedSatTrackingId'], ]) { if (otherLayerId === layerId) continue; - this._dataManager.setLayerParams(otherLayerId, { [otherKey]: null }, { origin }); + this._dataManager.setLayerParams( + otherLayerId, + { [otherKey]: null }, + { origin }, + ); } - this._dataManager.adoptLayerParams?.(layerId, { - [config.key]: selectedValue, - }, { origin }); + this._dataManager.adoptLayerParams?.( + layerId, + { + [config.key]: selectedValue, + }, + { origin }, + ); } /** @@ -1859,10 +2292,18 @@ export class StyleManager { this._cctvControls.connect(); this._radioControls.connect(); if (!this._awarenessSelectedHandler) { - this._awarenessSelectedHandler = (event) => this._persistAwarenessSelection(event, false); - this._awarenessClearedHandler = (event) => this._persistAwarenessSelection(event, true); - window.addEventListener('gev:awareness-subject-selected', this._awarenessSelectedHandler); - window.addEventListener('gev:awareness-subject-cleared', this._awarenessClearedHandler); + this._awarenessSelectedHandler = (event) => + this._persistAwarenessSelection(event, false); + this._awarenessClearedHandler = (event) => + this._persistAwarenessSelection(event, true); + window.addEventListener( + 'gev:awareness-subject-selected', + this._awarenessSelectedHandler, + ); + window.addEventListener( + 'gev:awareness-subject-cleared', + this._awarenessClearedHandler, + ); } this._layerStateCoordinator?.destroy(); this._layerStateCoordinator = null; @@ -1872,8 +2313,10 @@ export class StyleManager { this._dataManager, this.shareLinkManager, { - onDurableStateChange: (state) => this._syncModels3dFromLayerState(state), - onTrackingRestoreStatus: (result) => this._handleShareTrackingRestoreStatus(result), + onDurableStateChange: (state) => + this._syncModels3dFromLayerState(state), + onTrackingRestoreStatus: (result) => + this._handleShareTrackingRestoreStatus(result), }, ); this._layerStateRestorePromise = this._layerStateCoordinator.start({ @@ -1890,7 +2333,9 @@ export class StyleManager { ); } void this._layerStateRestorePromise.then(() => { - this._syncModels3dFromLayerState(this._layerStateCoordinator?.getDurableState()); + this._syncModels3dFromLayerState( + this._layerStateCoordinator?.getDurableState(), + ); }); } } @@ -1917,27 +2362,38 @@ export class StyleManager { this._updateGlobalLoadingFeedback(); } } - if (result.classification === 'followed' || result.classification === 'cancelled') return; + if ( + result.classification === 'followed' || + result.classification === 'cancelled' + ) + return; // A stale terminal result must never replace a newer target's acquisition. if (this._shareTrackingAcquiringKey) return; const noticeGeneration = ownsAcquiringNotice ? this._shareTrackingNoticeGeneration : ++this._shareTrackingNoticeGeneration; const subject = result.label || 'entity'; - const message = result.classification === 'expired' - ? `Shared ${subject} follow expired` - : result.classification === 'source-unavailable' - ? `Shared ${subject} could not be restored — feed unavailable` - : `Shared ${subject} is unavailable`; + const message = + result.classification === 'expired' + ? `Shared ${subject} follow expired` + : result.classification === 'source-unavailable' + ? `Shared ${subject} could not be restored — feed unavailable` + : `Shared ${subject} is unavailable`; const showAfterStartupCover = () => { this._lifetime.frame(() => { - if (!canPresentDeferredStatusNotice( - noticeGeneration, - this._shareTrackingNoticeGeneration, - this._disposed, - )) return; + if ( + !canPresentDeferredStatusNotice( + noticeGeneration, + this._shareTrackingNoticeGeneration, + this._disposed, + ) + ) + return; const startupCover = document.getElementById('loading-screen'); - if (!startupCover || getComputedStyle(startupCover).visibility === 'hidden') { + if ( + !startupCover || + getComputedStyle(startupCover).visibility === 'hidden' + ) { this._showGlobalStatusNotice(message); return; } @@ -1946,13 +2402,21 @@ export class StyleManager { const showOnce = () => { removeStartupListener(); if (fallbackTimer) this._lifetime.cancelTimeout(fallbackTimer); - if (canPresentDeferredStatusNotice( - noticeGeneration, - this._shareTrackingNoticeGeneration, - this._disposed, - )) this._showGlobalStatusNotice(message); + if ( + canPresentDeferredStatusNotice( + noticeGeneration, + this._shareTrackingNoticeGeneration, + this._disposed, + ) + ) + this._showGlobalStatusNotice(message); }; - removeStartupListener = this._lifetime.listen(startupCover, 'transitionend', showOnce, { once: true }); + removeStartupListener = this._lifetime.listen( + startupCover, + 'transitionend', + showOnce, + { once: true }, + ); fallbackTimer = this._lifetime.timeout(showOnce, 1000); }); }; @@ -1963,9 +2427,15 @@ export class StyleManager { showAfterStartupCover(); } - get _contextMode() { return this._contextControls?._contextMode ?? null; } - get _contextModeChanging() { return this._contextControls?._contextModeChanging ?? false; } - get _preservePanelStateDuringLayerClear() { return this._contextControls?._preservePanelStateDuringLayerClear ?? false; } + get _contextMode() { + return this._contextControls?._contextMode ?? null; + } + get _contextModeChanging() { + return this._contextControls?._contextModeChanging ?? false; + } + get _preservePanelStateDuringLayerClear() { + return this._contextControls?._preservePanelStateDuringLayerClear ?? false; + } _initGlobalContextPanel() { const { radioLayer, militaryInstallationsLayer } = this.services; @@ -1983,21 +2453,32 @@ export class StyleManager { actions: { getCockpit: () => this.cockpitView, refreshRadio: () => this._renderRadioState(radioLayer.getUIState()), - claimVisualAuthority: () => this.shareLinkManager?.claimRestoreLane?.('visual'), + claimVisualAuthority: () => + this.shareLinkManager?.claimRestoreLane?.('visual'), syncDetection: () => this._syncContactsDetection(), scheduleLayout: () => this._scheduleRightPanelLayout(), setPanelCollapsed: (...args) => this.setPanelCollapsed(...args), - showToast: (message) => { if (!this._disposed) this._showToast(message); }, - setClearBusy: (busy) => { if (!this._disposed) this._clearLayersControl?.setBusy(busy); }, + showToast: (message) => { + if (!this._disposed) this._showToast(message); + }, + setClearBusy: (busy) => { + if (!this._disposed) this._clearLayersControl?.setBusy(busy); + }, }, }); } - _runUserFacingContextAction(...args) { return this._contextControls?._runUserFacingContextAction(...args); } + _runUserFacingContextAction(...args) { + return this._contextControls?._runUserFacingContextAction(...args); + } - _waitForContextLayerSettlement(...args) { return this._contextControls?._waitForContextLayerSettlement(...args); } + _waitForContextLayerSettlement(...args) { + return this._contextControls?._waitForContextLayerSettlement(...args); + } - _syncContextModeButtons(...args) { return this._contextControls?._syncContextModeButtons(...args); } + _syncContextModeButtons(...args) { + return this._contextControls?._syncContextModeButtons(...args); + } /** Wire the independent Radio companion controls. */ _initRadioPanel() { @@ -2056,29 +2537,42 @@ export class StyleManager { actions: { isRegistered: () => this._dataManager?.layers?.has('radio'), isEnabled: () => this._dataManager?.isEnabled('radio'), - setEnabled: (enabled, options) => this._dataManager.setEnabled('radio', enabled, options), - setParams: (params, options) => this._dataManager?.setLayerParams('radio', params, options), - getLifecycle: () => this._dataManager?.getLayerLifecycleState?.('radio'), + setEnabled: (enabled, options) => + this._dataManager.setEnabled('radio', enabled, options), + setParams: (params, options) => + this._dataManager?.setLayerParams('radio', params, options), + getLifecycle: () => + this._dataManager?.getLayerLifecycleState?.('radio'), runUserAction: (...args) => this._runUserFacingContextAction(...args), setPanelCollapsed: (...args) => this.setPanelCollapsed(...args), revealStyleParameters: () => this._revealCockpitStyleParameters(), - setSignalCollapsed: (value) => this.cockpitView?.setSignalCollapsed(value), + setSignalCollapsed: (value) => + this.cockpitView?.setSignalCollapsed(value), isCockpitActive: () => this.cockpitView?.active, signalUserCollapsed: () => this.cockpitView?.signalUserCollapsed, layoutCockpit: () => this.cockpitView?.scheduleContextLayout(), - preservePanelStateDuringClear: () => this._preservePanelStateDuringLayerClear, + preservePanelStateDuringClear: () => + this._preservePanelStateDuringLayerClear, scheduleLayout: () => this._scheduleRightPanelLayout(), }, }); } - _setCockpitDisclosure(...args) { return this._radioControls?._setCockpitDisclosure?.(...args); } + _setCockpitDisclosure(...args) { + return this._radioControls?._setCockpitDisclosure?.(...args); + } - _setRadioDisclosure(...args) { return this._radioControls?._setRadioDisclosure?.(...args); } + _setRadioDisclosure(...args) { + return this._radioControls?._setRadioDisclosure?.(...args); + } - _syncContextRadioLauncherState(...args) { return this._radioControls?._syncContextRadioLauncherState?.(...args); } + _syncContextRadioLauncherState(...args) { + return this._radioControls?._syncContextRadioLauncherState?.(...args); + } - _renderRadioState(...args) { return this._radioControls?._renderRadioState?.(...args); } + _renderRadioState(...args) { + return this._radioControls?._renderRadioState?.(...args); + } /** * Activates an explicit CCTV target, then releases tracking before its camera @@ -2127,7 +2621,8 @@ export class StyleManager { cctv: cctvLayer, actions: { isEnabled: () => this._dataManager?.isEnabled('cctv'), - setParams: (params, options) => this._dataManager?.setLayerParams('cctv', params, options), + setParams: (params, options) => + this._dataManager?.setLayerParams('cctv', params, options), toggleEnabled: (...args) => this._toggleCctvEnabled(...args), runExplicitFocus: (...args) => this._runExplicitCctvFocus(...args), setPanelCollapsed: (...args) => this.setPanelCollapsed(...args), @@ -2156,18 +2651,22 @@ export class StyleManager { if (target === enabled) return true; await runCctvLayerEnableTransition({ target, - setEnabled: (next) => this._dataManager.setEnabled('cctv', next, { origin: 'user' }), + setEnabled: (next) => + this._dataManager.setEnabled('cctv', next, { origin: 'user' }), readOwnership: () => ({ trackedEntity: this.viewer?.trackedEntity, cockpitActive: !!this.cockpitView?.active, }), - shouldFocus: () => !this._disposed && this._dataManager.isEnabled('cctv') - && !this._cctvControls?.getState()?.activeCameraId, + shouldFocus: () => + !this._disposed && + this._dataManager.isEnabled('cctv') && + !this._cctvControls?.getState()?.activeCameraId, activate: () => cctvLayer.focusNearest({ focus: false }), - fly: (cameraId) => this._runExplicitCctvFocus( - () => cameraId, - (selectedId) => cctvLayer.focusCamera(selectedId, 1.6), - ), + fly: (cameraId) => + this._runExplicitCctvFocus( + () => cameraId, + (selectedId) => cctvLayer.focusCamera(selectedId, 1.6), + ), }); return true; } @@ -2177,14 +2676,18 @@ export class StyleManager { * @param {string} panelId - DOM id of the panel. * @returns {string} localStorage key. */ - _panelStorageKey(panelId) { return this._panelPosition._panelStorageKey(panelId); } + _panelStorageKey(panelId) { + return this._panelPosition._panelStorageKey(panelId); + } /** * Returns the versioned localStorage key for a panel's collapsed state. * @param {string} panelId - DOM id of the panel. * @returns {string} localStorage key. */ - _panelCollapseStorageKey(panelId) { return this._panelPosition._panelCollapseStorageKey(panelId); } + _panelCollapseStorageKey(panelId) { + return this._panelPosition._panelCollapseStorageKey(panelId); + } /** * Restores a panel's collapsed/expanded state from localStorage. @@ -2192,7 +2695,9 @@ export class StyleManager { * @param {string} panelId - DOM id of the panel. * @returns {void} */ - _restorePanelCollapsedState(panelId, options1) { return this._panelPosition._restorePanelCollapsedState(panelId, options1); } + _restorePanelCollapsedState(panelId, options1) { + return this._panelPosition._restorePanelCollapsedState(panelId, options1); + } /** * Persists a panel's collapsed state ('1' or '0') to localStorage. @@ -2200,7 +2705,9 @@ export class StyleManager { * @param {boolean} collapsed - Whether the panel is collapsed. * @returns {void} */ - _savePanelCollapsedState(panelId, collapsed) { return this._panelPosition._savePanelCollapsedState(panelId, collapsed); } + _savePanelCollapsedState(panelId, collapsed) { + return this._panelPosition._savePanelCollapsedState(panelId, collapsed); + } /** * Builds one fixed right-side rail from Display, CCTV, its parameter @@ -2209,9 +2716,13 @@ export class StyleManager { * aligned and within the available vertical corridor. * @returns {void} */ - _initRightPanelAdaptiveLayout() { return this._panelLayout._initRightPanelAdaptiveLayout(); } + _initRightPanelAdaptiveLayout() { + return this._panelLayout._initRightPanelAdaptiveLayout(); + } - _scheduleRightPanelLayout(options0) { return this._panelLayout._scheduleRightPanelLayout(options0); } + _scheduleRightPanelLayout(options0) { + return this._panelLayout._scheduleRightPanelLayout(options0); + } /** * Places the right rail inside the visible HUD-safe corridor. When the @@ -2220,7 +2731,9 @@ export class StyleManager { * while a panel is expanded; other HUD layouts keep them visible. * @returns {void} */ - _syncRightPanelAdaptiveLayout() { return this._panelLayout._syncRightPanelAdaptiveLayout(); } + _syncRightPanelAdaptiveLayout() { + return this._panelLayout._syncRightPanelAdaptiveLayout(); + } /** * Initializes the adaptive left accordion. The layout engine measures the @@ -2229,13 +2742,17 @@ export class StyleManager { * panel. No decision is keyed to a specific panel or HUD variant. * @returns {void} */ - _initLeftPanelAdaptiveLayout() { return this._panelLayout._initLeftPanelAdaptiveLayout(); } + _initLeftPanelAdaptiveLayout() { + return this._panelLayout._initLeftPanelAdaptiveLayout(); + } /** * Batches adaptive accordion work into one animation frame. * @returns {void} */ - _scheduleLeftPanelLayout(options0) { return this._panelLayout._scheduleLeftPanelLayout(options0); } + _scheduleLeftPanelLayout(options0) { + return this._panelLayout._scheduleLeftPanelLayout(options0); + } /** * Measures a live obstacle-free corridor for the left accordion and toggles @@ -2243,7 +2760,9 @@ export class StyleManager { * Safe boundaries are written as viewport-relative CSS values. * @returns {void} */ - _syncLeftPanelAdaptiveLayout() { return this._panelLayout._syncLeftPanelAdaptiveLayout(); } + _syncLeftPanelAdaptiveLayout() { + return this._panelLayout._syncLeftPanelAdaptiveLayout(); + } /** * Updates collapse button glyphs based on panel state. Right-rail panels @@ -2252,37 +2771,54 @@ export class StyleManager { * @returns {void} */ _syncPanelCollapseButton(panelEl) { - const isRightRail = ['pp-toggles', 'cctv-panel', 'global-context-panel'].includes(panelEl?.id); + const isRightRail = [ + 'pp-toggles', + 'cctv-panel', + 'global-context-panel', + ].includes(panelEl?.id); const collapsed = panelEl.classList.contains('collapsed'); - panelEl.querySelectorAll('.panel-collapse-btn[data-collapse-target]').forEach((btn) => { - const owner = btn.closest('[data-panel-id], #param-slider-panel'); - if (owner !== panelEl) return; - if (isRightRail) { - btn.textContent = collapsed ? '◀' : '▶'; - } else { - btn.textContent = collapsed ? '+' : '−'; - } - btn.setAttribute('aria-expanded', String(!collapsed)); - const panelName = panelEl.querySelector('.panel-title, .pp-header-label')?.textContent?.trim() || 'panel'; - const action = collapsed ? 'Expand' : 'Collapse'; - btn.title = `${action} ${panelName}`; - btn.setAttribute('aria-label', `${action} ${panelName}`); - if (panelEl.id === 'radio-panel') { + panelEl + .querySelectorAll('.panel-collapse-btn[data-collapse-target]') + .forEach((btn) => { + const owner = btn.closest('[data-panel-id], #param-slider-panel'); + if (owner !== panelEl) return; + if (isRightRail) { + btn.textContent = collapsed ? '◀' : '▶'; + } else { + btn.textContent = collapsed ? '+' : '−'; + } + btn.setAttribute('aria-expanded', String(!collapsed)); + const panelName = + panelEl + .querySelector('.panel-title, .pp-header-label') + ?.textContent?.trim() || 'panel'; const action = collapsed ? 'Expand' : 'Collapse'; - btn.title = `${action} Radio`; - btn.setAttribute('aria-label', `${action} Radio section`); - } - }); - const dockToggle = panelEl.querySelector(`[data-dock-toggle-target="${panelEl.id}"]`); + btn.title = `${action} ${panelName}`; + btn.setAttribute('aria-label', `${action} ${panelName}`); + if (panelEl.id === 'radio-panel') { + const action = collapsed ? 'Expand' : 'Collapse'; + btn.title = `${action} Radio`; + btn.setAttribute('aria-label', `${action} Radio section`); + } + }); + const dockToggle = panelEl.querySelector( + `[data-dock-toggle-target="${panelEl.id}"]`, + ); if (dockToggle) { - const panelName = panelEl.querySelector('.panel-title, .location-toolbar-label')?.textContent?.trim() || 'panel'; + const panelName = + panelEl + .querySelector('.panel-title, .location-toolbar-label') + ?.textContent?.trim() || 'panel'; const action = collapsed ? 'Expand' : 'Collapse'; dockToggle.setAttribute('aria-expanded', String(!collapsed)); dockToggle.setAttribute('aria-label', `${action} ${panelName}`); dockToggle.title = `${action} ${panelName}`; } if (panelEl.id === 'radio-panel' && this._contextRadioDetailsBtn) { - this._contextRadioDetailsBtn.setAttribute('aria-expanded', String(!collapsed)); + this._contextRadioDetailsBtn.setAttribute( + 'aria-expanded', + String(!collapsed), + ); } if (panelEl.id === 'radio-panel' || panelEl.id === 'global-context-panel') { this._syncContextRadioLauncherState(); @@ -2295,7 +2831,9 @@ export class StyleManager { * @param {HTMLElement} panelEl - The panel to re-anchor. * @returns {void} */ - _pinPanelToRight(panelEl) { return this._panelPosition._pinPanelToRight(panelEl); } + _pinPanelToRight(panelEl) { + return this._panelPosition._pinPanelToRight(panelEl); + } /** * Restores a panel's top/left position from localStorage. @@ -2304,7 +2842,9 @@ export class StyleManager { * @param {HTMLElement} panelEl - The panel DOM element. * @returns {void} */ - _restorePanelPosition(panelId, panelEl) { return this._panelPosition._restorePanelPosition(panelId, panelEl); } + _restorePanelPosition(panelId, panelEl) { + return this._panelPosition._restorePanelPosition(panelId, panelEl); + } /** * Clamp a desired left/top so the panel stays fully on-screen (6px inset), matching the drag @@ -2314,7 +2854,9 @@ export class StyleManager { * @param {HTMLElement} panelEl - the panel element * @returns {{left:number, top:number}} */ - _clampToViewport(left, top, panelEl) { return this._panelPosition._clampToViewport(left, top, panelEl); } + _clampToViewport(left, top, panelEl) { + return this._panelPosition._clampToViewport(left, top, panelEl); + } /** * Persists a panel's current bounding-rect position to localStorage. @@ -2322,7 +2864,9 @@ export class StyleManager { * @param {HTMLElement} panelEl - The panel DOM element. * @returns {void} */ - _savePanelPosition(panelId, panelEl) { return this._panelPosition._savePanelPosition(panelId, panelEl); } + _savePanelPosition(panelId, panelEl) { + return this._panelPosition._savePanelPosition(panelId, panelEl); + } /** * Makes a panel draggable via its handle element. Implements: @@ -2343,9 +2887,13 @@ export class StyleManager { * @param {HTMLElement} panelEl - Panel to bring to front. * @returns {void} */ - _promotePanelZ(panelEl) { return this._panelPosition._promotePanelZ(panelEl); } + _promotePanelZ(panelEl) { + return this._panelPosition._promotePanelZ(panelEl); + } - _makePanelDraggable(panelId, panelEl, handleEl) { return this._panelPosition._makePanelDraggable(panelId, panelEl, handleEl); } + _makePanelDraggable(panelId, panelEl, handleEl) { + return this._panelPosition._makePanelDraggable(panelId, panelEl, handleEl); + } _buildSharePanelState() { const specs = []; @@ -2358,7 +2906,8 @@ export class StyleManager { ? false : panelEl.classList.contains('collapsed'); const entry = { id: spec.id, collapsed }; - if (spec.pinnable) entry.pinned = panelEl.classList.contains('dock-pinned'); + if (spec.pinnable) + entry.pinned = panelEl.classList.contains('dock-pinned'); specs.push(entry); } return specs.length ? { specs } : null; @@ -2377,7 +2926,8 @@ export class StyleManager { syncShare: false, }); } - const nextCollapsed = state.pinned && spec.pinnable ? false : state.collapsed; + const nextCollapsed = + state.pinned && spec.pinnable ? false : state.collapsed; this.setPanelCollapsed(spec.id, nextCollapsed, { restore: true, persist: false, @@ -2396,40 +2946,61 @@ export class StyleManager { * @param {boolean} [options.explicit=false] Whether a direct user action owns the panel lane. * @returns {void} */ - setPanelCollapsed(panelId, collapsed, { - explicit = false, - restore = false, - persist = true, - syncShare = true, - } = {}) { - if (panelId === 'control-panel' && collapsed) this._cancelMapSourceFocus?.(); + setPanelCollapsed( + panelId, + collapsed, + { + explicit = false, + restore = false, + persist = true, + syncShare = true, + } = {}, + ) { + if (panelId === 'control-panel' && collapsed) + this._cancelMapSourceFocus?.(); const panelEl = document.getElementById(panelId); if (!panelEl) return; - if (explicit && !restore) this.shareLinkManager?.claimRestoreLane?.('panel', panelId); + if (explicit && !restore) + this.shareLinkManager?.claimRestoreLane?.('panel', panelId); const nextCollapsed = Boolean(collapsed); - const wasAutoCollapsed = panelEl.classList.contains('layout-auto-collapsed'); - const leftOwnerPanel = this._leftPanelStack?.contains(panelEl) ? panelEl : null; - const rightOwnerPanel = panelId === 'radio-panel' - ? document.getElementById('global-context-panel') - : (this._rightPanelStack?.contains(panelEl) ? panelEl : null); + const wasAutoCollapsed = panelEl.classList.contains( + 'layout-auto-collapsed', + ); + const leftOwnerPanel = this._leftPanelStack?.contains(panelEl) + ? panelEl + : null; + const rightOwnerPanel = + panelId === 'radio-panel' + ? document.getElementById('global-context-panel') + : this._rightPanelStack?.contains(panelEl) + ? panelEl + : null; const priorLeftOwner = this._panelLayout._leftStackPreferredPanelId; const priorRightOwner = this._panelLayout._rightStackPreferredPanelId; if (explicit && !restore && !nextCollapsed && leftOwnerPanel) { this._panelLayout._leftStackPreferredPanelId = leftOwnerPanel.id; - } else if (explicit && !restore && nextCollapsed && leftOwnerPanel?.id === this._panelLayout._leftStackPreferredPanelId) { + } else if ( + explicit && + !restore && + nextCollapsed && + leftOwnerPanel?.id === this._panelLayout._leftStackPreferredPanelId + ) { this._panelLayout._leftStackPreferredPanelId = null; } if (explicit && !restore && !nextCollapsed && rightOwnerPanel) { this._panelLayout._rightStackPreferredPanelId = rightOwnerPanel.id; } else if ( - explicit - && !restore - && nextCollapsed - && rightOwnerPanel?.id === this._panelLayout._rightStackPreferredPanelId + explicit && + !restore && + nextCollapsed && + rightOwnerPanel?.id === this._panelLayout._rightStackPreferredPanelId ) { this._panelLayout._rightStackPreferredPanelId = null; } - if (panelEl.classList.contains('collapsed') === nextCollapsed && !wasAutoCollapsed) { + if ( + panelEl.classList.contains('collapsed') === nextCollapsed && + !wasAutoCollapsed + ) { this._syncPanelCollapseButton(panelEl); if (priorLeftOwner !== this._panelLayout._leftStackPreferredPanelId) { this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true }); @@ -2440,39 +3011,69 @@ export class StyleManager { return; } panelEl.classList.remove('layout-auto-collapsed'); - if (!nextCollapsed && this.cockpitView?.active && panelId === 'data-panel') { - this._cockpitContextCollapsedForDataPanel = !this.cockpitView.contextCollapsed; + if ( + !nextCollapsed && + this.cockpitView?.active && + panelId === 'data-panel' + ) { + this._cockpitContextCollapsedForDataPanel = + !this.cockpitView.contextCollapsed; if (this._cockpitContextCollapsedForDataPanel) { this.cockpitView.setContextCollapsed(true); } } - if (!nextCollapsed && panelId === 'global-context-panel' - && this._contextRadioDock?.classList.contains('disclosure-open')) { + if ( + !nextCollapsed && + panelId === 'global-context-panel' && + this._contextRadioDock?.classList.contains('disclosure-open') + ) { this._setRadioDisclosure?.(false); } - if (!nextCollapsed && panelId === 'radio-panel' - && document.getElementById('global-context-panel')?.classList.contains('collapsed')) { - this.setPanelCollapsed('global-context-panel', false, { restore, persist, syncShare }); + if ( + !nextCollapsed && + panelId === 'radio-panel' && + document + .getElementById('global-context-panel') + ?.classList.contains('collapsed') + ) { + this.setPanelCollapsed('global-context-panel', false, { + restore, + persist, + syncShare, + }); } if (!nextCollapsed && !restore && panelId === 'location-bar') { const otherPanel = document.getElementById('control-panel'); if (otherPanel && !otherPanel.classList.contains('dock-pinned')) { - this.setPanelCollapsed('control-panel', true, { restore, persist, syncShare }); + this.setPanelCollapsed('control-panel', true, { + restore, + persist, + syncShare, + }); } } else if (!nextCollapsed && !restore && panelId === 'control-panel') { const otherPanel = document.getElementById('location-bar'); if (otherPanel && !otherPanel.classList.contains('dock-pinned')) { - this.setPanelCollapsed('location-bar', true, { restore, persist, syncShare }); + this.setPanelCollapsed('location-bar', true, { + restore, + persist, + syncShare, + }); } } panelEl.classList.toggle('collapsed', nextCollapsed); - if (nextCollapsed && this.cockpitView?.active && panelId === 'data-panel' - && this._cockpitContextCollapsedForDataPanel) { + if ( + nextCollapsed && + this.cockpitView?.active && + panelId === 'data-panel' && + this._cockpitContextCollapsedForDataPanel + ) { this._cockpitContextCollapsedForDataPanel = false; this.cockpitView.setContextCollapsed(false); } this._syncPanelCollapseButton(panelEl); - if (persist !== false) this._savePanelCollapsedState(panelId, nextCollapsed); + if (persist !== false) + this._savePanelCollapsedState(panelId, nextCollapsed); if (panelId === 'pp-toggles') { this._layoutRightPanels(); } @@ -2495,9 +3096,10 @@ export class StyleManager { * @returns {void} */ toggleCleanView(forceEnabled) { - const shouldEnable = typeof forceEnabled === 'boolean' - ? forceEnabled - : !document.body.classList.contains('ui-clean-view'); + const shouldEnable = + typeof forceEnabled === 'boolean' + ? forceEnabled + : !document.body.classList.contains('ui-clean-view'); document.body.classList.toggle('ui-clean-view', shouldEnable); if (this._cleanViewBtn) { this._cleanViewBtn.classList.toggle('active', shouldEnable); @@ -2524,7 +3126,12 @@ export class StyleManager { this.hud.setMode(normalized); this._updateHudButtonState(); this._syncShareState(); - return { ok: true, visible: !!this.hud.visible, mode: normalized, layout: this.hud.getVariant() }; + return { + ok: true, + visible: !!this.hud.visible, + mode: normalized, + layout: this.hud.getVariant(), + }; } /** @@ -2539,7 +3146,11 @@ export class StyleManager { } this.shareLinkManager?.claimRestoreLane?.('visual'); this._setHudVariant(variant); - return { ok: true, layout: this.hud.getVariant(), visible: !!this.hud.visible }; + return { + ok: true, + layout: this.hud.getVariant(), + visible: !!this.hud.visible, + }; } /** @@ -2556,7 +3167,10 @@ export class StyleManager { densityPct: pct, allocationStrategy: getDetectionTuning().allocationStrategy, fadePct: parseInt(this._detectionFadeSlider?.value || '7', 10), - outsideOpacityPct: parseInt(this._detectionOpacitySlider?.value || '0', 10), + outsideOpacityPct: parseInt( + this._detectionOpacitySlider?.value || '0', + 10, + ), }; } @@ -2579,27 +3193,50 @@ export class StyleManager { * @param {number} [options.outsideOpacityPct] - Opacity beyond the fade distance, 0-100%. * @returns {{ok: boolean, detectionMode?: string, densityPct?: number|null, error?: string}} */ - setDetection({ enabled, mode, densityPct, allocationStrategy, fadePct, outsideOpacityPct } = {}) { + setDetection({ + enabled, + mode, + densityPct, + allocationStrategy, + fadePct, + outsideOpacityPct, + } = {}) { const { getDetectionMode, setDetectionModeByLabel } = this.services; if (enabled !== undefined && typeof enabled !== 'boolean') { - return { ok: false, error: `Invalid detection enabled value: ${enabled}`, ...this.getDetectionState() }; + return { + ok: false, + error: `Invalid detection enabled value: ${enabled}`, + ...this.getDetectionState(), + }; } let requestedProfile = null; if (typeof mode === 'string' && mode.trim()) { requestedProfile = normalizeProfile(mode); if (!requestedProfile) { - return { ok: false, error: `Unknown detection mode: ${mode}`, ...this.getDetectionState() }; + return { + ok: false, + error: `Unknown detection mode: ${mode}`, + ...this.getDetectionState(), + }; } } let requestedDensity = null; if (densityPct != null) { if (!Number.isFinite(Number(densityPct))) { - return { ok: false, error: `Invalid density: ${densityPct}`, ...this.getDetectionState() }; + return { + ok: false, + error: `Invalid density: ${densityPct}`, + ...this.getDetectionState(), + }; } requestedDensity = canonicalizeDensity(Number(densityPct)); } - if (requestedProfile && requestedProfile !== 'OFF' && requestedDensity != null - && profileForDensity(requestedDensity) !== requestedProfile) { + if ( + requestedProfile && + requestedProfile !== 'OFF' && + requestedDensity != null && + profileForDensity(requestedDensity) !== requestedProfile + ) { return { ok: false, error: `Detection mode ${requestedProfile} conflicts with density ${requestedDensity}%`, @@ -2610,25 +3247,38 @@ export class StyleManager { if (allocationStrategy != null) { requestedAllocation = String(allocationStrategy).trim().toUpperCase(); if (!ALLOCATION_STRATEGIES.includes(requestedAllocation)) { - return { ok: false, error: `Unknown allocation strategy: ${allocationStrategy}`, ...this.getDetectionState() }; + return { + ok: false, + error: `Unknown allocation strategy: ${allocationStrategy}`, + ...this.getDetectionState(), + }; } } if (fadePct != null) { if (!Number.isFinite(Number(fadePct))) { - return { ok: false, error: `Invalid fade distance: ${fadePct}`, ...this.getDetectionState() }; + return { + ok: false, + error: `Invalid fade distance: ${fadePct}`, + ...this.getDetectionState(), + }; } } if (outsideOpacityPct != null) { if (!Number.isFinite(Number(outsideOpacityPct))) { - return { ok: false, error: `Invalid outside opacity: ${outsideOpacityPct}`, ...this.getDetectionState() }; + return { + ok: false, + error: `Invalid outside opacity: ${outsideOpacityPct}`, + ...this.getDetectionState(), + }; } } - const hasExplicitVisualChange = typeof enabled === 'boolean' - || requestedProfile !== null - || requestedDensity !== null - || requestedAllocation !== null - || fadePct != null - || outsideOpacityPct != null; + const hasExplicitVisualChange = + typeof enabled === 'boolean' || + requestedProfile !== null || + requestedDensity !== null || + requestedAllocation !== null || + fadePct != null || + outsideOpacityPct != null; if (hasExplicitVisualChange) { // Voice/scripted detection control counts as an explicit user choice, so // neither style presets nor a still-pending shared visual restore can @@ -2640,14 +3290,23 @@ export class StyleManager { this._setDetectionAllocation(requestedAllocation, { syncShare: false }); } if (fadePct != null && this._detectionFadeSlider) { - this._detectionFadeSlider.value = String(Math.max(0, Math.min(40, Math.round(Number(fadePct))))); + this._detectionFadeSlider.value = String( + Math.max(0, Math.min(40, Math.round(Number(fadePct)))), + ); } if (outsideOpacityPct != null && this._detectionOpacitySlider) { - this._detectionOpacitySlider.value = String(Math.max(0, Math.min(100, Math.round(Number(outsideOpacityPct))))); + this._detectionOpacitySlider.value = String( + Math.max(0, Math.min(100, Math.round(Number(outsideOpacityPct)))), + ); } - if (fadePct != null || outsideOpacityPct != null) this._applyDetectionFadeFromUi(); + if (fadePct != null || outsideOpacityPct != null) + this._applyDetectionFadeFromUi(); - if (requestedProfile && requestedProfile !== 'OFF' && requestedDensity == null) { + if ( + requestedProfile && + requestedProfile !== 'OFF' && + requestedDensity == null + ) { requestedDensity = defaultDensityForProfile(requestedProfile); } if (requestedDensity != null && this._detectionDensitySlider) { @@ -2660,9 +3319,11 @@ export class StyleManager { } else if (requestedProfile) { setDetectionModeByLabel(requestedProfile); } else if (enabled === true && getDetectionMode() === 'OFF') { - setDetectionModeByLabel(profileForDensity( - requestedDensity ?? this._detectionDensitySlider?.value ?? 50, - )); + setDetectionModeByLabel( + profileForDensity( + requestedDensity ?? this._detectionDensitySlider?.value ?? 50, + ), + ); } this._syncDetectionUiFromEngine(); this._syncShareState(); @@ -2681,10 +3342,18 @@ export class StyleManager { const stacks = this.mapStackController.getStacks(); const target = stacks.find((stack) => stack.id === stackId); if (!target) { - return { ok: false, error: `Unknown map stack: ${stackId}`, available: stacks.map((s) => s.id) }; + return { + ok: false, + error: `Unknown map stack: ${stackId}`, + available: stacks.map((s) => s.id), + }; } if (!target.available) { - return { ok: false, error: `${target.label} requires a Cesium ion token`, activeStack: this.mapStackController.getActiveId() }; + return { + ok: false, + error: `${target.label} requires a Cesium ion token`, + activeStack: this.mapStackController.getActiveId(), + }; } await this._setMapStack(stackId); const state = this.mapStackController.getState(); @@ -2692,7 +3361,7 @@ export class StyleManager { return { ok: landed, activeStack: state.activeId, - error: landed ? null : (state.lastError || 'Map stack did not switch'), + error: landed ? null : state.lastError || 'Map stack did not switch', }; } @@ -2706,19 +3375,35 @@ export class StyleManager { setBloom({ enabled, intensityPct } = {}) { const current = () => ({ enabled: !!this.bloomEnabled, - intensityPct: this._bloomSlider ? parseInt(this._bloomSlider.value, 10) : null, + intensityPct: this._bloomSlider + ? parseInt(this._bloomSlider.value, 10) + : null, }); if (enabled !== undefined && typeof enabled !== 'boolean') { - return { ok: false, error: `Invalid bloom enabled value: ${enabled}`, bloom: current() }; + return { + ok: false, + error: `Invalid bloom enabled value: ${enabled}`, + bloom: current(), + }; } - if (intensityPct !== undefined - && (typeof intensityPct !== 'number' || !Number.isFinite(intensityPct))) { - return { ok: false, error: `Invalid bloom intensity: ${intensityPct}`, bloom: current() }; + if ( + intensityPct !== undefined && + (typeof intensityPct !== 'number' || !Number.isFinite(intensityPct)) + ) { + return { + ok: false, + error: `Invalid bloom intensity: ${intensityPct}`, + bloom: current(), + }; } - const hasExplicitVisualChange = intensityPct !== undefined || enabled !== undefined; - if (hasExplicitVisualChange) this.shareLinkManager?.claimRestoreLane?.('visual'); + const hasExplicitVisualChange = + intensityPct !== undefined || enabled !== undefined; + if (hasExplicitVisualChange) + this.shareLinkManager?.claimRestoreLane?.('visual'); if (intensityPct !== undefined) { - this._setBloomIntensity(Math.round(Math.max(0, Math.min(200, intensityPct)))); + this._setBloomIntensity( + Math.round(Math.max(0, Math.min(200, intensityPct))), + ); } if (enabled !== undefined) this._setBloomEnabled(enabled); return { @@ -2737,21 +3422,36 @@ export class StyleManager { setSharpen({ enabled, intensityPct } = {}) { const current = () => ({ enabled: !!this.sharpenEnabled, - intensityPct: this._sharpenSlider ? parseInt(this._sharpenSlider.value, 10) : null, + intensityPct: this._sharpenSlider + ? parseInt(this._sharpenSlider.value, 10) + : null, }); if (enabled !== undefined && typeof enabled !== 'boolean') { - return { ok: false, error: `Invalid sharpen enabled value: ${enabled}`, sharpen: current() }; + return { + ok: false, + error: `Invalid sharpen enabled value: ${enabled}`, + sharpen: current(), + }; } - if (intensityPct !== undefined - && (typeof intensityPct !== 'number' || !Number.isFinite(intensityPct))) { - return { ok: false, error: `Invalid sharpen intensity: ${intensityPct}`, sharpen: current() }; + if ( + intensityPct !== undefined && + (typeof intensityPct !== 'number' || !Number.isFinite(intensityPct)) + ) { + return { + ok: false, + error: `Invalid sharpen intensity: ${intensityPct}`, + sharpen: current(), + }; } - const hasExplicitVisualChange = intensityPct !== undefined || enabled !== undefined; - if (hasExplicitVisualChange) this.shareLinkManager?.claimRestoreLane?.('visual'); + const hasExplicitVisualChange = + intensityPct !== undefined || enabled !== undefined; + if (hasExplicitVisualChange) + this.shareLinkManager?.claimRestoreLane?.('visual'); if (intensityPct !== undefined) { const pct = Math.round(Math.max(0, Math.min(100, intensityPct))); if (this._sharpenSlider) this._sharpenSlider.value = String(pct); - if (this._sharpenSliderValue) this._sharpenSliderValue.textContent = `${pct}%`; + if (this._sharpenSliderValue) + this._sharpenSliderValue.textContent = `${pct}%`; this._applySharpenIntensity(pct / 100); this._syncShareState(); } @@ -2847,7 +3547,11 @@ export class StyleManager { return { ok: true, orbiting: false }; } if (!this._currentTarget) { - return { ok: false, orbiting: false, error: 'No active landmark to orbit — fly to a landmark first' }; + return { + ok: false, + orbiting: false, + error: 'No active landmark to orbit — fly to a landmark first', + }; } this._toggleOrbit(); return { ok: true, orbiting: !!this.orbitController?.active }; @@ -2860,14 +3564,19 @@ export class StyleManager { */ setCleanView(enabled) { this.toggleCleanView(enabled); - return { ok: true, cleanView: document.body.classList.contains('ui-clean-view') }; + return { + ok: true, + cleanView: document.body.classList.contains('ui-clean-view'), + }; } /** * Reads global context mode state for voice/state-sync consumers. * @returns {{mode: 'flights'|'space-missions'|null, active: boolean, changing: boolean, entering: 'flights'|'space-missions'|null, snapshotCaptured: boolean}} */ - getContextModeState(...args) { return this._contextControls?.getContextModeState(...args); } + getContextModeState(...args) { + return this._contextControls?.getContextModeState(...args); + } /** * Sets global context mode (Contacts / Space Missions / off) for voice. @@ -2883,7 +3592,9 @@ export class StyleManager { * operator and must stay inert, so they pass `false`. * @returns {Promise<{ok:boolean, mode:'flights'|'space-missions'|null, active:boolean, action:string, error?:string}>} */ - setContextMode(...args) { return this._contextControls?.setContextMode(...args); } + setContextMode(...args) { + return this._contextControls?.setContextMode(...args); + } /** * Returns cockpit status for voice/state sync and navigation operations. @@ -2900,55 +3611,70 @@ export class StyleManager { // `viewer.trackedEntity` on entry and NEXT puts one back, which made this // flip true/false between calls while `active` stayed true; readers // (including the voice model) read that as a broken half-entered state. - const entryAllowed = !active && Boolean( - gateOpen - && info - && this.viewer?.trackedEntity?.position, - ); + const entryAllowed = + !active && + Boolean(gateOpen && info && this.viewer?.trackedEntity?.position); return { active, entryAllowed, // Why entry is unavailable, so a refusal can be explained rather than // guessed at. - entryBlockedReason: entryAllowed || active - ? null - : (!gateOpen - ? (this._contextModeChanging ? 'contacts-starting' : 'contacts-inactive') - : 'no-tracked-aircraft'), + entryBlockedReason: + entryAllowed || active + ? null + : !gateOpen + ? this._contextModeChanging + ? 'contacts-starting' + : 'contacts-inactive' + : 'no-tracked-aircraft', visionMode: this.cockpitView?.visionMode || null, - subject: info ? { - id: info.icao24 || info.id || null, - layerId: info.layerId || null, - callsign: info.callsign || null, - } : null, - navigation: snapshot ? { - canPrevious: Boolean(snapshot.navigation?.canPrevious), - canNext: Boolean(snapshot.navigation?.canNext), - canFocus: Boolean(snapshot.navigation?.canFocus), - } : null, - awareness: snapshot ? { - radiusM: Number.isFinite(snapshot.radiusM) ? snapshot.radiusM : null, - subject: snapshot.subject ? { - id: snapshot.subject.id || null, - layerId: snapshot.subject.layerId || null, - } : null, - cohorts: Array.isArray(snapshot.cohorts) - ? snapshot.cohorts.map((cohort) => ({ - id: cohort?.id || null, - source: cohort?.source || null, - count: Number.isFinite(cohort?.count) ? cohort.count : null, - relationship: cohort?.relationship || null, - reason: cohort?.reason || null, - coverage: cohort?.coverage || null, - })) - : [], - navigation: snapshot.navigation ? { - canPrevious: Boolean(snapshot.navigation.canPrevious), - canNext: Boolean(snapshot.navigation.canNext), - canFocus: Boolean(snapshot.navigation.canFocus), - } : null, - } : null, - activeTracked: this.cockpitView?.active ? Boolean(this.cockpitView?.trackedEntity) : false, + subject: info + ? { + id: info.icao24 || info.id || null, + layerId: info.layerId || null, + callsign: info.callsign || null, + } + : null, + navigation: snapshot + ? { + canPrevious: Boolean(snapshot.navigation?.canPrevious), + canNext: Boolean(snapshot.navigation?.canNext), + canFocus: Boolean(snapshot.navigation?.canFocus), + } + : null, + awareness: snapshot + ? { + radiusM: Number.isFinite(snapshot.radiusM) + ? snapshot.radiusM + : null, + subject: snapshot.subject + ? { + id: snapshot.subject.id || null, + layerId: snapshot.subject.layerId || null, + } + : null, + cohorts: Array.isArray(snapshot.cohorts) + ? snapshot.cohorts.map((cohort) => ({ + id: cohort?.id || null, + source: cohort?.source || null, + count: Number.isFinite(cohort?.count) ? cohort.count : null, + relationship: cohort?.relationship || null, + reason: cohort?.reason || null, + coverage: cohort?.coverage || null, + })) + : [], + navigation: snapshot.navigation + ? { + canPrevious: Boolean(snapshot.navigation.canPrevious), + canNext: Boolean(snapshot.navigation.canNext), + canFocus: Boolean(snapshot.navigation.canFocus), + } + : null, + } + : null, + activeTracked: this.cockpitView?.active + ? Boolean(this.cockpitView?.trackedEntity) + : false, activeMapView: !this.cockpitView?.active && entryAllowed, }; } @@ -2967,7 +3693,12 @@ export class StyleManager { * @param {{layerId: string}|null} options.selectedTarget Pending selection. * @returns {{ok: boolean, retargeted?: boolean, error?: string}} Outcome. */ - _retargetCockpitEntryLayer({ targetLayer, aircraftClass, currentTarget, selectedTarget }) { + _retargetCockpitEntryLayer({ + targetLayer, + aircraftClass, + currentTarget, + selectedTarget, + }) { const { militaryAwarenessLayer } = this.services; if (!['flights', 'military'].includes(targetLayer)) { return { @@ -2975,15 +3706,17 @@ export class StyleManager { error: `Cockpit flies aircraft only — ${targetLayer} contacts cannot be entered`, }; } - const activeLayer = selectedTarget?.layerId || currentTarget?.layerId || null; + const activeLayer = + selectedTarget?.layerId || currentTarget?.layerId || null; const alreadyOnLayer = activeLayer === targetLayer; - if (alreadyOnLayer && !aircraftClass) return { ok: true, retargeted: false }; + if (alreadyOnLayer && !aircraftClass) + return { ok: true, retargeted: false }; const moved = militaryAwarenessLayer?.navigateNext ? !!militaryAwarenessLayer.navigateNext({ - targetLayer, - aircraftClass, - origin: 'voice', - }) + targetLayer, + aircraftClass, + origin: 'voice', + }) : false; if (moved) return { ok: true, retargeted: true }; // A filter that matched nothing still enters, as long as the layer is @@ -3008,13 +3741,16 @@ export class StyleManager { * @param {{layerId:'flights'|'military',id:string}|null} [options.rollbackTarget] * @returns {{ok:boolean, action:string, error?:string, state?:object}} */ - controlCockpit(action, { - notificationToken = null, - targetLayer = null, - aircraftClass = null, - selectedTarget = null, - rollbackTarget = undefined, - } = {}) { + controlCockpit( + action, + { + notificationToken = null, + targetLayer = null, + aircraftClass = null, + selectedTarget = null, + rollbackTarget = undefined, + } = {}, + ) { const { flightsLayer, militaryFlightsLayer } = this.services; const normalized = String(action || '').toLowerCase(); if (!this.cockpitView) { @@ -3049,9 +3785,12 @@ export class StyleManager { }; } let currentTarget = this.getAircraftTrackingTarget(); - const layerForTarget = (target) => target?.layerId === 'military' - ? militaryFlightsLayer - : target?.layerId === 'flights' ? flightsLayer : null; + const layerForTarget = (target) => + target?.layerId === 'military' + ? militaryFlightsLayer + : target?.layerId === 'flights' + ? flightsLayer + : null; // A requested layer retargets BEFORE entry, through the same filtered // navigation NEXT uses. Ignoring it entered on whatever was already // tracked and reported success, so "cockpit in that military helicopter" @@ -3075,13 +3814,17 @@ export class StyleManager { // The retarget is now the authority; a selection sampled before it // would drag entry back to the wrong layer. selectedTarget = null; - rollbackTarget = rollbackTarget === undefined ? currentTarget : rollbackTarget; + rollbackTarget = + rollbackTarget === undefined ? currentTarget : rollbackTarget; currentTarget = this.getAircraftTrackingTarget(); } } - const selectedLayer = selectedTarget?.layerId === 'military' - ? militaryFlightsLayer - : selectedTarget?.layerId === 'flights' ? flightsLayer : null; + const selectedLayer = + selectedTarget?.layerId === 'military' + ? militaryFlightsLayer + : selectedTarget?.layerId === 'flights' + ? flightsLayer + : null; const entry = enterCockpitWithTracking({ cockpitView: this.cockpitView, selectedLayer, @@ -3142,22 +3885,32 @@ export class StyleManager { return { style: this.activeStyle || 'normal', mapStack: this.mapStackController?.getActiveId?.() || null, - hud: { visible: !!this.hud?.visible, layout: this.hud?.getVariant?.() || null }, + hud: { + visible: !!this.hud?.visible, + layout: this.hud?.getVariant?.() || null, + }, detection: this.getDetectionState(), bloom: { enabled: !!this.bloomEnabled, - intensityPct: this._bloomSlider ? parseInt(this._bloomSlider.value, 10) : null, + intensityPct: this._bloomSlider + ? parseInt(this._bloomSlider.value, 10) + : null, }, sharpen: { enabled: !!this.sharpenEnabled, - intensityPct: this._sharpenSlider ? parseInt(this._sharpenSlider.value, 10) : null, + intensityPct: this._sharpenSlider + ? parseInt(this._sharpenSlider.value, 10) + : null, }, celestialRing: { enabled: this.celestialRingEnabled, visible: !!this.celestialRing?.visible, }, orbiting: !!this.orbitController?.active, - models3d: { enabled: !!this._models3dEnabled, mode: this._models3dMode || 'proximity' }, + models3d: { + enabled: !!this._models3dEnabled, + mode: this._models3dMode || 'proximity', + }, recording: !!this._recording._recordingMode, cleanView: document.body.classList.contains('ui-clean-view'), }; @@ -3192,7 +3945,7 @@ export class StyleManager { destination: Cesium.Cartesian3.fromDegrees( cameraState.lon, cameraState.lat, - cameraState.alt + cameraState.alt, ), orientation: { heading: Cesium.Math.toRadians(cameraState.heading || 0), @@ -3210,7 +3963,12 @@ export class StyleManager { * @returns {object} Serializable visual state object. */ getVisualState() { - const { getDetectionTuning, getDetectionMode, isScopeMaskEnabled, getScopeMaskFeather } = this.services; + const { + getDetectionTuning, + getDetectionMode, + isScopeMaskEnabled, + getScopeMaskFeather, + } = this.services; const styleParams = {}; for (const [styleName, stage] of Object.entries(this.stages)) { const shader = STYLES[styleName]; @@ -3241,7 +3999,10 @@ export class StyleManager { density: parseInt(this._detectionDensitySlider?.value || '50', 10), allocation: getDetectionTuning().allocationStrategy, fadePct: parseInt(this._detectionFadeSlider?.value || '7', 10), - outsideOpacityPct: parseInt(this._detectionOpacitySlider?.value || '0', 10), + outsideOpacityPct: parseInt( + this._detectionOpacitySlider?.value || '0', + 10, + ), }, scope: { enabled: isScopeMaskEnabled(), @@ -3281,7 +4042,7 @@ export class StyleManager { if (typeof bloomState.intensity === 'number' && this._bloomSlider) { const intensity = decodeBloomIntensity( bloomState.intensity, - bloomState.version ?? state.bloomVersion ?? BLOOM_SCALE_VERSION + bloomState.version ?? state.bloomVersion ?? BLOOM_SCALE_VERSION, ); this._setBloomIntensity(intensity, { syncShare: false }); } @@ -3291,7 +4052,10 @@ export class StyleManager { const sharpenState = state.sharpen || {}; if (typeof sharpenState.intensity === 'number' && this._sharpenSlider) { - const pct = Math.max(0, Math.min(100, Math.round(sharpenState.intensity))); + const pct = Math.max( + 0, + Math.min(100, Math.round(sharpenState.intensity)), + ); this._sharpenSlider.value = String(pct); this._sharpenSliderValue.textContent = `${pct}%`; this._applySharpenIntensity(pct / 100); @@ -3318,25 +4082,40 @@ export class StyleManager { if (typeof scopeState.featherPct === 'number' && this._scopeFeatherSlider) { const pct = Math.max(0, Math.min(100, Math.round(scopeState.featherPct))); this._scopeFeatherSlider.value = String(pct); - if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`; + if (this._scopeFeatherValue) + this._scopeFeatherValue.textContent = `${pct}%`; setScopeMaskFeather(pct / 100); } const detectionState = state.detection || {}; - if (typeof detectionState.density === 'number' && this._detectionDensitySlider) { + if ( + typeof detectionState.density === 'number' && + this._detectionDensitySlider + ) { const pct = canonicalizeDensity(detectionState.density); this._detectionDensitySlider.value = String(pct); - if (this._detectionDensityValue) this._detectionDensityValue.textContent = `${pct}%`; + if (this._detectionDensityValue) + this._detectionDensityValue.textContent = `${pct}%`; this._applyDetectionDensityFromUi(); } if (detectionState.allocation) { - this._setDetectionAllocation(detectionState.allocation, { syncShare: false }); + this._setDetectionAllocation(detectionState.allocation, { + syncShare: false, + }); } - if (typeof detectionState.fadePct === 'number' && this._detectionFadeSlider) { + if ( + typeof detectionState.fadePct === 'number' && + this._detectionFadeSlider + ) { this._detectionFadeSlider.value = String(detectionState.fadePct); } - if (typeof detectionState.outsideOpacityPct === 'number' && this._detectionOpacitySlider) { - this._detectionOpacitySlider.value = String(detectionState.outsideOpacityPct); + if ( + typeof detectionState.outsideOpacityPct === 'number' && + this._detectionOpacitySlider + ) { + this._detectionOpacitySlider.value = String( + detectionState.outsideOpacityPct, + ); } this._applyDetectionFadeFromUi(); if (detectionState.mode) { @@ -3348,7 +4127,8 @@ export class StyleManager { // so it needs a gate on BOTH sides of the await. if (superseded()) return false; const stackBefore = this.mapStackController?.getActiveId?.() ?? null; - const genBefore = this.mapStackController?.getSwitchGeneration?.() ?? null; + const genBefore = + this.mapStackController?.getSwitchGeneration?.() ?? null; await this._setMapStack(state.mapStack, { syncShare: false }); @@ -3359,13 +4139,14 @@ export class StyleManager { // state that omits `mapStack` never issues one — every normalized scene // shot omits it — so this stale globe would simply stand. Put back what // the winner inherited. - const genAfter = this.mapStackController?.getSwitchGeneration?.() ?? null; + const genAfter = + this.mapStackController?.getSwitchGeneration?.() ?? null; // _setMapStack issues exactly one setStack(), which advances the // generation once, or not at all when the stack was unavailable and // nothing was mutated. Anything past that is a NEWER switch whose // caller owns the globe now, and reverting would stomp a live intent. - const globeIsStillOurs = genBefore !== null && genAfter !== null - && genAfter <= genBefore + 1; + const globeIsStillOurs = + genBefore !== null && genAfter !== null && genAfter <= genBefore + 1; const landed = this.mapStackController?.getActiveId?.() ?? null; if (globeIsStillOurs && stackBefore && landed !== stackBefore) { await this._setMapStack(stackBefore, { syncShare: false }); @@ -3395,31 +4176,44 @@ export class StyleManager { * Resets the safe-frame overlay to its inactive state on init. * @returns {void} */ - _initRecordingOverlay() { return this._recording._initRecordingOverlay(); } + _initRecordingOverlay() { + return this._recording._initRecordingOverlay(); + } /** * Applies recording-friendly post-processing and shader uniform overrides. * @param {object} preset */ applyCinematicPreset(preset = {}) { - const bloomInput = typeof preset.bloom === 'object' ? preset.bloom : { intensity: preset.bloom }; + const bloomInput = + typeof preset.bloom === 'object' + ? preset.bloom + : { intensity: preset.bloom }; let decodedBloomIntensity = null; if (typeof bloomInput.intensity === 'number') { decodedBloomIntensity = decodeBloomIntensity( bloomInput.intensity, - bloomInput.version ?? preset.bloomVersion ?? BLOOM_SCALE_VERSION + bloomInput.version ?? preset.bloomVersion ?? BLOOM_SCALE_VERSION, ); this._setBloomIntensity(decodedBloomIntensity, { syncShare: false }); } if (typeof bloomInput.enabled === 'boolean') { this._setBloomEnabled(bloomInput.enabled); } else if (typeof bloomInput.intensity === 'number') { - this._setBloomEnabled((decodedBloomIntensity ?? this._getBloomIntensity()) > 0); + this._setBloomEnabled( + (decodedBloomIntensity ?? this._getBloomIntensity()) > 0, + ); } - const sharpenInput = typeof preset.sharpen === 'object' ? preset.sharpen : { enabled: preset.sharpen }; + const sharpenInput = + typeof preset.sharpen === 'object' + ? preset.sharpen + : { enabled: preset.sharpen }; if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) { - const sharpenPct = Math.max(0, Math.min(100, Math.round(sharpenInput.intensity))); + const sharpenPct = Math.max( + 0, + Math.min(100, Math.round(sharpenInput.intensity)), + ); this._sharpenSlider.value = String(sharpenPct); this._sharpenSliderValue.textContent = `${sharpenPct}%`; this._applySharpenIntensity(sharpenPct / 100); @@ -3437,14 +4231,19 @@ export class StyleManager { if (preset.detectionMode) { this._setDetectionMode(preset.detectionMode); } - if (typeof preset.detectionDensity === 'number' && this._detectionDensitySlider) { + if ( + typeof preset.detectionDensity === 'number' && + this._detectionDensitySlider + ) { const density = canonicalizeDensity(preset.detectionDensity); this._detectionDensitySlider.value = String(density); this._detectionDensityValue.textContent = `${density}%`; this._applyDetectionDensityFromUi(); } if (preset.detectionAllocation) { - this._setDetectionAllocation(preset.detectionAllocation, { syncShare: false }); + this._setDetectionAllocation(preset.detectionAllocation, { + syncShare: false, + }); } if (preset.styleParams && typeof preset.styleParams === 'object') { @@ -3476,7 +4275,9 @@ export class StyleManager { * @param {string} [options.safeFrame='16:9'] - Aspect ratio for the safe-frame overlay. * @returns {void} */ - setRecordingMode(enabled, options) { return this._recording.setRecordingMode(enabled, options); } + setRecordingMode(enabled, options) { + return this._recording.setRecordingMode(enabled, options); + } // ── Parameter Sliders ───────────────────────── @@ -3489,7 +4290,9 @@ export class StyleManager { */ _updateSliderPanel(styleName, { reveal = false } = {}) { const { governorRequestRender } = this.services; - this._styleParameters ||= createStyleParameters({ container: this._sliderContainer }); + this._styleParameters ||= createStyleParameters({ + container: this._sliderContainer, + }); this._styleParameters.clear(); const shader = STYLES[styleName]; @@ -3525,13 +4328,15 @@ export class StyleManager { this._sliderPanel.classList.remove('collapsed'); this._syncPanelCollapseButton(this._sliderPanel); this.setPanelCollapsed('pp-toggles', false, { explicit: true }); - this._lifetime.frame(() => this._lifetime.frame(() => { - const scrollOwner = this._ppToggles; - if (!scrollOwner) return; - const ownerRect = scrollOwner.getBoundingClientRect(); - const panelRect = this._sliderPanel.getBoundingClientRect(); - scrollOwner.scrollTop += panelRect.top - ownerRect.top - 8; - })); + this._lifetime.frame(() => + this._lifetime.frame(() => { + const scrollOwner = this._ppToggles; + if (!scrollOwner) return; + const ownerRect = scrollOwner.getBoundingClientRect(); + const panelRect = this._sliderPanel.getBoundingClientRect(); + scrollOwner.scrollTop += panelRect.top - ownerRect.top - 8; + }), + ); } // ── Style switching ─────────────────────────── @@ -3547,15 +4352,19 @@ export class StyleManager { * @param {boolean} [options.applyPreset=true] - Whether to apply STYLE_PRESET_DEFAULTS for the new style. * @returns {void} */ - setStyle(styleName, { - applyPreset = true, - revealParameters = applyPreset, - restore = false, - } = {}) { + setStyle( + styleName, + { + applyPreset = true, + revealParameters = applyPreset, + restore = false, + } = {}, + ) { const { setDetectionStyle } = this.services; if (!restore) this.shareLinkManager?.claimRestoreLane?.('visual'); if (styleName === this.activeStyle) { - if (revealParameters && styleName !== 'normal') this._revealStyleParameters(); + if (revealParameters && styleName !== 'normal') + this._revealStyleParameters(); return; } @@ -3569,12 +4378,20 @@ export class StyleManager { // Transition out the previous shader style if (previousStyle !== 'normal' && this.stages[previousStyle]) { - this._startTransition(previousStyle, this.stages[previousStyle].uniforms.intensity, 0.0); + this._startTransition( + previousStyle, + this.stages[previousStyle].uniforms.intensity, + 0.0, + ); } // Transition in the new shader style if (styleName !== 'normal' && this.stages[styleName]) { - this._startTransition(styleName, this.stages[styleName].uniforms.intensity, 1.0); + this._startTransition( + styleName, + this.stages[styleName].uniforms.intensity, + 1.0, + ); } if (applyPreset) { @@ -3582,13 +4399,14 @@ export class StyleManager { } // Update button UI - document.querySelectorAll('.style-btn').forEach(btn => { + document.querySelectorAll('.style-btn').forEach((btn) => { btn.classList.toggle('active', btn.dataset.style === styleName); }); // Update style indicator const displayNames = { surveillance: 'NVG', thermal: 'FLIR', retro: 'CRT' }; - this._styleIndicator.textContent = displayNames[styleName] || styleName.toUpperCase(); + this._styleIndicator.textContent = + displayNames[styleName] || styleName.toUpperCase(); this._updateStyleMiniStatus(styleName); // Update parameter sliders @@ -3601,9 +4419,11 @@ export class StyleManager { // Sync detection overlay tone to active post-process style setDetectionStyle(styleName); this._syncIrBoost(); - window.dispatchEvent(new CustomEvent('gev:style-change', { - detail: { style: styleName }, - })); + window.dispatchEvent( + new CustomEvent('gev:style-change', { + detail: { style: styleName }, + }), + ); this._syncCockpitInheritedStyle(); @@ -3634,10 +4454,14 @@ export class StyleManager { * @param {number} [now] - performance.now() sample. * @returns {void} */ - _updateGlobalLoadingFeedback(now) { return this._feedback._updateGlobalLoadingFeedback(now); } + _updateGlobalLoadingFeedback(now) { + return this._feedback._updateGlobalLoadingFeedback(now); + } /** Show a message in the universal top-center status banner. */ - _showGlobalStatusNotice(message, options) { return this._feedback._showGlobalStatusNotice(message, options); } + _showGlobalStatusNotice(message, options) { + return this._feedback._showGlobalStatusNotice(message, options); + } /** * Style animation loop — self-stopping (perf wave 2). Runs only while a @@ -3657,7 +4481,9 @@ export class StyleManager { * its own `stats.loading` without emitting a manager event, and that is * the one loading start the event path cannot see. */ - _startTrafficChipTicker() { return this._feedback._startTrafficChipTicker(); } + _startTrafficChipTicker() { + return this._feedback._startTrafficChipTicker(); + } /** * Self-stopping 60 ms ticker for the global loading chip. @@ -3673,10 +4499,14 @@ export class StyleManager { * (rebase 2026-08-16: main's loading chip vs wave 2's stopped loop) * @returns {void} */ - _armLoadingFeedbackTicker() { return this._feedback._armLoadingFeedbackTicker(); } + _armLoadingFeedbackTicker() { + return this._feedback._armLoadingFeedbackTicker(); + } /** Stop the loading-chip ticker if it is running. Idempotent. */ - _stopLoadingFeedbackTicker() { return this._feedback._stopLoadingFeedbackTicker(); } + _stopLoadingFeedbackTicker() { + return this._feedback._stopLoadingFeedbackTicker(); + } // ── Location Bar ───────────────────────────── @@ -3693,13 +4523,17 @@ export class StyleManager { this._locationLookup = new LocationSearch({ input: this._locationSearch, begin: () => this._beginDeferredNavigation('location'), - isCurrent: (generation) => !this._disposed && generation === this._navigationGeneration, + isCurrent: (generation) => + !this._disposed && generation === this._navigationGeneration, beforeFly: (generation) => this._reassertNavigationHandoff(generation), - search: (query, options) => searchAndFlyTo(this.viewer, query, { - placeSearch: this.placeSearch, - ...options, - }), - onStart: (generation) => { this._activeLocationSearchGeneration = generation; }, + search: (query, options) => + searchAndFlyTo(this.viewer, query, { + placeSearch: this.placeSearch, + ...options, + }), + onStart: (generation) => { + this._activeLocationSearchGeneration = generation; + }, onResult: (destination, query) => { this._searchedLocationLabel = destination.label || query; this._setActiveLocation(null); @@ -3716,10 +4550,14 @@ export class StyleManager { }); this._locationControls = new LocationControls({ elements: { - pills: this._locationPills, poiRow: this._poiRow, divider: this._locationBarDivider, - search: this._locationSearch, searchToggle: this._searchToggle, + pills: this._locationPills, + poiRow: this._poiRow, + divider: this._locationBarDivider, + search: this._locationSearch, + searchToggle: this._searchToggle, resetButtons: [this._resetGlobeBtn, this._cockpitResetGlobeBtn], - statusCity: this._locationMiniCity, statusPoi: this._locationMiniPoi, + statusCity: this._locationMiniCity, + statusPoi: this._locationMiniPoi, }, cities: CITY_POIS, getExpandedCity: () => this._expandedCityId, @@ -3810,8 +4648,11 @@ export class StyleManager { return; } - const isCityChanged = this._activeLocationId && this._activeLocationId !== cityId; - const result = this._flyWithTransition(!!isCityChanged, (hooks) => flyToPresetLocation(this.viewer, cityId, hooks)); + const isCityChanged = + this._activeLocationId && this._activeLocationId !== cityId; + const result = this._flyWithTransition(!!isCityChanged, (hooks) => + flyToPresetLocation(this.viewer, cityId, hooks), + ); if (result === false) return; this._expandPOIRow(cityId); this._setActiveLocation(cityId); @@ -3835,8 +4676,11 @@ export class StyleManager { */ _onPoiClick(cityId, poiIndex) { const { CITY_POIS, flyToPOI } = this.services; - const isCityChanged = this._activeLocationId && this._activeLocationId !== cityId; - const result = this._flyWithTransition(!!isCityChanged, (hooks) => flyToPOI(this.viewer, cityId, poiIndex, hooks)); + const isCityChanged = + this._activeLocationId && this._activeLocationId !== cityId; + const result = this._flyWithTransition(!!isCityChanged, (hooks) => + flyToPOI(this.viewer, cityId, poiIndex, hooks), + ); if (result === false) return; this._setActiveLocation(cityId); this._activePoiIndex = poiIndex; @@ -3929,7 +4773,9 @@ export class StyleManager { */ _updateStyleMiniStatus(styleName = this.activeStyle) { if (!this._styleMiniValue) return; - this._styleMiniValue.textContent = STYLE_STATUS_LABELS[styleName] || String(styleName || 'normal').toUpperCase(); + this._styleMiniValue.textContent = + STYLE_STATUS_LABELS[styleName] || + String(styleName || 'normal').toUpperCase(); } // ── Orbit Mode ────────────────────────────── @@ -3976,7 +4822,10 @@ export class StyleManager { _initClearSelectedLayersButton() { if (!this._clearSelectedLayersBtn) return; this._clearLayersControl?.destroy(); - this._clearLayersControl = bindClearLayersControl(this._clearSelectedLayersBtn, () => this.clearSelectedLayers()); + this._clearLayersControl = bindClearLayersControl( + this._clearSelectedLayersBtn, + () => this.clearSelectedLayers(), + ); } /** @@ -3985,7 +4834,9 @@ export class StyleManager { * established disable lifecycle. * @returns {Promise} Aggregate manager lifecycle truth for the batch. */ - clearSelectedLayers(...args) { return this._contextControls?.clearSelectedLayers(...args); } + clearSelectedLayers(...args) { + return this._contextControls?.clearSelectedLayers(...args); + } /** * Release every camera owner and return to the canonical full-globe frame. @@ -3993,7 +4844,17 @@ export class StyleManager { * @returns {Promise} Canonical reset result shared with voice. */ resetToGlobeView() { - const { GLOBE_VIEW, flyToGlobeView, interruptCameraMotion, flightsLayer, militaryFlightsLayer, satellitesLayer, aisLiveVesselsLayer, militaryAwarenessLayer, rocketLaunchesLayer } = this.services; + const { + GLOBE_VIEW, + flyToGlobeView, + interruptCameraMotion, + flightsLayer, + militaryFlightsLayer, + satellitesLayer, + aisLiveVesselsLayer, + militaryAwarenessLayer, + rocketLaunchesLayer, + } = this.services; if (this._globeResetPromise) return this._globeResetPromise; this._stampNavigation(); interruptCameraMotion('reset-globe'); @@ -4003,19 +4864,41 @@ export class StyleManager { militaryAwarenessLayer.releaseCameraOwnership?.({ origin: 'tool' }); } catch { // Keep reset available if Context has not initialized completely. - try { flightsLayer.stopTracking?.({ origin: 'tool' }); } catch { /* best-effort release */ } - try { militaryFlightsLayer.stopTracking?.({ origin: 'tool' }); } catch { /* best-effort release */ } - try { aisLiveVesselsLayer.clearSelection?.(); } catch { /* best-effort release */ } + try { + flightsLayer.stopTracking?.({ origin: 'tool' }); + } catch { + /* best-effort release */ + } + try { + militaryFlightsLayer.stopTracking?.({ origin: 'tool' }); + } catch { + /* best-effort release */ + } + try { + aisLiveVesselsLayer.clearSelection?.(); + } catch { + /* best-effort release */ + } + } + try { + satellitesLayer.stopTracking?.({ origin: 'tool' }); + } catch { + /* best-effort release */ + } + try { + rocketLaunchesLayer.releaseCameraOwnership?.(); + } catch { + /* best-effort release */ } - try { satellitesLayer.stopTracking?.({ origin: 'tool' }); } catch { /* best-effort release */ } - try { rocketLaunchesLayer.releaseCameraOwnership?.(); } catch { /* best-effort release */ } this.viewer.trackedEntity = undefined; this.viewer.camera.cancelFlight(); this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); this._beginWorldJumpTransition(); let resolveReset; - const resetPromise = new Promise((resolve) => { resolveReset = resolve; }); + const resetPromise = new Promise((resolve) => { + resolveReset = resolve; + }); this._globeResetPromise = resetPromise; let settled = false; let timer = null; @@ -4035,17 +4918,32 @@ export class StyleManager { longitude: Number(Cesium.Math.toDegrees(carto.longitude).toFixed(2)), }, }; - this._resetGlobeBtn?.setAttribute('aria-label', 'Reset to full globe view'); - this._cockpitResetGlobeBtn?.setAttribute('aria-label', 'Reset cockpit to full globe view'); + this._resetGlobeBtn?.setAttribute( + 'aria-label', + 'Reset to full globe view', + ); + this._cockpitResetGlobeBtn?.setAttribute( + 'aria-label', + 'Reset cockpit to full globe view', + ); this._globeResetPromise = null; resolveReset(result); }; timer = window.setTimeout(() => { const height = this.viewer.camera.positionCartographic?.height; - finish(!Number.isFinite(height) || Math.abs(height - GLOBE_VIEW.heightM) > 1000); + finish( + !Number.isFinite(height) || + Math.abs(height - GLOBE_VIEW.heightM) > 1000, + ); }, 4200); - this._resetGlobeBtn?.setAttribute('aria-label', 'Resetting to full globe view'); - this._cockpitResetGlobeBtn?.setAttribute('aria-label', 'Resetting cockpit to full globe view'); + this._resetGlobeBtn?.setAttribute( + 'aria-label', + 'Resetting to full globe view', + ); + this._cockpitResetGlobeBtn?.setAttribute( + 'aria-label', + 'Resetting cockpit to full globe view', + ); const target = flyToGlobeView(this.viewer, { onComplete: () => finish(false), onCancel: () => finish(true), @@ -4072,7 +4970,9 @@ export class StyleManager { * @param {string} message - Text to show in the toast. * @returns {void} */ - _showToast(message) { return this._feedback._showToast(message); } + _showToast(message) { + return this._feedback._showToast(message); + } // ── HUD Toggle ─────────────────────────────── @@ -4114,7 +5014,8 @@ export class StyleManager { } _syncModels3dModeRow() { - if (this._models3dModeRow) this._models3dModeRow.classList.toggle('visible', this._models3dEnabled); + if (this._models3dModeRow) + this._models3dModeRow.classList.toggle('visible', this._models3dEnabled); this._layoutRightPanels(); } @@ -4150,7 +5051,10 @@ export class StyleManager { // ACTIVE from markup (default-on, 2026-08-22): the very first thing assistive // tech reported was an unpressed-looking control over an armed layer. // Mirrors #scope-toggle, which has always carried aria-pressed. - this._models3dBtn?.setAttribute('aria-pressed', String(this._models3dEnabled)); + this._models3dBtn?.setAttribute( + 'aria-pressed', + String(this._models3dEnabled), + ); } _initHUDToggle() { @@ -4162,7 +5066,8 @@ export class StyleManager { this._updateHudButtonState(); this._lifetime.listen(this._cockpitDisplayToggleBtn, 'click', () => { - const open = this._cockpitDisplayToggleBtn.getAttribute('aria-expanded') === 'true'; + const open = + this._cockpitDisplayToggleBtn.getAttribute('aria-expanded') === 'true'; this._setCockpitDisclosure?.('display', !open); }); this._initCockpitDisplayPortal(); @@ -4198,10 +5103,18 @@ export class StyleManager { * @param {boolean} active Whether Cockpit owns the Display control groups. * @returns {void} */ - _setCockpitDisplayPortalActive(active) { this._cockpitDisplayPortal?.setActive(active); } - get _cockpitDisplayPortalActive() { return this._cockpitDisplayPortal?.active ?? false; } - get _displayPortalScrollRestoreOwner() { return this._cockpitDisplayPortal?.restoreOwner ?? null; } - get _standardDisplayScrollTop() { return this._cockpitDisplayPortal?.standardScrollTop ?? 0; } + _setCockpitDisplayPortalActive(active) { + this._cockpitDisplayPortal?.setActive(active); + } + get _cockpitDisplayPortalActive() { + return this._cockpitDisplayPortal?.active ?? false; + } + get _displayPortalScrollRestoreOwner() { + return this._cockpitDisplayPortal?.restoreOwner ?? null; + } + get _standardDisplayScrollTop() { + return this._cockpitDisplayPortal?.standardScrollTop ?? 0; + } /** * Syncs the HUD toggle button active class and HUD layout row visibility @@ -4227,9 +5140,12 @@ export class StyleManager { const btn = this._detectionBtn; const enabled = modeLabel !== 'OFF'; btn.setAttribute('aria-pressed', String(enabled)); - btn.setAttribute('aria-label', enabled - ? `Detection overlay: ${String(modeLabel).toLowerCase()}` - : 'Detection overlay: off'); + btn.setAttribute( + 'aria-label', + enabled + ? `Detection overlay: ${String(modeLabel).toLowerCase()}` + : 'Detection overlay: off', + ); btn.classList.remove('active', 'god', 'panoptic'); if (modeLabel === 'SPARSE') { btn.querySelector('.pp-label').textContent = 'SPARSE'; @@ -4248,13 +5164,19 @@ export class StyleManager { this._detectionSliderRow.classList.toggle('visible', modeLabel !== 'OFF'); } if (this._detectionAllocationRow) { - this._detectionAllocationRow.classList.toggle('visible', modeLabel !== 'OFF'); + this._detectionAllocationRow.classList.toggle( + 'visible', + modeLabel !== 'OFF', + ); } if (this._detectionFadeRow) { this._detectionFadeRow.classList.toggle('visible', modeLabel !== 'OFF'); } if (this._detectionOpacityRow) { - this._detectionOpacityRow.classList.toggle('visible', modeLabel !== 'OFF'); + this._detectionOpacityRow.classList.toggle( + 'visible', + modeLabel !== 'OFF', + ); } this._layoutRightPanels(); } @@ -4285,7 +5207,10 @@ export class StyleManager { return; } const rect = this._cctvPanel.getBoundingClientRect(); - const availableHeight = Math.max(190, Math.floor(window.innerHeight - rect.top - 12)); + const availableHeight = Math.max( + 190, + Math.floor(window.innerHeight - rect.top - 12), + ); this._cctvPanel.style.maxHeight = `${availableHeight}px`; if (inner) { inner.style.maxHeight = `${availableHeight}px`; @@ -4300,7 +5225,10 @@ export class StyleManager { /** Terminal result for the complete initial share restoration. */ get initialRestorePromise() { - return this._initialShareRestorePromise || Promise.resolve({ status: 'not-requested' }); + return ( + this._initialShareRestorePromise || + Promise.resolve({ status: 'not-requested' }) + ); } _settleInitialShareRestore(result) { @@ -4308,7 +5236,9 @@ export class StyleManager { const resolve = this._resolveInitialShareRestore; this._resolveInitialShareRestore = null; resolve(result); - window.dispatchEvent(new CustomEvent('gev:initial-share-restore-settled', { detail: result })); + window.dispatchEvent( + new CustomEvent('gev:initial-share-restore-settled', { detail: result }), + ); } /** @@ -4318,7 +5248,8 @@ export class StyleManager { * @returns {Promise} Resolves after focused-session state restoration. */ async dispose() { - const { destroyTrackedReadout, destroyWorldOverlay, destroyDetection } = this.services; + const { destroyTrackedReadout, destroyWorldOverlay, destroyDetection } = + this.services; if (this._disposed) return; this._shareTrackingNoticeGeneration += 1; this._shareTrackingAcquiringKey = null; @@ -4341,7 +5272,8 @@ export class StyleManager { this._cockpitDisplayPortal?.stop(); this._visualEffects.stop(); this._styleParameters?.destroy(); - for (const control of this._panelDisclosureControls || []) control.destroy(); + for (const control of this._panelDisclosureControls || []) + control.destroy(); this._panelDisclosureControls = []; this._hoverPanelControls?.forEach((control) => control.destroy()); this._hoverPanelControls?.clear(); @@ -4353,19 +5285,35 @@ export class StyleManager { this._layerStateRestorePromise = null; clearTimeout(this._initialShareRestoreTimeout); this._initialShareRestoreTimeout = null; - this._settleInitialShareRestore({ status: 'destroyed', share: null, layers: [] }); + this._settleInitialShareRestore({ + status: 'destroyed', + share: null, + layers: [], + }); if (this._initialShareGestureHandler) { - this.viewer?.canvas?.removeEventListener('pointerdown', this._initialShareGestureHandler); - this.viewer?.canvas?.removeEventListener('wheel', this._initialShareGestureHandler); + this.viewer?.canvas?.removeEventListener( + 'pointerdown', + this._initialShareGestureHandler, + ); + this.viewer?.canvas?.removeEventListener( + 'wheel', + this._initialShareGestureHandler, + ); this._initialShareGestureHandler = null; } this.shareLinkManager?.destroy(); if (this._awarenessSelectedHandler) { - window.removeEventListener('gev:awareness-subject-selected', this._awarenessSelectedHandler); + window.removeEventListener( + 'gev:awareness-subject-selected', + this._awarenessSelectedHandler, + ); this._awarenessSelectedHandler = null; } if (this._awarenessClearedHandler) { - window.removeEventListener('gev:awareness-subject-cleared', this._awarenessClearedHandler); + window.removeEventListener( + 'gev:awareness-subject-cleared', + this._awarenessClearedHandler, + ); this._awarenessClearedHandler = null; } diff --git a/src/ui/panelLayoutController.js b/src/ui/panelLayoutController.js index 05b78dece..b3ca22cd1 100644 --- a/src/ui/panelLayoutController.js +++ b/src/ui/panelLayoutController.js @@ -50,7 +50,12 @@ const RIGHT_STACK_OBSTACLE_SELECTOR = [ '#gev-voice-control', ].join(', '); export class PanelLayoutController { - constructor({ readHud, scheduleCockpitLayout, syncPanelCollapseButton, readDisplayScrollTop }) { + constructor({ + readHud, + scheduleCockpitLayout, + syncPanelCollapseButton, + readDisplayScrollTop, + }) { this.readHud = readHud; this.scheduleCockpitLayout = scheduleCockpitLayout; this._syncPanelCollapseButton = syncPanelCollapseButton; @@ -103,7 +108,9 @@ export class PanelLayoutController { if (!dock) return; this._commandDockTrayObserver?.disconnect?.(); if (typeof ResizeObserver === 'function') { - this._commandDockTrayObserver = new ResizeObserver(() => this._updateCommandDockTrayStack()); + this._commandDockTrayObserver = new ResizeObserver(() => + this._updateCommandDockTrayStack(), + ); dock.querySelectorAll('.dock-popover-content').forEach((tray) => { this._commandDockTrayObserver.observe(tray); }); @@ -115,24 +122,48 @@ export class PanelLayoutController { if (this.destroyed) return; const dock = document.getElementById('command-dock'); if (!dock) return; - const locationPanel = dock.querySelector('#location-bar.dock-pinned:not(.collapsed)'); - const presetsPanel = dock.querySelector('#control-panel.dock-pinned:not(.collapsed)'); - const locationHeight = locationPanel?.querySelector('.dock-popover-content')?.getBoundingClientRect().height || 0; - const presetsHeight = presetsPanel?.querySelector('.dock-popover-content')?.getBoundingClientRect().height || 0; + const locationPanel = dock.querySelector( + '#location-bar.dock-pinned:not(.collapsed)', + ); + const presetsPanel = dock.querySelector( + '#control-panel.dock-pinned:not(.collapsed)', + ); + const locationHeight = + locationPanel + ?.querySelector('.dock-popover-content') + ?.getBoundingClientRect().height || 0; + const presetsHeight = + presetsPanel + ?.querySelector('.dock-popover-content') + ?.getBoundingClientRect().height || 0; const pinnedCount = Number(locationHeight > 0) + Number(presetsHeight > 0); const locationHeightPx = Math.ceil(locationHeight); const presetsHeightPx = Math.ceil(presetsHeight); - const topPinnedPanel = dock.querySelector('.dock-pinned-top.dock-pinned:not(.collapsed)'); - const lowerPinnedPanel = topPinnedPanel?.id === 'location-bar' ? presetsPanel : locationPanel; - const lowerPinnedHeight = lowerPinnedPanel - ?.querySelector('.dock-popover-content') - ?.getBoundingClientRect().height || 0; - const stackHeight = pinnedCount > 1 - ? `calc(${locationHeightPx}px + ${presetsHeightPx}px + 1.2rem)` - : `${locationHeightPx + presetsHeightPx}px`; - dock.style.setProperty('--dock-location-pinned-height', `${locationHeightPx}px`); - dock.style.setProperty('--dock-presets-pinned-height', `${presetsHeightPx}px`); - dock.style.setProperty('--dock-lower-pinned-height', `${Math.ceil(lowerPinnedHeight)}px`); + const topPinnedPanel = dock.querySelector( + '.dock-pinned-top.dock-pinned:not(.collapsed)', + ); + const lowerPinnedPanel = + topPinnedPanel?.id === 'location-bar' ? presetsPanel : locationPanel; + const lowerPinnedHeight = + lowerPinnedPanel + ?.querySelector('.dock-popover-content') + ?.getBoundingClientRect().height || 0; + const stackHeight = + pinnedCount > 1 + ? `calc(${locationHeightPx}px + ${presetsHeightPx}px + 1.2rem)` + : `${locationHeightPx + presetsHeightPx}px`; + dock.style.setProperty( + '--dock-location-pinned-height', + `${locationHeightPx}px`, + ); + dock.style.setProperty( + '--dock-presets-pinned-height', + `${presetsHeightPx}px`, + ); + dock.style.setProperty( + '--dock-lower-pinned-height', + `${Math.ceil(lowerPinnedHeight)}px`, + ); dock.style.setProperty('--dock-pinned-stack-height', stackHeight); dock.classList.toggle('dock-has-pinned-tray', pinnedCount > 0); dock.classList.toggle('dock-has-two-pinned-trays', pinnedCount > 1); @@ -177,12 +208,18 @@ export class PanelLayoutController { this._scheduleRightPanelLayout(); }); this._rightStackResizeObserver.observe(stack); - for (const panel of [this._ppToggles, this._cctvPanel, globalContextPanel]) { + for (const panel of [ + this._ppToggles, + this._cctvPanel, + globalContextPanel, + ]) { if (panel) this._rightStackResizeObserver.observe(panel); } - document.querySelectorAll(RIGHT_STACK_OBSTACLE_SELECTOR).forEach((element) => { - this._rightStackResizeObserver.observe(element); - }); + document + .querySelectorAll(RIGHT_STACK_OBSTACLE_SELECTOR) + .forEach((element) => { + this._rightStackResizeObserver.observe(element); + }); } if (typeof MutationObserver !== 'undefined') { @@ -209,11 +246,17 @@ export class PanelLayoutController { const transitionHud = document.getElementById('intel-hud'); if (transitionHud) { this._rightStackHudTransitionHandler = (event) => { - if (event.propertyName === 'opacity' || event.propertyName === 'visibility') { + if ( + event.propertyName === 'opacity' || + event.propertyName === 'visibility' + ) { this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true }); } }; - transitionHud.addEventListener('transitionend', this._rightStackHudTransitionHandler); + transitionHud.addEventListener( + 'transitionend', + this._rightStackHudTransitionHandler, + ); } this._scheduleRightPanelLayout(); @@ -228,7 +271,9 @@ export class PanelLayoutController { this._rightStackLayoutFrame = null; if (this._rightStackReconsiderAutoCollapse) { this._rightStackReconsiderAutoCollapse = false; - for (const panel of this._rightPanelStack.querySelectorAll('.layout-auto-collapsed')) { + for (const panel of this._rightPanelStack.querySelectorAll( + '.layout-auto-collapsed', + )) { panel.classList.remove('collapsed', 'layout-auto-collapsed'); this._syncPanelCollapseButton(panel); } @@ -265,12 +310,16 @@ export class PanelLayoutController { this._leftStackResizeObserver.observe(stack); stack.querySelectorAll(':scope > [data-panel-id]').forEach((panel) => { this._leftStackResizeObserver.observe(panel); - const inner = [...panel.children].find((child) => !child.classList.contains('panel-glow')); + const inner = [...panel.children].find( + (child) => !child.classList.contains('panel-glow'), + ); if (inner) this._leftStackResizeObserver.observe(inner); }); - document.querySelectorAll(LEFT_STACK_OBSTACLE_SELECTOR).forEach((element) => { - this._leftStackResizeObserver.observe(element); - }); + document + .querySelectorAll(LEFT_STACK_OBSTACLE_SELECTOR) + .forEach((element) => { + this._leftStackResizeObserver.observe(element); + }); } if (typeof MutationObserver !== 'undefined') { @@ -303,7 +352,10 @@ export class PanelLayoutController { const transitionHud = document.getElementById('intel-hud'); if (transitionHud) { this._leftStackHudTransitionHandler = (event) => { - if (event.propertyName === 'opacity' || event.propertyName === 'visibility') { + if ( + event.propertyName === 'opacity' || + event.propertyName === 'visibility' + ) { this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true }); // The Cockpit strip hangs off the HUD's REC readout, so it has to // remeasure on the same event: the readout keeps its rect through @@ -311,7 +363,10 @@ export class PanelLayoutController { this.scheduleCockpitLayout(); } }; - transitionHud.addEventListener('transitionend', this._leftStackHudTransitionHandler); + transitionHud.addEventListener( + 'transitionend', + this._leftStackHudTransitionHandler, + ); } this._leftStackCockpitModeHandler = () => { @@ -321,12 +376,22 @@ export class PanelLayoutController { // the accordion remains in the same obstacle-safe lane instead of // jumping to a cockpit-specific top anchor. this._scheduleLeftPanelLayout(); - if (this._cockpitLayoutFrame !== null) cancelAnimationFrame(this._cockpitLayoutFrame); - this._cockpitLayoutFrame = requestAnimationFrame(() => { this._cockpitLayoutFrame = null; this._scheduleLeftPanelLayout(); }); + if (this._cockpitLayoutFrame !== null) + cancelAnimationFrame(this._cockpitLayoutFrame); + this._cockpitLayoutFrame = requestAnimationFrame(() => { + this._cockpitLayoutFrame = null; + this._scheduleLeftPanelLayout(); + }); clearTimeout(this._cockpitLayoutTimer); - this._cockpitLayoutTimer = setTimeout(() => { this._cockpitLayoutTimer = null; this._scheduleLeftPanelLayout(); }, 300); + this._cockpitLayoutTimer = setTimeout(() => { + this._cockpitLayoutTimer = null; + this._scheduleLeftPanelLayout(); + }, 300); }; - window.addEventListener('gev:cockpit-mode-changed', this._leftStackCockpitModeHandler); + window.addEventListener( + 'gev:cockpit-mode-changed', + this._leftStackCockpitModeHandler, + ); this._scheduleLeftPanelLayout(); } @@ -340,7 +405,9 @@ export class PanelLayoutController { this._leftStackLayoutFrame = null; if (this._leftStackReconsiderAutoCollapse) { this._leftStackReconsiderAutoCollapse = false; - for (const panel of this._leftPanelStack.querySelectorAll('.layout-auto-collapsed')) { + for (const panel of this._leftPanelStack.querySelectorAll( + '.layout-auto-collapsed', + )) { panel.classList.remove('collapsed', 'layout-auto-collapsed'); this._syncPanelCollapseButton(panel); } @@ -366,21 +433,42 @@ export class PanelLayoutController { destroy() { if (this.destroyed) return; this.destroyed = true; - for (const field of ['_leftStackLayoutFrame', '_rightStackLayoutFrame', '_cockpitLayoutFrame']) { + for (const field of [ + '_leftStackLayoutFrame', + '_rightStackLayoutFrame', + '_cockpitLayoutFrame', + ]) { if (this[field] !== null) cancelAnimationFrame(this[field]); this[field] = null; } clearTimeout(this._adaptivePanelSettleTimer); clearTimeout(this._cockpitLayoutTimer); this._adaptivePanelSettleTimer = this._cockpitLayoutTimer = null; - for (const field of ['_commandDockTrayObserver', '_leftStackResizeObserver', '_leftStackMutationObserver', '_rightStackResizeObserver', '_rightStackMutationObserver']) { - this[field]?.disconnect(); this[field] = null; - } - for (const field of ['_leftStackHudTransitionHandler', '_rightStackHudTransitionHandler']) { - if (this[field]) document.getElementById('intel-hud')?.removeEventListener('transitionend', this[field]); + for (const field of [ + '_commandDockTrayObserver', + '_leftStackResizeObserver', + '_leftStackMutationObserver', + '_rightStackResizeObserver', + '_rightStackMutationObserver', + ]) { + this[field]?.disconnect(); this[field] = null; } - if (this._leftStackCockpitModeHandler) window.removeEventListener('gev:cockpit-mode-changed', this._leftStackCockpitModeHandler); + for (const field of [ + '_leftStackHudTransitionHandler', + '_rightStackHudTransitionHandler', + ]) { + if (this[field]) + document + .getElementById('intel-hud') + ?.removeEventListener('transitionend', this[field]); + this[field] = null; + } + if (this._leftStackCockpitModeHandler) + window.removeEventListener( + 'gev:cockpit-mode-changed', + this._leftStackCockpitModeHandler, + ); this._leftStackCockpitModeHandler = null; } } diff --git a/src/ui/panelPositionControls.js b/src/ui/panelPositionControls.js index df15fa99b..7b4468358 100644 --- a/src/ui/panelPositionControls.js +++ b/src/ui/panelPositionControls.js @@ -4,7 +4,12 @@ const PANEL_POSITION_STORAGE_VERSION = 'v8'; const PANEL_Z_BASE = 100; const PANEL_Z_MAX = 139; export class PanelPositionControls { - constructor({ syncPanelCollapseButton, layoutRightPanels, syncCctvPanelViewport, showToast }) { + constructor({ + syncPanelCollapseButton, + layoutRightPanels, + syncCctvPanelViewport, + showToast, + }) { this._syncPanelCollapseButton = syncPanelCollapseButton; this._layoutRightPanels = layoutRightPanels; this._syncCctvPanelViewport = syncCctvPanelViewport; @@ -17,7 +22,9 @@ export class PanelPositionControls { this.destroyed = false; } listen(target, type, callback) { - const listener = (event) => { if (!this.destroyed) callback(event); }; + const listener = (event) => { + if (!this.destroyed) callback(event); + }; target.addEventListener(type, listener); this.removers.push(() => target.removeEventListener(type, listener)); } @@ -35,10 +42,13 @@ export class PanelPositionControls { const marker = `godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.layoutResetNotified`; if (localStorage.getItem(marker)) return; localStorage.setItem(marker, '1'); - const hadOldPositions = Object.keys(localStorage) - .some((key) => key.startsWith('godsEyeView.v6.panelPos.')); + const hadOldPositions = Object.keys(localStorage).some((key) => + key.startsWith('godsEyeView.v6.panelPos.'), + ); if (hadOldPositions) { - this._showToast('Panel layout updated — positions reset to new defaults'); + this._showToast( + 'Panel layout updated — positions reset to new defaults', + ); } } catch { // storage unavailable @@ -63,7 +73,9 @@ export class PanelPositionControls { // full row set a frame or two later, so the restore-time clamp used a stale (shorter) height and // the panel could still hang off the bottom (audit U2). Re-clamp on every size change. if (this._ppToggles && typeof ResizeObserver !== 'undefined') { - this._draggableResizeObserver = new ResizeObserver(() => this._reclampDraggablePanels()); + this._draggableResizeObserver = new ResizeObserver(() => + this._reclampDraggablePanels(), + ); this._draggableResizeObserver.observe(this._ppToggles); } } @@ -105,7 +117,10 @@ export class PanelPositionControls { _savePanelCollapsedState(panelId, collapsed) { try { - localStorage.setItem(this._panelCollapseStorageKey(panelId), collapsed ? '1' : '0'); + localStorage.setItem( + this._panelCollapseStorageKey(panelId), + collapsed ? '1' : '0', + ); } catch { // storage unavailable } @@ -124,10 +139,15 @@ export class PanelPositionControls { const raw = localStorage.getItem(this._panelStorageKey(panelId)); if (!raw) return; const pos = JSON.parse(raw); - if (!pos || typeof pos.left !== 'number' || typeof pos.top !== 'number') return; + if (!pos || typeof pos.left !== 'number' || typeof pos.top !== 'number') + return; // Clamp to the viewport: a position saved at one window size would otherwise land off-screen at // another (audit U2 — observed a panel at x:-192). The drag handler clamps; restore must too. - const { left, top } = this._clampToViewport(Math.round(pos.left), Math.round(pos.top), panelEl); + const { left, top } = this._clampToViewport( + Math.round(pos.left), + Math.round(pos.top), + panelEl, + ); panelEl.style.left = `${left}px`; panelEl.style.top = `${top}px`; panelEl.style.right = 'auto'; @@ -153,10 +173,13 @@ export class PanelPositionControls { _savePanelPosition(panelId, panelEl) { const rect = panelEl.getBoundingClientRect(); try { - localStorage.setItem(this._panelStorageKey(panelId), JSON.stringify({ - left: Math.round(rect.left), - top: Math.round(rect.top), - })); + localStorage.setItem( + this._panelStorageKey(panelId), + JSON.stringify({ + left: Math.round(rect.left), + top: Math.round(rect.top), + }), + ); } catch { // storage unavailable } @@ -187,7 +210,12 @@ export class PanelPositionControls { this.listen(handleEl, 'pointerdown', (event) => { if (event.button !== 0) return; if (event.target.closest('.panel-collapse-btn')) return; - if (event.target.closest('input, select, option, button:not(.panel-collapse-btn)')) return; + if ( + event.target.closest( + 'input, select, option, button:not(.panel-collapse-btn)', + ) + ) + return; this._cancelDrag?.(); event.preventDefault(); diff --git a/src/ui/recordingControls.js b/src/ui/recordingControls.js index 9efbf8189..eb50c9a1b 100644 --- a/src/ui/recordingControls.js +++ b/src/ui/recordingControls.js @@ -4,7 +4,11 @@ export class RecordingControls { this._syncShareState = syncShareState; this.destroyed = false; this._recordingMode = false; - this._recordingConfig = { hidePanels: true, hudMode: 'minimal', safeFrame: '16:9' }; + this._recordingConfig = { + hidePanels: true, + hudMode: 'minimal', + safeFrame: '16:9', + }; this._preRecordingHudState = null; this._safeFrameOverlay = document.getElementById('safe-frame-overlay'); this._safeFrameBox = document.getElementById('safe-frame-box'); @@ -15,21 +19,34 @@ export class RecordingControls { _initRecordingOverlay() { if (this.destroyed) return; if (!this._safeFrameOverlay || !this._safeFrameBox) return; - this._safeFrameOverlay.classList.remove('active', 'ratio-9-16', 'ratio-16-9'); + this._safeFrameOverlay.classList.remove( + 'active', + 'ratio-9-16', + 'ratio-16-9', + ); } setRecordingMode(enabled, options = {}) { if (this.destroyed) return; - const { hidePanels = true, hudMode = 'minimal', safeFrame = '16:9' } = options; + const { + hidePanels = true, + hudMode = 'minimal', + safeFrame = '16:9', + } = options; this._recordingMode = !!enabled; this._recordingConfig = { hidePanels, hudMode, safeFrame }; - document.body.classList.toggle('recording-mode', this._recordingMode && hidePanels); + document.body.classList.toggle( + 'recording-mode', + this._recordingMode && hidePanels, + ); if (this._safeFrameOverlay) { this._safeFrameOverlay.classList.remove('ratio-9-16', 'ratio-16-9'); this._safeFrameOverlay.classList.toggle('active', this._recordingMode); - this._safeFrameOverlay.classList.add(safeFrame === '9:16' ? 'ratio-9-16' : 'ratio-16-9'); + this._safeFrameOverlay.classList.add( + safeFrame === '9:16' ? 'ratio-9-16' : 'ratio-16-9', + ); } if (this._recordingMode) { @@ -46,7 +63,8 @@ export class RecordingControls { } else if (hudMode === 'full' || hudMode === 'minimal') { this.hud.setMode('on'); this.hud.setVariant(hudMode === 'minimal' ? 'minimal' : 'tactical'); - if (this._hudLayoutSelect) this._hudLayoutSelect.value = this.hud.getVariant(); + if (this._hudLayoutSelect) + this._hudLayoutSelect.value = this.hud.getVariant(); } else { this.hud.setMode('auto'); } @@ -55,11 +73,16 @@ export class RecordingControls { this._preRecordingHudState = null; if (saved) { this.hud.setVariant(saved.variant); - if (this._hudLayoutSelect) this._hudLayoutSelect.value = this.hud.getVariant(); + if (this._hudLayoutSelect) + this._hudLayoutSelect.value = this.hud.getVariant(); } this.hud.setMode(saved ? saved.mode : 'auto'); if (this._safeFrameOverlay) { - this._safeFrameOverlay.classList.remove('active', 'ratio-9-16', 'ratio-16-9'); + this._safeFrameOverlay.classList.remove( + 'active', + 'ratio-9-16', + 'ratio-16-9', + ); } } this._hudBtn.classList.toggle('active', this.hud.visible); diff --git a/src/ui/shellElements.js b/src/ui/shellElements.js index db79cf301..f87d5304f 100644 --- a/src/ui/shellElements.js +++ b/src/ui/shellElements.js @@ -18,14 +18,20 @@ export function readShellElements(document = globalThis.document) { _hudLayoutRow: document.getElementById('hud-layout-row'), _hudLayoutSelect: document.getElementById('hud-layout-select'), _detectionSliderRow: document.getElementById('detection-slider-row'), - _detectionDensitySlider: document.getElementById('detection-density-slider'), + _detectionDensitySlider: document.getElementById( + 'detection-density-slider', + ), _detectionDensityValue: document.getElementById('detection-density-value'), - _detectionAllocationRow: document.getElementById('detection-allocation-row'), + _detectionAllocationRow: document.getElementById( + 'detection-allocation-row', + ), _detectionFadeRow: document.getElementById('detection-fade-row'), _detectionFadeSlider: document.getElementById('detection-fade-slider'), _detectionFadeValue: document.getElementById('detection-fade-value'), _detectionOpacityRow: document.getElementById('detection-opacity-row'), - _detectionOpacitySlider: document.getElementById('detection-opacity-slider'), + _detectionOpacitySlider: document.getElementById( + 'detection-opacity-slider', + ), _detectionOpacityValue: document.getElementById('detection-opacity-value'), _celestialBtn: document.getElementById('celestial-toggle'), _scopeBtn: document.getElementById('scope-toggle'), @@ -42,17 +48,39 @@ export function readShellElements(document = globalThis.document) { _contextRadioDock: document.getElementById('context-radio-dock'), _contextRadioToggleBtn: document.getElementById('context-radio-toggle-btn'), _contextRadioMini: document.getElementById('context-radio-mini'), - _contextRadioMiniEnableBtn: document.getElementById('context-radio-mini-enable-btn'), - _contextRadioDetailsBtn: document.getElementById('context-radio-details-btn'), - _contextRadioMiniCloseBtn: document.getElementById('context-radio-mini-close-btn'), - _contextRadioMiniStation: document.getElementById('context-radio-mini-station'), - _contextRadioMiniPrevBtn: document.getElementById('context-radio-mini-prev-btn'), - _contextRadioMiniPlayBtn: document.getElementById('context-radio-mini-play-btn'), - _contextRadioMiniNextBtn: document.getElementById('context-radio-mini-next-btn'), - _contextRadioMiniVolume: document.getElementById('context-radio-mini-volume'), - _contextRadioMiniVolumeValue: document.getElementById('context-radio-mini-volume-value'), - _cockpitUtilityControls: document.getElementById('cockpit-utility-controls'), - _cockpitDisplayToggleBtn: document.getElementById('cockpit-display-toggle-btn'), + _contextRadioMiniEnableBtn: document.getElementById( + 'context-radio-mini-enable-btn', + ), + _contextRadioDetailsBtn: document.getElementById( + 'context-radio-details-btn', + ), + _contextRadioMiniCloseBtn: document.getElementById( + 'context-radio-mini-close-btn', + ), + _contextRadioMiniStation: document.getElementById( + 'context-radio-mini-station', + ), + _contextRadioMiniPrevBtn: document.getElementById( + 'context-radio-mini-prev-btn', + ), + _contextRadioMiniPlayBtn: document.getElementById( + 'context-radio-mini-play-btn', + ), + _contextRadioMiniNextBtn: document.getElementById( + 'context-radio-mini-next-btn', + ), + _contextRadioMiniVolume: document.getElementById( + 'context-radio-mini-volume', + ), + _contextRadioMiniVolumeValue: document.getElementById( + 'context-radio-mini-volume-value', + ), + _cockpitUtilityControls: document.getElementById( + 'cockpit-utility-controls', + ), + _cockpitDisplayToggleBtn: document.getElementById( + 'cockpit-display-toggle-btn', + ), _cockpitDisplayPanel: document.getElementById('cockpit-display-panel'), _cockpitRadioToggleBtn: document.getElementById('cockpit-radio-toggle-btn'), _cockpitRadioPanel: document.getElementById('cockpit-radio-panel'), @@ -62,7 +90,9 @@ export function readShellElements(document = globalThis.document) { _cockpitRadioPlayBtn: document.getElementById('cockpit-radio-play-btn'), _cockpitRadioNextBtn: document.getElementById('cockpit-radio-next-btn'), _cockpitRadioVolume: document.getElementById('cockpit-radio-volume'), - _cockpitRadioVolumeValue: document.getElementById('cockpit-radio-volume-value'), + _cockpitRadioVolumeValue: document.getElementById( + 'cockpit-radio-volume-value', + ), _radioLayerState: document.getElementById('radio-layer-state'), _radioEnableBtn: document.getElementById('radio-enable-btn'), _radioFilter: document.getElementById('radio-filter'), @@ -83,12 +113,18 @@ export function readShellElements(document = globalThis.document) { _radioVolumeValue: document.getElementById('radio-volume-value'), _radioPlaybackState: document.getElementById('radio-playback-state'), _radioStationHomepage: document.getElementById('radio-station-homepage'), - _globalContextFlightsBtn: document.getElementById('global-context-flights-btn'), - _globalContextMissionsBtn: document.getElementById('global-context-missions-btn'), + _globalContextFlightsBtn: document.getElementById( + 'global-context-flights-btn', + ), + _globalContextMissionsBtn: document.getElementById( + 'global-context-missions-btn', + ), _contextModeStandby: document.getElementById('context-mode-standby'), _contextFlightsView: document.getElementById('context-flights-view'), _contextMissionsView: document.getElementById('context-missions-view'), - _installationsSearchBtn: document.getElementById('installations-search-btn'), + _installationsSearchBtn: document.getElementById( + 'installations-search-btn', + ), _leftPanelStack: document.getElementById('left-panel-stack'), _cctvEnableBtn: document.getElementById('cctv-enable-btn'), _cctvNearestBtn: document.getElementById('cctv-nearest-btn'), diff --git a/src/ui/shellFeedback.js b/src/ui/shellFeedback.js index cb36771dd..e7a37122b 100644 --- a/src/ui/shellFeedback.js +++ b/src/ui/shellFeedback.js @@ -1,5 +1,13 @@ /** Own loading notices, traffic status, toast timing and visibility cleanup. */ -import { aggregateLayerLoading, createGlobalStatusNotice, createLoadingFeedbackState, createTrafficSyncFeedbackState, presentGlobalLoadingStatus, reduceLoadingFeedback, reduceTrafficSyncFeedback } from '../loadingFeedback.js'; +import { + aggregateLayerLoading, + createGlobalStatusNotice, + createLoadingFeedbackState, + createTrafficSyncFeedbackState, + presentGlobalLoadingStatus, + reduceLoadingFeedback, + reduceTrafficSyncFeedback, +} from '../loadingFeedback.js'; import { setSplitFlapText, disposeSplitFlap } from '../splitFlap.js'; export class ShellFeedback { constructor({ readLayers }) { @@ -9,15 +17,24 @@ export class ShellFeedback { this._trafficSyncFeedbackState = createTrafficSyncFeedbackState(); this._loadingFeedbackEvent = null; this._globalStatusNotice = null; - this._loadingFeedbackTicker = this._trafficChipTicker = this._toastTimer = null; + this._loadingFeedbackTicker = + this._trafficChipTicker = + this._toastTimer = + null; this._loadingVisibilityHandler = null; this._lastLoadingFeedbackUpdateAt = 0; - this._globalLoadingStatus = document.getElementById('global-loading-status'); + this._globalLoadingStatus = document.getElementById( + 'global-loading-status', + ); this._globalLoadingLabel = document.getElementById('global-loading-label'); - this._globalLoadingDetail = document.getElementById('global-loading-detail'); + this._globalLoadingDetail = document.getElementById( + 'global-loading-detail', + ); this._trafficSyncChip = document.getElementById('traffic-sync-chip'); this._trafficSyncLabel = document.getElementById('traffic-sync-label'); - this._trafficSyncProgress = document.getElementById('traffic-sync-progress'); + this._trafficSyncProgress = document.getElementById( + 'traffic-sync-progress', + ); this._toast = document.getElementById('toast'); } observeVisibility() { @@ -26,23 +43,38 @@ export class ShellFeedback { if (!document.hidden) this._updateGlobalLoadingFeedback(); else this._stopLoadingFeedbackTicker(); }; - document.addEventListener('visibilitychange', this._loadingVisibilityHandler); + document.addEventListener( + 'visibilitychange', + this._loadingVisibilityHandler, + ); } _updateTrafficSyncChip(forceShow = false, now = performance.now()) { if (this.destroyed) return; - if (!this._trafficSyncChip || !this._trafficSyncLabel || !this._trafficSyncProgress) return; + if ( + !this._trafficSyncChip || + !this._trafficSyncLabel || + !this._trafficSyncProgress + ) + return; const layers = this.readLayers(); - const traffic = Array.isArray(layers) ? layers.find((layer) => layer.id === 'traffic') : null; + const traffic = Array.isArray(layers) + ? layers.find((layer) => layer.id === 'traffic') + : null; this._trafficSyncFeedbackState = reduceTrafficSyncFeedback( this._trafficSyncFeedbackState, - { enabled: traffic?.enabled === true, stats: traffic?.stats || {}, forceShow }, + { + enabled: traffic?.enabled === true, + stats: traffic?.stats || {}, + forceShow, + }, now, ); const presentation = this._trafficSyncFeedbackState; // setSplitFlapText carries the same unchanged-text guard internally, and // the flap keeps textContent equal to the settled label throughout, so // this stays a no-op on the repeat ticks exactly as it did before. - if (presentation.label) setSplitFlapText(this._trafficSyncLabel, presentation.label); + if (presentation.label) + setSplitFlapText(this._trafficSyncLabel, presentation.label); // Written on every change INCLUDING the empty settled value — the reducer // clears the progress number once the sync lands, and a truthiness guard // here would strand the last "..." beside the settled label. @@ -69,9 +101,11 @@ export class ShellFeedback { summary, now, ); - if (this._globalStatusNotice?.persistent !== true - && Number.isFinite(this._globalStatusNotice?.hideAt) - && now >= this._globalStatusNotice.hideAt) { + if ( + this._globalStatusNotice?.persistent !== true && + Number.isFinite(this._globalStatusNotice?.hideAt) && + now >= this._globalStatusNotice.hideAt + ) { this._globalStatusNotice = null; } // Loading phases and universal notices both have time-driven transitions. @@ -129,7 +163,9 @@ export class ShellFeedback { const now = performance.now(); this._lastLoadingFeedbackUpdateAt = now; this._updateGlobalLoadingFeedback(now); - const noticeNeedsTicker = Number.isFinite(this._globalStatusNotice?.hideAt); + const noticeNeedsTicker = Number.isFinite( + this._globalStatusNotice?.hideAt, + ); if (this._loadingFeedbackState?.phase === 'idle' && !noticeNeedsTicker) { this._stopLoadingFeedbackTicker(); } @@ -158,7 +194,11 @@ export class ShellFeedback { clearInterval(this._trafficChipTicker); clearTimeout(this._toastTimer); this._trafficChipTicker = this._toastTimer = null; - if (this._loadingVisibilityHandler) document.removeEventListener('visibilitychange', this._loadingVisibilityHandler); + if (this._loadingVisibilityHandler) + document.removeEventListener( + 'visibilitychange', + this._loadingVisibilityHandler, + ); this._loadingVisibilityHandler = null; disposeSplitFlap(this._globalLoadingLabel); disposeSplitFlap(this._trafficSyncLabel); diff --git a/src/ui/shellLifecycle.test.mjs b/src/ui/shellLifecycle.test.mjs index 8d7f508a3..eb2dbb6bc 100644 --- a/src/ui/shellLifecycle.test.mjs +++ b/src/ui/shellLifecycle.test.mjs @@ -7,133 +7,258 @@ import { ShellFeedback } from './shellFeedback.js'; import { RecordingControls } from './recordingControls.js'; function fixture() { - const saved = Object.fromEntries(['document', 'window', 'localStorage', 'requestAnimationFrame', 'cancelAnimationFrame', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'].map((key) => [key, globalThis[key]])); - const frames = new Map(), timers = new Map(); + const saved = Object.fromEntries( + [ + 'document', + 'window', + 'localStorage', + 'requestAnimationFrame', + 'cancelAnimationFrame', + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + ].map((key) => [key, globalThis[key]]), + ); + const frames = new Map(), + timers = new Map(); let next = 0; const element = () => { const classes = new Set(); return Object.assign(new EventTarget(), { - style: {}, dataset: {}, hidden: false, textContent: '', + style: {}, + dataset: {}, + hidden: false, + textContent: '', classList: { add: (...names) => names.forEach((name) => classes.add(name)), remove: (...names) => names.forEach((name) => classes.delete(name)), contains: (name) => classes.has(name), - toggle: (name, active) => active ? classes.add(name) : classes.delete(name), + toggle: (name, active) => + active ? classes.add(name) : classes.delete(name), }, - querySelector: () => null, querySelectorAll: () => [], closest: () => null, - getBoundingClientRect: () => ({ left: 20, top: 30, width: 100, height: 80 }), + querySelector: () => null, + querySelectorAll: () => [], + closest: () => null, + getBoundingClientRect: () => ({ + left: 20, + top: 30, + width: 100, + height: 80, + }), }); }; - const nodes = new Map(['left-panel-stack', 'right-context-rail', 'pp-toggles', 'toast', 'global-loading-status', 'global-loading-detail', 'safe-frame-overlay', 'safe-frame-box', 'hud-toggle', 'hud-layout-select'].map((id) => [id, element()])); - globalThis.document = Object.assign(new EventTarget(), { hidden: false, getElementById: (id) => nodes.get(id) || null, querySelectorAll: () => [], body: element() }); - globalThis.window = Object.assign(new EventTarget(), { innerWidth: 600, innerHeight: 400 }); + const nodes = new Map( + [ + 'left-panel-stack', + 'right-context-rail', + 'pp-toggles', + 'toast', + 'global-loading-status', + 'global-loading-detail', + 'safe-frame-overlay', + 'safe-frame-box', + 'hud-toggle', + 'hud-layout-select', + ].map((id) => [id, element()]), + ); + globalThis.document = Object.assign(new EventTarget(), { + hidden: false, + getElementById: (id) => nodes.get(id) || null, + querySelectorAll: () => [], + body: element(), + }); + globalThis.window = Object.assign(new EventTarget(), { + innerWidth: 600, + innerHeight: 400, + }); const writes = []; - globalThis.localStorage = { setItem: (...args) => writes.push(args), getItem: () => null }; - globalThis.requestAnimationFrame = (callback) => { frames.set(++next, callback); return next; }; + globalThis.localStorage = { + setItem: (...args) => writes.push(args), + getItem: () => null, + }; + globalThis.requestAnimationFrame = (callback) => { + frames.set(++next, callback); + return next; + }; globalThis.cancelAnimationFrame = (id) => frames.delete(id); - globalThis.setTimeout = globalThis.setInterval = (callback) => { timers.set(++next, callback); return next; }; - globalThis.clearTimeout = globalThis.clearInterval = (id) => timers.delete(id); - return { frames, timers, nodes, element, writes, restore() { Object.assign(globalThis, saved); } }; + globalThis.setTimeout = globalThis.setInterval = (callback) => { + timers.set(++next, callback); + return next; + }; + globalThis.clearTimeout = globalThis.clearInterval = (id) => + timers.delete(id); + return { + frames, + timers, + nodes, + element, + writes, + restore() { + Object.assign(globalThis, saved); + }, + }; } test('UI lifetime revokes listeners and nested presentation work synchronously', () => { const f = fixture(); try { - const owner = new UiLifetime(), target = new EventTarget(); + const owner = new UiLifetime(), + target = new EventTarget(); let calls = 0; owner.listen(target, 'change', () => calls++); - owner.frame(() => { calls++; owner.frame(() => calls++); }); + owner.frame(() => { + calls++; + owner.frame(() => calls++); + }); owner.timeout(() => calls++, 100); const retainedFrame = [...f.frames.values()][0]; - owner.destroy(); owner.destroy(); + owner.destroy(); + owner.destroy(); target.dispatchEvent(new Event('change')); retainedFrame(); - owner.frame(() => calls++); owner.timeout(() => calls++, 1); + owner.frame(() => calls++); + owner.timeout(() => calls++, 1); assert.equal(calls, 0); assert.equal(f.frames.size + f.timers.size + owner.removers.size, 0); - } finally { f.restore(); } + } finally { + f.restore(); + } }); test('one-time UI listeners and cancelled notice timers release their registrations', () => { const f = fixture(); try { - const owner = new UiLifetime(), target = new EventTarget(); + const owner = new UiLifetime(), + target = new EventTarget(); let calls = 0; owner.listen(target, 'change', () => calls++, { once: true }); - target.dispatchEvent(new Event('change')); target.dispatchEvent(new Event('change')); + target.dispatchEvent(new Event('change')); + target.dispatchEvent(new Event('change')); owner.cancelTimeout(owner.timeout(() => calls++, 100)); assert.equal(calls, 1); assert.equal(owner.removers.size + owner.timers.size + f.timers.size, 0); owner.destroy(); - } finally { f.restore(); } + } finally { + f.restore(); + } }); test('panel layout coalesces each rail and cannot rearm after destruction', () => { const f = fixture(); try { let layouts = 0; - const owner = new PanelLayoutController({ readHud: () => ({}), scheduleCockpitLayout() {}, syncPanelCollapseButton() {}, readDisplayScrollTop: () => 0 }); + const owner = new PanelLayoutController({ + readHud: () => ({}), + scheduleCockpitLayout() {}, + syncPanelCollapseButton() {}, + readDisplayScrollTop: () => 0, + }); owner._syncLeftPanelAdaptiveLayout = () => layouts++; - owner._scheduleLeftPanelLayout(); owner._scheduleLeftPanelLayout(); + owner._scheduleLeftPanelLayout(); + owner._scheduleLeftPanelLayout(); assert.equal(f.frames.size, 1); owner._scheduleAdaptivePanelLayout({ settle: true }); assert.equal(f.frames.size, 2); assert.equal(f.timers.size, 1); const late = [...f.frames.values(), ...f.timers.values()]; - owner.destroy(); owner.destroy(); + owner.destroy(); + owner.destroy(); late.forEach((callback) => callback()); owner._scheduleAdaptivePanelLayout({ settle: true }); assert.equal(layouts, 0); assert.equal(f.frames.size + f.timers.size, 0); - } finally { f.restore(); } + } finally { + f.restore(); + } }); test('panel disposal cancels a live drag without saving or accepting later pointer movement', () => { const f = fixture(); try { - const owner = new PanelPositionControls({ syncPanelCollapseButton() {}, layoutRightPanels() {}, syncCctvPanelViewport() {}, showToast() {} }); - const panel = f.element(), handle = f.element(); + const owner = new PanelPositionControls({ + syncPanelCollapseButton() {}, + layoutRightPanels() {}, + syncCctvPanelViewport() {}, + showToast() {}, + }); + const panel = f.element(), + handle = f.element(); owner._makePanelDraggable('sample', panel, handle); - handle.dispatchEvent(Object.assign(new Event('pointerdown', { cancelable: true }), { button: 0, clientX: 30, clientY: 40 })); + handle.dispatchEvent( + Object.assign(new Event('pointerdown', { cancelable: true }), { + button: 0, + clientX: 30, + clientY: 40, + }), + ); assert.equal(panel.classList.contains('panel-dragging'), true); owner.destroy(); const before = { ...panel.style }; - window.dispatchEvent(Object.assign(new Event('pointermove'), { clientX: 500, clientY: 300 })); + window.dispatchEvent( + Object.assign(new Event('pointermove'), { clientX: 500, clientY: 300 }), + ); window.dispatchEvent(new Event('pointerup')); assert.deepEqual(panel.style, before); assert.equal(panel.classList.contains('panel-dragging'), false); assert.equal(owner.removers.length, 0); assert.deepEqual(f.writes, []); - } finally { f.restore(); } + } finally { + f.restore(); + } }); test('feedback disposal clears toast and polling work and rejects retained notices', () => { const f = fixture(); try { const owner = new ShellFeedback({ readLayers: () => [] }); - owner.observeVisibility(); owner._startTrafficChipTicker(); owner._showToast('Saved'); + owner.observeVisibility(); + owner._startTrafficChipTicker(); + owner._showToast('Saved'); assert.equal(f.timers.size, 2); owner.destroy(); - owner._showToast('Late'); owner._showGlobalStatusNotice('Late'); owner._startTrafficChipTicker(); + owner._showToast('Late'); + owner._showGlobalStatusNotice('Late'); + owner._startTrafficChipTicker(); document.dispatchEvent(new Event('visibilitychange')); assert.equal(f.timers.size, 0); assert.equal(f.nodes.get('toast').textContent, 'Saved'); assert.equal(f.nodes.get('toast').classList.contains('visible'), false); assert.equal(f.nodes.get('global-loading-status').hidden, true); - } finally { f.restore(); } + } finally { + f.restore(); + } }); test('recording restores the original HUD after repeated entry and revokes retained controls', () => { const f = fixture(); try { - let mode = 'auto', variant = 'tactical'; + let mode = 'auto', + variant = 'tactical'; const owner = new RecordingControls({ syncShareState() {} }); - owner.hud = { getMode: () => mode, getVariant: () => variant, setMode: (value) => { mode = value; }, setVariant: (value) => { variant = value; }, visible: true }; + owner.hud = { + getMode: () => mode, + getVariant: () => variant, + setMode: (value) => { + mode = value; + }, + setVariant: (value) => { + variant = value; + }, + visible: true, + }; owner.setRecordingMode(true, { hudMode: 'minimal', safeFrame: '9:16' }); owner.setRecordingMode(true, { hudMode: 'off' }); - owner.destroy(); owner.setRecordingMode(true); - assert.equal(mode, 'auto'); assert.equal(variant, 'tactical'); + owner.destroy(); + owner.setRecordingMode(true); + assert.equal(mode, 'auto'); + assert.equal(variant, 'tactical'); assert.equal(document.body.classList.contains('recording-mode'), false); - assert.equal(f.nodes.get('safe-frame-overlay').classList.contains('active'), false); - } finally { f.restore(); } + assert.equal( + f.nodes.get('safe-frame-overlay').classList.contains('active'), + false, + ); + } finally { + f.restore(); + } }); diff --git a/src/ui/styles/cctv.css b/src/ui/styles/cctv.css index d786be7d1..08b620c4e 100644 --- a/src/ui/styles/cctv.css +++ b/src/ui/styles/cctv.css @@ -14,7 +14,11 @@ #cctv-panel .panel-glow { position: absolute; inset: -18px; - background: radial-gradient(ellipse at center, rgba(0, 212, 255, 0.16) 0%, transparent 65%); + background: radial-gradient( + ellipse at center, + rgba(0, 212, 255, 0.16) 0%, + transparent 65% + ); filter: blur(16px); opacity: 0.25; pointer-events: none; @@ -51,7 +55,11 @@ } .cctv-panel-inner::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, rgba(0, 212, 255, 0.6), rgba(0, 212, 255, 0.28)); + background: linear-gradient( + 180deg, + rgba(0, 212, 255, 0.6), + rgba(0, 212, 255, 0.28) + ); border: 1px solid rgba(0, 212, 255, 0.4); border-radius: 999px; } @@ -99,7 +107,9 @@ border-radius: 7px; padding: 4px 7px; cursor: text; - transition: border-color 0.15s ease, background 0.15s ease; + transition: + border-color 0.15s ease, + background 0.15s ease; } .cctv-cal-value:hover:not(:disabled) { @@ -212,7 +222,11 @@ overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.12); background: - radial-gradient(circle at 20% 20%, rgba(0, 244, 255, 0.09), transparent 44%), + radial-gradient( + circle at 20% 20%, + rgba(0, 244, 255, 0.09), + transparent 44% + ), linear-gradient(180deg, rgba(8, 14, 19, 0.88), rgba(4, 7, 11, 0.96)); min-height: 150px; /* .cctv-panel-inner is a scrolling column flex container, so the default @@ -335,4 +349,3 @@ color: #a8f7ff; text-shadow: 0 0 10px rgba(0, 212, 255, 0.18); } - diff --git a/src/ui/styles/cockpit.css b/src/ui/styles/cockpit.css index e7e3f5595..c5d4280b1 100644 --- a/src/ui/styles/cockpit.css +++ b/src/ui/styles/cockpit.css @@ -19,13 +19,20 @@ cursor: pointer; } -#cockpit-entry[hidden], #cockpit-hud[hidden] { display: none; } -#cockpit-entry:hover, #cockpit-entry:focus-visible { +#cockpit-entry[hidden], +#cockpit-hud[hidden] { + display: none; +} +#cockpit-entry:hover, +#cockpit-entry:focus-visible { border-color: #00dcff; color: #fff; outline: none; } -#cockpit-entry .material-symbols-outlined { color: #00dcff; font-size: 18px; } +#cockpit-entry .material-symbols-outlined { + color: #00dcff; + font-size: 18px; +} #cockpit-entry kbd { padding: 3px 5px; border: 1px solid rgba(220, 238, 255, 0.25); @@ -46,10 +53,14 @@ pointer-events: none; color: #f4fbff; font-family: var(--font-mono); - text-shadow: 0 0 10px color-mix(in srgb, var(--cockpit-accent) 60%, transparent), 0 1px 2px #000; + text-shadow: + 0 0 10px color-mix(in srgb, var(--cockpit-accent) 60%, transparent), + 0 1px 2px #000; perspective: 920px; } -#cockpit-hud[data-layer="military"] { --cockpit-accent: #ffb800; } +#cockpit-hud[data-layer='military'] { + --cockpit-accent: #ffb800; +} .cockpit-visor-shell { position: absolute; inset: clamp(62px, 9vh, 90px) clamp(18px, 3vw, 48px) clamp(38px, 5vh, 56px); @@ -69,7 +80,8 @@ content: ''; position: absolute; inset: 18px 6% 0; - border-top: 1px solid color-mix(in srgb, var(--cockpit-accent) 62%, transparent); + border-top: 1px solid + color-mix(in srgb, var(--cockpit-accent) 62%, transparent); border-radius: 50%; opacity: 0.72; } @@ -80,10 +92,8 @@ color: rgba(244, 251, 255, 0.46); font-size: 8px; letter-spacing: 0.1em; - transform: - translateX(calc(-50% + (var(--slot) * 74px))) - translateY(calc(var(--depth) * 9px)) - rotateZ(calc(var(--slot) * 4deg)) + transform: translateX(calc(-50% + (var(--slot) * 74px))) + translateY(calc(var(--depth) * 9px)) rotateZ(calc(var(--slot) * 4deg)) rotateY(calc(var(--slot) * -5deg)); } .cockpit-roll-arc span::after { @@ -94,7 +104,10 @@ margin: 5px auto 0; background: color-mix(in srgb, var(--cockpit-accent) 62%, transparent); } -.cockpit-roll-arc span.active { color: #fff; font-size: 10px; } +.cockpit-roll-arc span.active { + color: #fff; + font-size: 10px; +} .cockpit-pitch-rail { position: absolute; top: 25%; @@ -117,7 +130,8 @@ text-align: left; transform: rotateY(-24deg) translateZ(-18px); } -body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .cockpit-pitch-rail-left { +body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) + .cockpit-pitch-rail-left { opacity: 0; transition: opacity 0.25s; } @@ -126,15 +140,25 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c align-items: center; gap: 7px; } -.cockpit-pitch-rail-left span { justify-content: flex-end; } +.cockpit-pitch-rail-left span { + justify-content: flex-end; +} .cockpit-pitch-rail span::after { content: ''; width: 24px; - border-top: 1px solid color-mix(in srgb, var(--cockpit-accent) 46%, transparent); + border-top: 1px solid + color-mix(in srgb, var(--cockpit-accent) 46%, transparent); +} +.cockpit-pitch-rail-right span::after { + order: -1; +} +.cockpit-pitch-rail span.active { + color: var(--cockpit-accent); +} +.cockpit-pitch-rail span.active::after { + width: 42px; + border-color: var(--cockpit-accent); } -.cockpit-pitch-rail-right span::after { order: -1; } -.cockpit-pitch-rail span.active { color: var(--cockpit-accent); } -.cockpit-pitch-rail span.active::after { width: 42px; border-color: var(--cockpit-accent); } .cockpit-horizon-guide { position: absolute; top: 46%; @@ -147,10 +171,21 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c transform: translate(-50%, -50%) translateZ(22px); color: color-mix(in srgb, var(--cockpit-accent) 72%, white); } -.cockpit-horizon-guide span { border-top: 1px solid currentColor; } -.cockpit-horizon-guide span:first-child { transform: rotateZ(2deg); transform-origin: right; } -.cockpit-horizon-guide span:last-child { transform: rotateZ(-2deg); transform-origin: left; } -.cockpit-horizon-guide small { font-size: 7px; letter-spacing: 0.16em; } +.cockpit-horizon-guide span { + border-top: 1px solid currentColor; +} +.cockpit-horizon-guide span:first-child { + transform: rotateZ(2deg); + transform-origin: right; +} +.cockpit-horizon-guide span:last-child { + transform: rotateZ(-2deg); + transform-origin: left; +} +.cockpit-horizon-guide small { + font-size: 7px; + letter-spacing: 0.16em; +} .cockpit-route-direction { --route-angle: 0deg; position: absolute; @@ -167,9 +202,13 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c opacity: 0.84; transform: translate(-50%, -50%) translateZ(24px); transition: opacity 180ms ease; - filter: drop-shadow(0 0 8px color-mix(in srgb, var(--cockpit-accent) 34%, transparent)); + filter: drop-shadow( + 0 0 8px color-mix(in srgb, var(--cockpit-accent) 34%, transparent) + ); +} +.cockpit-route-direction[hidden] { + display: none; } -.cockpit-route-direction[hidden] { display: none; } .cockpit-route-direction .cockpit-route-chevron { display: block; width: clamp(56px, 6vw, 78px); @@ -180,12 +219,12 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c stroke: color-mix(in srgb, var(--cockpit-accent) 58%, #d8f3ff); stroke-width: 0.7px; stroke-linejoin: round; - filter: - drop-shadow(0 0 4px rgba(213, 242, 255, 0.68)) + filter: drop-shadow(0 0 4px rgba(213, 242, 255, 0.68)) drop-shadow(0 5px 6px rgba(0, 53, 102, 0.5)); /* Present the direction cue on the apparent ground plane while its parent remains locked to the same fixed HUD position. */ - transform: perspective(220px) rotateX(62deg) rotateZ(var(--route-angle)) scaleY(1.35); + transform: perspective(220px) rotateX(62deg) rotateZ(var(--route-angle)) + scaleY(1.35); transform-origin: center; transition: transform 180ms ease; } @@ -259,7 +298,8 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c height: calc((var(--cockpit-keyhole-radius) - 20px) * 2); border: 2px solid color-mix(in srgb, var(--cockpit-accent) 72%, transparent); border-radius: 50%; - box-shadow: 5px 0 18px color-mix(in srgb, var(--cockpit-accent) 18%, transparent); + box-shadow: 5px 0 18px + color-mix(in srgb, var(--cockpit-accent) 18%, transparent); } .cockpit-altitude-rim-scale { position: absolute; @@ -298,13 +338,17 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c color: rgba(244, 251, 255, 0.68); transform: translateY(-50%) rotateZ(calc(var(--slot) * -1.7deg)); transform-origin: right center; - transition: top 120ms linear, right 120ms linear, transform 120ms linear; + transition: + top 120ms linear, + right 120ms linear, + transform 120ms linear; } .cockpit-altitude-rim-scale > span i { order: 2; display: block; width: 20px; - border-top: 1px solid color-mix(in srgb, var(--cockpit-accent) 58%, transparent); + border-top: 1px solid + color-mix(in srgb, var(--cockpit-accent) 58%, transparent); } .cockpit-altitude-rim-scale > span b { min-width: 46px; @@ -316,7 +360,9 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c letter-spacing: 0.08em; text-align: right; } -.cockpit-altitude-rim-scale > span.major { color: rgba(244, 251, 255, 0.9); } +.cockpit-altitude-rim-scale > span.major { + color: rgba(244, 251, 255, 0.9); +} .cockpit-altitude-rim-scale > span.major i { width: 32px; border-color: color-mix(in srgb, var(--cockpit-accent) 86%, transparent); @@ -342,13 +388,20 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c margin-left: 4px; color: var(--cockpit-accent); font-size: 24px; - font-variation-settings: 'FILL' 1, 'wght' 600; + font-variation-settings: + 'FILL' 1, + 'wght' 600; animation: cockpit-altitude-pointer 1.8s ease-in-out infinite; } @keyframes cockpit-altitude-pointer { - 50% { transform: translateX(-3px); filter: brightness(1.35); } + 50% { + transform: translateX(-3px); + filter: brightness(1.35); + } +} +.cockpit-altitude-rim.unavailable { + opacity: 0.24; } -.cockpit-altitude-rim.unavailable { opacity: 0.24; } .cockpit-speed-rim { --speed-tape-inset: clamp(112px, 9vw, 148px); right: auto; @@ -362,7 +415,8 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c .cockpit-speed-rim .cockpit-altitude-rim-rail::before { right: auto; left: 0; - box-shadow: -5px 0 18px color-mix(in srgb, var(--cockpit-accent) 18%, transparent); + box-shadow: -5px 0 18px + color-mix(in srgb, var(--cockpit-accent) 18%, transparent); } .cockpit-speed-rim .cockpit-altitude-rim-scale > span { right: auto; @@ -374,19 +428,26 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c transform: translateY(-50%) rotateZ(calc(var(--slot) * 1.7deg)); transform-origin: left center; } -.cockpit-speed-rim .cockpit-altitude-rim-scale > span b { text-align: left; } +.cockpit-speed-rim .cockpit-altitude-rim-scale > span b { + text-align: left; +} .cockpit-speed-rim .cockpit-altitude-rim-pointer { right: auto; left: 0; } -.cockpit-speed-rim .cockpit-altitude-rim-pointer strong { text-align: left; } +.cockpit-speed-rim .cockpit-altitude-rim-pointer strong { + text-align: left; +} .cockpit-speed-rim .cockpit-altitude-rim-pointer .material-symbols-outlined { margin-right: 4px; margin-left: 0; animation-name: cockpit-speed-pointer; } @keyframes cockpit-speed-pointer { - 50% { transform: translateX(3px); filter: brightness(1.35); } + 50% { + transform: translateX(3px); + filter: brightness(1.35); + } } .cockpit-visor-status { position: absolute; @@ -395,8 +456,14 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c font-size: 7px; letter-spacing: 0.22em; } -.cockpit-visor-status-left { left: clamp(110px, 15vw, 220px); transform: rotateY(22deg); } -.cockpit-visor-status-right { right: clamp(110px, 15vw, 220px); transform: rotateY(-22deg); } +.cockpit-visor-status-left { + left: clamp(110px, 15vw, 220px); + transform: rotateY(22deg); +} +.cockpit-visor-status-right { + right: clamp(110px, 15vw, 220px); + transform: rotateY(-22deg); +} .cockpit-topline { position: absolute; z-index: 5; @@ -408,14 +475,22 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c align-items: start; gap: 24px; } -.cockpit-topline > div:first-child { display: flex; flex-direction: column; gap: 4px; } -.cockpit-kicker, .cockpit-readout-label { +.cockpit-topline > div:first-child { + display: flex; + flex-direction: column; + gap: 4px; +} +.cockpit-kicker, +.cockpit-readout-label { color: var(--cockpit-accent); font-size: 9px; font-weight: 600; letter-spacing: 0.22em; } -#cockpit-callsign { font-size: 18px; letter-spacing: 0.12em; } +#cockpit-callsign { + font-size: 18px; + letter-spacing: 0.12em; +} .cockpit-aircraft-meta { color: rgba(244, 251, 255, 0.48); font-size: 7px; @@ -442,15 +517,26 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c box-shadow: 0 0 8px var(--cockpit-accent); animation: cockpit-live 1.6s ease-in-out infinite; } -@keyframes cockpit-live { 50% { opacity: 0.35; } } +@keyframes cockpit-live { + 50% { + opacity: 0.35; + } +} .cockpit-readout { position: relative; display: grid; min-width: 118px; padding: 6px 10px 7px; - border-top: 1px solid color-mix(in srgb, var(--cockpit-accent) 50%, transparent); - border-bottom: 1px solid color-mix(in srgb, var(--cockpit-accent) 18%, transparent); - background: linear-gradient(90deg, transparent, rgba(0, 7, 12, 0.34), transparent); + border-top: 1px solid + color-mix(in srgb, var(--cockpit-accent) 50%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--cockpit-accent) 18%, transparent); + background: linear-gradient( + 90deg, + transparent, + rgba(0, 7, 12, 0.34), + transparent + ); transform-style: preserve-3d; } .cockpit-speed { @@ -485,7 +571,8 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c } .cockpit-roll-token.is-rolling { color: color-mix(in srgb, currentColor 92%, var(--cockpit-accent)); - text-shadow: 0 0 10px color-mix(in srgb, var(--cockpit-accent) 35%, transparent); + text-shadow: 0 0 10px + color-mix(in srgb, var(--cockpit-accent) 35%, transparent); } .cockpit-roll-track { display: flex; @@ -503,18 +590,29 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c animation: cockpit-number-roll-up 200ms cubic-bezier(0.2, 0.75, 0.25, 1) both; } .cockpit-roll-token.roll-down .cockpit-roll-track { - animation: cockpit-number-roll-down 200ms cubic-bezier(0.2, 0.75, 0.25, 1) both; + animation: cockpit-number-roll-down 200ms cubic-bezier(0.2, 0.75, 0.25, 1) + both; } @keyframes cockpit-number-roll-up { - from { transform: translateY(0); } - to { transform: translateY(-1em); } + from { + transform: translateY(0); + } + to { + transform: translateY(-1em); + } } @keyframes cockpit-number-roll-down { - from { transform: translateY(-1em); } - to { transform: translateY(0); } + from { + transform: translateY(-1em); + } + to { + transform: translateY(0); + } } @media (prefers-reduced-motion: reduce) { - .cockpit-roll-token .cockpit-roll-track { animation-duration: 1ms; } + .cockpit-roll-token .cockpit-roll-track { + animation-duration: 1ms; + } } .cockpit-readout-unit { margin-top: 2px; @@ -556,7 +654,10 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c line-height: 1; white-space: nowrap; } -.cockpit-heading-value small { color: var(--cockpit-accent); font-size: 12px; } +.cockpit-heading-value small { + color: var(--cockpit-accent); + font-size: 12px; +} .cockpit-heading-caret { display: block; height: 20px; @@ -574,7 +675,8 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c content: ''; position: absolute; inset: 10px 2% 0; - border-top: 1px solid color-mix(in srgb, var(--cockpit-accent) 78%, transparent); + border-top: 1px solid + color-mix(in srgb, var(--cockpit-accent) 78%, transparent); border-radius: 50%; background: rgba(0, 7, 12, 0.16); } @@ -585,10 +687,8 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c color: rgba(244, 251, 255, 0.52); font-size: 12px; letter-spacing: 0.12em; - transform: - translateX(calc(-50% + (var(--slot) * 86px))) - translateY(calc(var(--depth) * 8px)) - rotateZ(calc(var(--slot) * 3.6deg)) + transform: translateX(calc(-50% + (var(--slot) * 86px))) + translateY(calc(var(--depth) * 8px)) rotateZ(calc(var(--slot) * 3.6deg)) rotateY(calc(var(--slot) * -9deg)); } .cockpit-compass-tape span::before { @@ -672,20 +772,28 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c } .cockpit-vision-controls .cockpit-vision-current { border-color: var(--cockpit-accent); - background: color-mix(in srgb, var(--cockpit-accent) 10%, rgba(0, 7, 12, 0.72)); - box-shadow: 0 0 10px color-mix(in srgb, var(--cockpit-accent) 30%, transparent); + background: color-mix( + in srgb, + var(--cockpit-accent) 10%, + rgba(0, 7, 12, 0.72) + ); + box-shadow: 0 0 10px + color-mix(in srgb, var(--cockpit-accent) 30%, transparent); } .cockpit-route-card { display: grid; gap: 8px; min-width: 0; padding: 2px 6px 10px 8px; - border-bottom: 1px solid color-mix(in srgb, var(--cockpit-accent) 22%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--cockpit-accent) 22%, transparent); color: rgba(224, 247, 255, 0.9); font: 7px/1.25 var(--font-mono); letter-spacing: 0.09em; } -.cockpit-route-card[hidden] { display: none; } +.cockpit-route-card[hidden] { + display: none; +} .cockpit-route-heading { display: flex; min-width: 0; @@ -702,7 +810,11 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c } .cockpit-route-heading small { overflow: hidden; - color: color-mix(in srgb, var(--cockpit-accent) 68%, rgba(244, 251, 255, 0.46)); + color: color-mix( + in srgb, + var(--cockpit-accent) 68%, + rgba(244, 251, 255, 0.46) + ); font-size: 5px; letter-spacing: 0.08em; text-overflow: ellipsis; @@ -719,7 +831,8 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c min-width: 0; gap: 3px; padding: 6px 7px; - border-left: 1px solid color-mix(in srgb, var(--cockpit-accent) 52%, transparent); + border-left: 1px solid + color-mix(in srgb, var(--cockpit-accent) 52%, transparent); background: rgba(0, 7, 12, 0.3); } .cockpit-route-airport > span { @@ -765,9 +878,18 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed)) .c cursor: pointer; box-shadow: 0 0 18px rgba(0, 0, 0, 0.24); } -#view-switcher button[hidden] { display: none; } -#view-switcher button:hover, #view-switcher button:focus-visible { border-color: #fff; color: #fff; outline: none; } -#view-switcher .material-symbols-outlined { font-size: 16px; } +#view-switcher button[hidden] { + display: none; +} +#view-switcher button:hover, +#view-switcher button:focus-visible { + border-color: #fff; + color: #fff; + outline: none; +} +#view-switcher .material-symbols-outlined { + font-size: 16px; +} body.cockpit-mode #view-switcher { right: auto; bottom: max(clamp(128px, 15vh, 150px), env(safe-area-inset-bottom)); @@ -806,31 +928,48 @@ body.cockpit-mode #view-switcher button { border-top-color: color-mix(in srgb, var(--cockpit-accent) 64%, transparent); border-radius: 8px 24px 24px 8px; background: rgba(0, 7, 12, 0.68); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 0 28px rgba(0,0,0,0.26); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + 0 0 28px rgba(0, 0, 0, 0.26); transform: perspective(900px) rotateY(4deg) translateZ(18px); transform-origin: left center; transform-style: preserve-3d; pointer-events: auto; - text-shadow: 0 0 8px color-mix(in srgb, var(--cockpit-accent) 45%, transparent); - scrollbar-color: color-mix(in srgb, var(--cockpit-accent) 45%, transparent) transparent; + text-shadow: 0 0 8px + color-mix(in srgb, var(--cockpit-accent) 45%, transparent); + scrollbar-color: color-mix(in srgb, var(--cockpit-accent) 45%, transparent) + transparent; scrollbar-width: thin; - transition: width 180ms ease, background-color 180ms ease, border-radius 180ms ease; + transition: + width 180ms ease, + background-color 180ms ease, + border-radius 180ms ease; +} +#cockpit-context[hidden] { + display: none; +} +.cockpit-context-window[data-state='uncertain'] { + border-top-color: rgba(116, 219, 255, 0.72); } -#cockpit-context[hidden] { display: none; } -.cockpit-context-window[data-state="uncertain"] { border-top-color: rgba(116, 219, 255, 0.72); } /* CONTACT LOST reuses the amber the app already spends on unknown/stale inputs (.cockpit-context-cohorts > div.unknown, .cockpit-brief-status[data-state="unavailable"]), so a held last-known readout never reads as a live one. */ -.cockpit-context-window[data-state="lost"] { border-top-color: rgba(213, 168, 99, 0.72); } +.cockpit-context-window[data-state='lost'] { + border-top-color: rgba(213, 168, 99, 0.72); +} .cockpit-context-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 5px 10px; padding: 0 5px 7px; - border-bottom: 1px solid color-mix(in srgb, var(--cockpit-accent) 22%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--cockpit-accent) 22%, transparent); +} +.cockpit-context-header > div { + display: grid; + gap: 2px; } -.cockpit-context-header > div { display: grid; gap: 2px; } .cockpit-context-kicker { color: var(--cockpit-accent); font-size: 8px; @@ -861,7 +1000,11 @@ body.cockpit-mode #view-switcher button { padding: 0 7px; border: 1px solid color-mix(in srgb, var(--cockpit-accent) 46%, transparent); border-radius: 999px; - background: color-mix(in srgb, var(--cockpit-accent) 8%, rgba(0, 7, 12, 0.78)); + background: color-mix( + in srgb, + var(--cockpit-accent) 8%, + rgba(0, 7, 12, 0.78) + ); color: color-mix(in srgb, var(--cockpit-accent) 82%, white); font: 700 6px/1 var(--font-mono); letter-spacing: 0.1em; @@ -871,18 +1014,24 @@ body.cockpit-mode #view-switcher button { color: #fff; font: inherit; } -.cockpit-context-weather[aria-pressed="false"] { +.cockpit-context-weather[aria-pressed='false'] { border-color: rgba(244, 251, 255, 0.15); background: rgba(0, 7, 12, 0.54); color: rgba(244, 251, 255, 0.42); } -.cockpit-context-weather[aria-pressed="false"] strong { color: rgba(244, 251, 255, 0.5); } -.cockpit-context-weather:hover, .cockpit-context-weather:focus-visible { +.cockpit-context-weather[aria-pressed='false'] strong { + color: rgba(244, 251, 255, 0.5); +} +.cockpit-context-weather:hover, +.cockpit-context-weather:focus-visible { border-color: var(--cockpit-accent); color: #fff; outline: none; } -.cockpit-context-actions { display: flex; gap: 4px; } +.cockpit-context-actions { + display: flex; + gap: 4px; +} .cockpit-context-actions button { display: grid; width: 28px; @@ -895,7 +1044,8 @@ body.cockpit-mode #view-switcher button { color: color-mix(in srgb, var(--cockpit-accent) 82%, white); cursor: pointer; } -.cockpit-context-actions button:hover, .cockpit-context-actions button:focus-visible { +.cockpit-context-actions button:hover, +.cockpit-context-actions button:focus-visible { border-color: var(--cockpit-accent); color: #fff; outline: none; @@ -905,7 +1055,9 @@ body.cockpit-mode #view-switcher button { color: rgba(244, 251, 255, 0.2); cursor: default; } -.cockpit-context-actions .material-symbols-outlined { font-size: 15px; } +.cockpit-context-actions .material-symbols-outlined { + font-size: 15px; +} .cockpit-context-cohorts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -918,7 +1070,8 @@ body.cockpit-mode #view-switcher button { gap: 3px 8px; min-width: 0; padding: 6px 9px; - border-left: 1px solid color-mix(in srgb, var(--cockpit-accent) 40%, transparent); + border-left: 1px solid + color-mix(in srgb, var(--cockpit-accent) 40%, transparent); background: rgba(0, 7, 12, 0.34); } .cockpit-context-cohorts span { @@ -942,8 +1095,12 @@ body.cockpit-mode #view-switcher button { text-overflow: ellipsis; white-space: nowrap; } -.cockpit-context-cohorts > div.unknown { border-left-color: rgba(213, 168, 99, 0.58); } -.cockpit-context-cohorts > div.unknown strong { color: #d5a863; } +.cockpit-context-cohorts > div.unknown { + border-left-color: rgba(213, 168, 99, 0.58); +} +.cockpit-context-cohorts > div.unknown strong { + color: #d5a863; +} .cockpit-context-nearest { display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; @@ -959,8 +1116,16 @@ body.cockpit-mode #view-switcher button { transform-origin: 50% 50%; transition: transform 180ms ease-out; } -#cockpit-context-direction.unknown { opacity: 0.3; } -.cockpit-context-nearest > div { grid-column: 2; grid-row: 1; display: grid; gap: 2px; min-width: 0; } +#cockpit-context-direction.unknown { + opacity: 0.3; +} +.cockpit-context-nearest > div { + grid-column: 2; + grid-row: 1; + display: grid; + gap: 2px; + min-width: 0; +} .cockpit-context-nearest small { color: rgba(244, 251, 255, 0.42); font-size: 6px; @@ -974,16 +1139,26 @@ body.cockpit-mode #view-switcher button { text-overflow: ellipsis; white-space: nowrap; } -#cockpit-context-bearing, #cockpit-context-distance { +#cockpit-context-bearing, +#cockpit-context-distance { color: var(--cockpit-accent); font-size: 10px; font-weight: 700; letter-spacing: 0.08em; white-space: nowrap; } -#cockpit-context-bearing { grid-column: 2; grid-row: 2; justify-self: start; } -#cockpit-context-distance { grid-column: 3; grid-row: 1 / 3; } -#cockpit-context-distance { color: #fff; } +#cockpit-context-bearing { + grid-column: 2; + grid-row: 2; + justify-self: start; +} +#cockpit-context-distance { + grid-column: 3; + grid-row: 1 / 3; +} +#cockpit-context-distance { + color: #fff; +} .cockpit-context-footer { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; @@ -995,26 +1170,28 @@ body.cockpit-mode #view-switcher button { font-size: 6px; letter-spacing: 0.11em; } -.cockpit-context-window[data-state="uncertain"] #cockpit-context-uncertainty { color: #74dbff; } -.cockpit-context-window[data-state="lost"] #cockpit-context-uncertainty { +.cockpit-context-window[data-state='uncertain'] #cockpit-context-uncertainty { + color: #74dbff; +} +.cockpit-context-window[data-state='lost'] #cockpit-context-uncertainty { color: #d5a863; font-weight: 600; } /* The held values are last-known, not current — dim them to match the cue. */ -.cockpit-context-window[data-state="lost"] :is( - .cockpit-context-cohorts, - .cockpit-context-nearest -) { opacity: 0.55; } +.cockpit-context-window[data-state='lost'] + :is(.cockpit-context-cohorts, .cockpit-context-nearest) { + opacity: 0.55; +} #cockpit-hud.context-active .cockpit-pitch-rail-left { opacity: 0.16; transition: opacity 180ms ease; } -.cockpit-context-window[data-layout-mode="rail-left-constrained"] { +.cockpit-context-window[data-layout-mode='rail-left-constrained'] { gap: 5px; padding-top: 8px; padding-bottom: 8px; } -.cockpit-context-window[data-collapsed="true"] { +.cockpit-context-window[data-collapsed='true'] { width: min(248px, 22vw); max-height: none; gap: 0; @@ -1022,19 +1199,22 @@ body.cockpit-mode #view-switcher button { border-radius: 7px 18px 18px 7px; background: rgba(0, 7, 12, 0.54); } -.cockpit-context-window[data-collapsed="true"] .cockpit-context-header { +.cockpit-context-window[data-collapsed='true'] .cockpit-context-header { gap: 8px; padding: 0; border-bottom: 0; } -.cockpit-context-window[data-collapsed="true"] :is( - .cockpit-context-qualifier, - .cockpit-context-cohorts, - .cockpit-context-nearest, - .cockpit-context-footer, - .cockpit-context-actions button:not(#cockpit-context-toggle) -) { display: none; } -.cockpit-context-window[data-collapsed="true"] #cockpit-context-subject { +.cockpit-context-window[data-collapsed='true'] + :is( + .cockpit-context-qualifier, + .cockpit-context-cohorts, + .cockpit-context-nearest, + .cockpit-context-footer, + .cockpit-context-actions button:not(#cockpit-context-toggle) + ) { + display: none; +} +.cockpit-context-window[data-collapsed='true'] #cockpit-context-subject { font-size: 8px; letter-spacing: 0.08em; } @@ -1060,20 +1240,32 @@ body.cockpit-mode #view-switcher button { border-top-color: color-mix(in srgb, var(--cockpit-accent) 64%, transparent); border-radius: 24px 8px 8px 24px; background: rgba(0, 7, 12, 0.68); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 0 28px rgba(0,0,0,0.26); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + 0 0 28px rgba(0, 0, 0, 0.26); transform: perspective(900px) rotateY(-4deg) translateZ(18px); transform-origin: right center; pointer-events: auto; - text-shadow: 0 0 8px color-mix(in srgb, var(--cockpit-accent) 45%, transparent); - scrollbar-color: color-mix(in srgb, var(--cockpit-accent) 45%, transparent) transparent; + text-shadow: 0 0 8px + color-mix(in srgb, var(--cockpit-accent) 45%, transparent); + scrollbar-color: color-mix(in srgb, var(--cockpit-accent) 45%, transparent) + transparent; scrollbar-width: thin; - transition: width 180ms ease, background-color 180ms ease, border-radius 180ms ease; + transition: + width 180ms ease, + background-color 180ms ease, + border-radius 180ms ease; +} +#cockpit-signal-stream[hidden] { + display: none; } -#cockpit-signal-stream[hidden] { display: none; } .cockpit-utility-controls { position: absolute; z-index: 5; - top: var(--cockpit-utility-top, var(--left-stack-safe-top, var(--left-stack-top))); + top: var( + --cockpit-utility-top, + var(--left-stack-safe-top, var(--left-stack-top)) + ); right: clamp(24px, 4vw, 58px); bottom: auto; display: flex; @@ -1083,7 +1275,9 @@ body.cockpit-mode #view-switcher button { gap: 7px; pointer-events: auto; } -.cockpit-utility-control { position: relative; } +.cockpit-utility-control { + position: relative; +} .cockpit-utility-control.is-expanded { width: 272px; box-sizing: border-box; @@ -1091,7 +1285,9 @@ body.cockpit-mode #view-switcher button { border: 1px solid var(--glass-border); border-radius: var(--panel-radius); background: var(--glass-bg); - box-shadow: 0 8px 32px rgba(0, 0, 0, .5), inset 0 1px 0 rgba(255, 255, 255, .05); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.05); backdrop-filter: blur(24px) saturate(1.4); -webkit-backdrop-filter: blur(24px) saturate(1.4); max-height: var( @@ -1100,19 +1296,23 @@ body.cockpit-mode #view-switcher button { ); overflow-y: auto; } -.cockpit-utility-control.is-expanded:has([data-cockpit-launcher="display"]) { +.cockpit-utility-control.is-expanded:has([data-cockpit-launcher='display']) { width: var(--display-panel-expanded-width); display: flex; flex-direction: column; align-items: flex-end; gap: 7px; - box-shadow: 0 8px 32px rgba(0, 0, 0, .42), inset 0 1px 0 rgba(255, 255, 255, .04); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.42), + inset 0 1px 0 rgba(255, 255, 255, 0.04); } -.cockpit-utility-control.is-expanded:has([data-cockpit-launcher="radio"]) { +.cockpit-utility-control.is-expanded:has([data-cockpit-launcher='radio']) { width: var(--cockpit-radio-expanded-width, 232px); } .cockpit-utility-controls.layout-primary-only - > .cockpit-utility-control:not(.is-expanded) { display: none; } + > .cockpit-utility-control:not(.is-expanded) { + display: none; +} .cockpit-utility-launcher { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; @@ -1124,7 +1324,9 @@ body.cockpit-mode #view-switcher button { border: 1px solid var(--glass-border); border-radius: var(--panel-radius); background: var(--glass-bg); - box-shadow: 0 8px 32px rgba(0, 0, 0, .42), inset 0 1px 0 rgba(255, 255, 255, .04); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.42), + inset 0 1px 0 rgba(255, 255, 255, 0.04); backdrop-filter: blur(24px) saturate(1.4); -webkit-backdrop-filter: blur(24px) saturate(1.4); color: var(--text-dim); @@ -1132,10 +1334,10 @@ body.cockpit-mode #view-switcher button { letter-spacing: 1.4px; text-align: left; } -[data-cockpit-launcher="display"] { +[data-cockpit-launcher='display'] { width: var(--left-collapsed-width, 176px); } -[data-cockpit-launcher="radio"] { +[data-cockpit-launcher='radio'] { width: var(--cockpit-radio-collapsed-width, 148px); } .cockpit-utility-control.is-expanded .cockpit-utility-launcher { @@ -1152,7 +1354,7 @@ body.cockpit-mode #view-switcher button { .cockpit-utility-control.is-expanded .cockpit-utility-launcher { width: 100%; } -.cockpit-utility-control.is-expanded:has([data-cockpit-launcher="display"]) +.cockpit-utility-control.is-expanded:has([data-cockpit-launcher='display']) .cockpit-utility-launcher { min-height: 42px; box-sizing: border-box; @@ -1167,8 +1369,13 @@ body.cockpit-mode #view-switcher button { box-shadow: none; } .cockpit-utility-control.is-expanded .cockpit-utility-divider { - background: linear-gradient(90deg, rgb(0 212 255 / 28%), rgba(0, 212, 255, 0.18) 58%, transparent); - box-shadow: 0 0 7px rgba(0, 212, 255, .22); + background: linear-gradient( + 90deg, + rgb(0 212 255 / 28%), + rgba(0, 212, 255, 0.18) 58%, + transparent + ); + box-shadow: 0 0 7px rgba(0, 212, 255, 0.22); } .cockpit-utility-glyph { display: grid; @@ -1178,7 +1385,7 @@ body.cockpit-mode #view-switcher button { place-items: center; border: 1px solid var(--glass-border); border-radius: 6px; - background: rgba(255, 255, 255, .03); + background: rgba(255, 255, 255, 0.03); color: var(--text-secondary); font-family: var(--font-mono); font-size: 12px; @@ -1204,7 +1411,9 @@ body.cockpit-mode #view-switcher button { background: transparent; box-shadow: none; } -.cockpit-utility-popover[hidden] { display: none !important; } +.cockpit-utility-popover[hidden] { + display: none !important; +} .cockpit-display-slot { display: block; min-width: 0; @@ -1222,7 +1431,9 @@ body.cockpit-mode #view-switcher button { width: 100%; min-width: 0; } -#cockpit-display-panel [data-cockpit-display-slot="parameters"] #param-slider-panel.active { +#cockpit-display-panel + [data-cockpit-display-slot='parameters'] + #param-slider-panel.active { width: 100%; max-height: none; overflow-y: visible; @@ -1232,12 +1443,14 @@ body.cockpit-mode #view-switcher button { min-height: 38px; box-sizing: border-box; } -.cockpit-radio-compact { width: 100%; } +.cockpit-radio-compact { + width: 100%; +} .cockpit-radio-station { overflow: hidden; color: #fff; font: 500 8px/1.2 var(--font-mono); - letter-spacing: .08em; + letter-spacing: 0.08em; text-overflow: ellipsis; white-space: nowrap; } @@ -1248,33 +1461,45 @@ body.cockpit-mode #view-switcher button { } .cockpit-radio-transport button { min-height: 32px; - border: 1px solid rgba(65, 220, 231, .24); + border: 1px solid rgba(65, 220, 231, 0.24); border-radius: 6px; - background: rgba(65, 220, 231, .05); + background: rgba(65, 220, 231, 0.05); color: #9ceaf1; cursor: pointer; } -.cockpit-radio-transport button:disabled { opacity: .38; cursor: not-allowed; } -.cockpit-radio-transport button.active { border-color: var(--cockpit-accent); color: #fff; } +.cockpit-radio-transport button:disabled { + opacity: 0.38; + cursor: not-allowed; +} +.cockpit-radio-transport button.active { + border-color: var(--cockpit-accent); + color: #fff; +} .cockpit-radio-volume { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; - color: rgba(244, 251, 255, .58); + color: rgba(244, 251, 255, 0.58); font: 7px/1 var(--font-mono); - letter-spacing: .1em; + letter-spacing: 0.1em; +} +.cockpit-radio-volume input { + width: 100%; } -.cockpit-radio-volume input { width: 100%; } .cockpit-signal-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 0 4px 7px 8px; - border-bottom: 1px solid color-mix(in srgb, var(--cockpit-accent) 22%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--cockpit-accent) 22%, transparent); +} +.cockpit-signal-header > div { + display: grid; + gap: 2px; } -.cockpit-signal-header > div { display: grid; gap: 2px; } .cockpit-signal-header strong { color: #fff; font-size: 8px; @@ -1313,25 +1538,36 @@ body.cockpit-mode #view-switcher button { color: color-mix(in srgb, var(--cockpit-accent) 82%, white); cursor: pointer; } -.cockpit-brief-actions button:hover, .cockpit-brief-actions button:focus-visible { +.cockpit-brief-actions button:hover, +.cockpit-brief-actions button:focus-visible { border-color: var(--cockpit-accent); color: #fff; outline: none; } -.cockpit-brief-actions .material-symbols-outlined { font-size: 14px; } +.cockpit-brief-actions .material-symbols-outlined { + font-size: 14px; +} #cockpit-brief-auto { width: 50px; color: rgba(244, 251, 255, 0.42); font: 700 6px/1 var(--font-mono); letter-spacing: 0.05em; } -#cockpit-brief-auto[aria-pressed="true"] { +#cockpit-brief-auto[aria-pressed='true'] { border-color: var(--cockpit-accent); - background: color-mix(in srgb, var(--cockpit-accent) 14%, rgba(0, 7, 12, 0.72)); + background: color-mix( + in srgb, + var(--cockpit-accent) 14%, + rgba(0, 7, 12, 0.72) + ); color: #fff; - box-shadow: 0 0 10px color-mix(in srgb, var(--cockpit-accent) 24%, transparent); + box-shadow: 0 0 10px + color-mix(in srgb, var(--cockpit-accent) 24%, transparent); +} +#cockpit-signal-toggle { + width: 28px; + margin-left: 2px; } -#cockpit-signal-toggle { width: 28px; margin-left: 2px; } #cockpit-brief-position { min-width: 25px; color: rgba(244, 251, 255, 0.48); @@ -1346,7 +1582,9 @@ body.cockpit-mode #view-switcher button { [data-cockpit-brief-page] { animation: cockpit-brief-enter 220ms ease-out both; } -[data-cockpit-brief-page][hidden] { display: none; } +[data-cockpit-brief-page][hidden] { + display: none; +} .cockpit-signal-list { display: grid; gap: 3px; @@ -1363,23 +1601,42 @@ body.cockpit-mode #view-switcher button { background: rgba(0, 7, 12, 0.3); } .cockpit-signal-list li.actionable { - transition: border-color 160ms ease, background 160ms ease; + transition: + border-color 160ms ease, + background 160ms ease; } .cockpit-signal-list li.actionable:hover, .cockpit-signal-list li.actionable:focus-within { border-left-color: var(--cockpit-accent); - background: color-mix(in srgb, var(--cockpit-accent) 10%, rgba(0, 7, 12, 0.4)); + background: color-mix( + in srgb, + var(--cockpit-accent) 10%, + rgba(0, 7, 12, 0.4) + ); +} +.cockpit-signal-list li.track, +.cockpit-signal-list li.current { + border-left-color: var(--cockpit-accent); +} +.cockpit-signal-list li.nearby { + border-left-color: #fff; +} +.cockpit-signal-list li.military { + border-left-color: #d5a863; +} +.cockpit-signal-list li.warning { + border-left-color: #d5a863; } -.cockpit-signal-list li.track, .cockpit-signal-list li.current { border-left-color: var(--cockpit-accent); } -.cockpit-signal-list li.nearby { border-left-color: #fff; } -.cockpit-signal-list li.military { border-left-color: #d5a863; } -.cockpit-signal-list li.warning { border-left-color: #d5a863; } .cockpit-signal-list time { color: rgba(244, 251, 255, 0.38); font-size: 6px; letter-spacing: 0.06em; } -.cockpit-signal-list li > div { display: grid; gap: 2px; min-width: 0; } +.cockpit-signal-list li > div { + display: grid; + gap: 2px; + min-width: 0; +} .cockpit-signal-list strong { color: color-mix(in srgb, var(--cockpit-accent) 78%, white); font-size: 10px; @@ -1402,7 +1659,9 @@ body.cockpit-mode #view-switcher button { text-align: left; cursor: pointer; } -.cockpit-signal-list li.military .cockpit-signal-target { color: #f0c968; } +.cockpit-signal-list li.military .cockpit-signal-target { + color: #f0c968; +} .cockpit-signal-target-label { overflow: hidden; color: currentColor !important; @@ -1415,7 +1674,9 @@ body.cockpit-mode #view-switcher button { overflow: visible !important; background: linear-gradient(90deg, currentColor, transparent); opacity: 0.42; - transition: opacity 160ms ease, transform 160ms ease; + transition: + opacity 160ms ease, + transform 160ms ease; transform-origin: left center; } .cockpit-signal-target-chevron { @@ -1429,7 +1690,8 @@ body.cockpit-mode #view-switcher button { .cockpit-signal-target:focus-visible { color: #fff; outline: none; - text-shadow: 0 0 8px color-mix(in srgb, var(--cockpit-accent) 72%, transparent); + text-shadow: 0 0 8px + color-mix(in srgb, var(--cockpit-accent) 72%, transparent); } .cockpit-signal-target:hover .cockpit-signal-target-rule, .cockpit-signal-target:focus-visible .cockpit-signal-target-rule { @@ -1440,7 +1702,9 @@ body.cockpit-mode #view-switcher button { .cockpit-signal-target:focus-visible .cockpit-signal-target-chevron { transform: translateX(2px); } -.cockpit-signal-list li.warning strong { color: #d5a863; } +.cockpit-signal-list li.warning strong { + color: #d5a863; +} .cockpit-signal-list span { overflow: hidden; color: rgba(244, 251, 255, 0.66); @@ -1456,9 +1720,12 @@ body.cockpit-mode #view-switcher button { padding: 0; list-style: none; } -.cockpit-news-list:empty { display: none; } +.cockpit-news-list:empty { + display: none; +} .cockpit-news-list li { - border-left: 1px solid color-mix(in srgb, var(--cockpit-accent) 62%, transparent); + border-left: 1px solid + color-mix(in srgb, var(--cockpit-accent) 62%, transparent); background: rgba(0, 7, 12, 0.3); } .cockpit-news-list a { @@ -1468,7 +1735,8 @@ body.cockpit-mode #view-switcher button { color: inherit; text-decoration: none; } -.cockpit-news-list a:hover, .cockpit-news-list a:focus-visible { +.cockpit-news-list a:hover, +.cockpit-news-list a:focus-visible { background: color-mix(in srgb, var(--cockpit-accent) 8%, transparent); outline: 1px solid color-mix(in srgb, var(--cockpit-accent) 46%, transparent); } @@ -1484,7 +1752,11 @@ body.cockpit-mode #view-switcher button { -webkit-line-clamp: 2; } .cockpit-news-list span { - color: color-mix(in srgb, var(--cockpit-accent) 65%, rgba(244, 251, 255, 0.42)); + color: color-mix( + in srgb, + var(--cockpit-accent) 65%, + rgba(244, 251, 255, 0.42) + ); font-size: 6px; letter-spacing: 0.08em; text-transform: uppercase; @@ -1500,7 +1772,9 @@ body.cockpit-mode #view-switcher button { letter-spacing: 0.13em; text-align: center; } -.cockpit-brief-status[data-state="unavailable"] { color: #d5a863; } +.cockpit-brief-status[data-state='unavailable'] { + color: #d5a863; +} .cockpit-local-place { display: grid; gap: 3px; @@ -1587,7 +1861,10 @@ body.cockpit-mode #view-switcher button { text-overflow: ellipsis; white-space: nowrap; } -.cockpit-brief-tabs { display: flex; gap: 3px; } +.cockpit-brief-tabs { + display: flex; + gap: 3px; +} .cockpit-brief-tabs button { padding: 2px 4px; border: 0; @@ -1598,16 +1875,19 @@ body.cockpit-mode #view-switcher button { letter-spacing: 0.08em; cursor: pointer; } -.cockpit-brief-tabs button[aria-current="true"] { +.cockpit-brief-tabs button[aria-current='true'] { border-bottom-color: var(--cockpit-accent); color: color-mix(in srgb, var(--cockpit-accent) 80%, white); } -.cockpit-brief-tabs button:hover, .cockpit-brief-tabs button:focus-visible { +.cockpit-brief-tabs button:hover, +.cockpit-brief-tabs button:focus-visible { color: #fff; outline: none; } -.cockpit-signal-window[data-layout-mode="rail-right-constrained"] { gap: 5px; } -.cockpit-signal-window[data-collapsed="true"] { +.cockpit-signal-window[data-layout-mode='rail-right-constrained'] { + gap: 5px; +} +.cockpit-signal-window[data-collapsed='true'] { width: min(248px, 22vw); max-height: none; gap: 0; @@ -1615,20 +1895,31 @@ body.cockpit-mode #view-switcher button { border-radius: 18px 7px 7px 18px; background: rgba(0, 7, 12, 0.54); } -.cockpit-signal-window[data-collapsed="true"] .cockpit-signal-header { +.cockpit-signal-window[data-collapsed='true'] .cockpit-signal-header { padding: 0; border-bottom: 0; } -.cockpit-signal-window[data-collapsed="true"] .cockpit-route-card { display: none; } -.cockpit-signal-window[data-collapsed="true"] :is(.cockpit-brief-stage, .cockpit-signal-footer) { +.cockpit-signal-window[data-collapsed='true'] .cockpit-route-card { display: none; } -.cockpit-signal-window[data-collapsed="true"] .cockpit-brief-actions > :not(#cockpit-signal-toggle) { +.cockpit-signal-window[data-collapsed='true'] + :is(.cockpit-brief-stage, .cockpit-signal-footer) { + display: none; +} +.cockpit-signal-window[data-collapsed='true'] + .cockpit-brief-actions + > :not(#cockpit-signal-toggle) { display: none; } @keyframes cockpit-brief-enter { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } .cockpit-help { position: absolute; @@ -1638,13 +1929,23 @@ body.cockpit-mode #view-switcher button { font-size: 9px; letter-spacing: 0.14em; } -.cockpit-help span { color: var(--cockpit-accent); } -body.cockpit-mode :is( - #title-bar, #style-indicator, #top-center-actions, #traffic-sync-chip, #cctv-sync-chip, - #command-dock, - #military-awareness-panel, #military-awareness-direction-overlay, - #cockpit-entry -) { display: none !important; } +.cockpit-help span { + color: var(--cockpit-accent); +} +body.cockpit-mode + :is( + #title-bar, + #style-indicator, + #top-center-actions, + #traffic-sync-chip, + #cctv-sync-chip, + #command-dock, + #military-awareness-panel, + #military-awareness-direction-overlay, + #cockpit-entry + ) { + display: none !important; +} body.cockpit-mode :is(#clear-selected-layers, #share-btn, #reset-globe-view) { display: none !important; } @@ -1657,7 +1958,9 @@ body.cockpit-mode #left-panel-stack { max-height: none; transition: none; } -body.cockpit-mode #left-panel-stack > #scene-panel { display: none !important; } +body.cockpit-mode #left-panel-stack > #scene-panel { + display: none !important; +} body.cockpit-mode #right-context-rail { display: none !important; } @@ -1690,13 +1993,24 @@ body.cockpit-mode #intel-hud .hud-bottom-left, body.cockpit-mode #intel-hud .hud-bottom-right { bottom: clamp(84px, 11vh, 118px); } -body.cockpit-mode #intel-hud .hud-summary-wrap { max-width: min(34vw, 380px); } -body.cockpit-mode #intel-hud .hud-summary { font-size: 9px; } -body.cockpit-mode #intel-hud .hud-left-edge { left: 22px; } -body.cockpit-mode #intel-hud .hud-right-edge { right: 22px; } +body.cockpit-mode #intel-hud .hud-summary-wrap { + max-width: min(34vw, 380px); +} +body.cockpit-mode #intel-hud .hud-summary { + font-size: 9px; +} +body.cockpit-mode #intel-hud .hud-left-edge { + left: 22px; +} +body.cockpit-mode #intel-hud .hud-right-edge { + right: 22px; +} @media (max-width: 760px) { - #view-switcher { right: 16px; bottom: 16px; } + #view-switcher { + right: 16px; + bottom: 16px; + } body.cockpit-mode #view-switcher { right: auto; bottom: max(76px, env(safe-area-inset-bottom)); @@ -1708,7 +2022,11 @@ body.cockpit-mode #intel-hud .hud-right-edge { right: 22px; } padding-inline: 9px; font-size: 7px; } - #cockpit-entry { left: auto; right: 16px; bottom: 88px; } + #cockpit-entry { + left: auto; + right: 16px; + bottom: 88px; + } body.cockpit-mode #left-panel-stack, body.cockpit-mode #intel-hud, body.cockpit-mode .cockpit-context-window, @@ -1738,21 +2056,44 @@ body.cockpit-mode #intel-hud .hud-right-edge { right: 22px; } width: min(272px, calc(100vw - 24px)); max-height: calc(100vh - 260px); } - body.cockpit-mode .cockpit-utility-controls:has(.cockpit-utility-control.is-expanded) - > .cockpit-utility-control:not(.is-expanded) { display: none; } + body.cockpit-mode + .cockpit-utility-controls:has(.cockpit-utility-control.is-expanded) + > .cockpit-utility-control:not(.is-expanded) { + display: none; + } body.cockpit-mode .cockpit-utility-popover { position: static; width: 100%; } - .cockpit-topline { grid-template-columns: 1fr auto; } - .cockpit-mode-chip { display: none; } - .cockpit-readout { min-width: 76px; padding: 4px 6px 5px; } - .cockpit-readout strong { font-size: clamp(24px, 8vw, 34px); } - .cockpit-readout-label { font-size: 7px; letter-spacing: 0.12em; } - .cockpit-readout-unit { font-size: 8px; } - .cockpit-speed { transform: perspective(600px) rotateY(7deg); } - .cockpit-altitude { transform: perspective(600px) rotateY(-7deg); } - .cockpit-compass { width: 92vw; } + .cockpit-topline { + grid-template-columns: 1fr auto; + } + .cockpit-mode-chip { + display: none; + } + .cockpit-readout { + min-width: 76px; + padding: 4px 6px 5px; + } + .cockpit-readout strong { + font-size: clamp(24px, 8vw, 34px); + } + .cockpit-readout-label { + font-size: 7px; + letter-spacing: 0.12em; + } + .cockpit-readout-unit { + font-size: 8px; + } + .cockpit-speed { + transform: perspective(600px) rotateY(7deg); + } + .cockpit-altitude { + transform: perspective(600px) rotateY(-7deg); + } + .cockpit-compass { + width: 92vw; + } .cockpit-instrument-row { grid-template-columns: minmax(76px, 1fr) auto minmax(76px, 1fr); gap: 8px; @@ -1768,7 +2109,7 @@ body.cockpit-mode #intel-hud .hud-right-edge { right: 22px; } border-radius: 20px 20px 10px 10px; transform: translateZ(22px); } - .cockpit-context-window[data-collapsed="true"] { + .cockpit-context-window[data-collapsed='true'] { right: auto; width: min(248px, calc(100vw - 24px)); max-height: none; @@ -1782,33 +2123,59 @@ body.cockpit-mode #intel-hud .hud-right-edge { right: 22px; } border-radius: 20px 10px 10px 20px; transform: translateZ(22px); } - .cockpit-signal-window[data-collapsed="true"] { + .cockpit-signal-window[data-collapsed='true'] { left: auto; width: min(248px, calc(100vw - 24px)); max-height: none; } - .cockpit-context-header { grid-template-columns: 1fr auto; } - .cockpit-context-qualifier { display: none; } - .cockpit-context-cohorts small { display: none; } - .cockpit-context-cohorts { grid-template-columns: repeat(4, minmax(0, 1fr)); } - .cockpit-context-cohorts > div { padding: 4px 6px; } - .cockpit-context-nearest { grid-template-columns: 20px minmax(0, 1fr) auto; gap: 6px; } - #cockpit-context-bearing { display: none; } - .cockpit-context-footer { font-size: 5px; } + .cockpit-context-header { + grid-template-columns: 1fr auto; + } + .cockpit-context-qualifier { + display: none; + } + .cockpit-context-cohorts small { + display: none; + } + .cockpit-context-cohorts { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .cockpit-context-cohorts > div { + padding: 4px 6px; + } + .cockpit-context-nearest { + grid-template-columns: 20px minmax(0, 1fr) auto; + gap: 6px; + } + #cockpit-context-bearing { + display: none; + } + .cockpit-context-footer { + font-size: 5px; + } .cockpit-compass-tape span { - transform: - translateX(calc(-50% + (var(--slot) * 52px))) - translateY(calc(var(--depth) * 7px)) - rotateZ(calc(var(--slot) * 3deg)); + transform: translateX(calc(-50% + (var(--slot) * 52px))) + translateY(calc(var(--depth) * 7px)) rotateZ(calc(var(--slot) * 3deg)); + } + .cockpit-pitch-rail { + display: none; + } + .cockpit-altitude-rim { + display: none; + } + .cockpit-visor-status { + display: none; } - .cockpit-pitch-rail { display: none; } - .cockpit-altitude-rim { display: none; } - .cockpit-visor-status { display: none; } } @media (prefers-reduced-motion: reduce) { - .cockpit-altitude-rim-pointer .material-symbols-outlined { animation: none; } - .cockpit-altitude-rim-scale > span { transition: none; } - [data-cockpit-brief-page] { animation: none; } + .cockpit-altitude-rim-pointer .material-symbols-outlined { + animation: none; + } + .cockpit-altitude-rim-scale > span { + transition: none; + } + [data-cockpit-brief-page] { + animation: none; + } } - diff --git a/src/ui/styles/command-dock-compact.css b/src/ui/styles/command-dock-compact.css index 1d36825a2..821877002 100644 --- a/src/ui/styles/command-dock-compact.css +++ b/src/ui/styles/command-dock-compact.css @@ -21,11 +21,15 @@ } #command-dock:has(#location-bar:not(.collapsed)) { - transform: translateX(calc(-50% + (var(--dock-location-open) - var(--dock-side-compact)) / 2)); + transform: translateX( + calc(-50% + (var(--dock-location-open) - var(--dock-side-compact)) / 2) + ); } #command-dock:has(#control-panel:not(.collapsed)) { - transform: translateX(calc(-50% - (var(--dock-presets-open) - var(--dock-side-compact)) / 2)); + transform: translateX( + calc(-50% - (var(--dock-presets-open) - var(--dock-side-compact)) / 2) + ); } #command-dock > #location-bar, @@ -300,13 +304,27 @@ transform-origin: bottom; } -#command-dock .gev-voice-visualizer span:nth-child(3n) { height: 1.35rem; } -#command-dock .gev-voice-visualizer span:nth-child(4n) { height: 0.95rem; } -#command-dock .gev-voice-visualizer span:nth-child(5n) { height: 1.7rem; } -#command-dock .gev-voice-visualizer span:nth-child(7n) { height: 0.75rem; } +#command-dock .gev-voice-visualizer span:nth-child(3n) { + height: 1.35rem; +} +#command-dock .gev-voice-visualizer span:nth-child(4n) { + height: 0.95rem; +} +#command-dock .gev-voice-visualizer span:nth-child(5n) { + height: 1.7rem; +} +#command-dock .gev-voice-visualizer span:nth-child(7n) { + height: 0.75rem; +} -#command-dock #gev-voice-control[data-status='listening'] .gev-voice-visualizer span, -#command-dock #gev-voice-control[data-status='executing'] .gev-voice-visualizer span { +#command-dock + #gev-voice-control[data-status='listening'] + .gev-voice-visualizer + span, +#command-dock + #gev-voice-control[data-status='executing'] + .gev-voice-visualizer + span { animation: gev-voice-bar 680ms ease-in-out infinite alternate; animation-delay: calc(var(--bar) * -45ms); } @@ -336,13 +354,25 @@ } @keyframes command-dock-reveal { - from { opacity: 0; transform: translateY(0.2rem); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(0.2rem); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes gev-voice-bar { - from { transform: scaleY(0.35); opacity: 0.5; } - to { transform: scaleY(1); opacity: 1; } + from { + transform: scaleY(0.35); + opacity: 0.5; + } + to { + transform: scaleY(1); + opacity: 1; + } } /* Compact dock clearance keeps the reference-like low profile. */ @@ -401,4 +431,3 @@ body:not(.ui-clean-view):not(.recording-mode) #cesium-credits { flex-basis: 8.5rem; } } - diff --git a/src/ui/styles/command-dock-sliding.css b/src/ui/styles/command-dock-sliding.css index 1b61ffbb1..396a34c21 100644 --- a/src/ui/styles/command-dock-sliding.css +++ b/src/ui/styles/command-dock-sliding.css @@ -170,7 +170,13 @@ } #command-dock #location-bar .dock-popover-content { - left: calc((var(--dock-side-compact) + var(--dock-side-compact) + var(--dock-voice-width) - var(--dock-popover-mobile-width)) / 2); + left: calc( + ( + var(--dock-side-compact) + var(--dock-side-compact) + + var(--dock-voice-width) - var(--dock-popover-mobile-width) + ) / + 2 + ); } #command-dock #control-panel .dock-popover-content { @@ -182,7 +188,13 @@ left edge — pinned by the 480 px bounds assert in `scripts/qa-map-source-tray.mjs`. */ width: var(--dock-popover-mobile-width); - right: calc((var(--dock-side-compact) + var(--dock-side-compact) + var(--dock-voice-width) - var(--dock-popover-mobile-width)) / 2); + right: calc( + ( + var(--dock-side-compact) + var(--dock-side-compact) + + var(--dock-voice-width) - var(--dock-popover-mobile-width) + ) / + 2 + ); } /* ── Required-attribution clearance ───────────────────────────────── @@ -213,8 +225,12 @@ bottom: calc(100% + 3rem); } - #command-dock.dock-has-pinned-tray #location-bar:not(.collapsed):not(.dock-pinned) .dock-popover-content, - #command-dock.dock-has-pinned-tray #control-panel:not(.collapsed):not(.dock-pinned) .dock-popover-content { + #command-dock.dock-has-pinned-tray + #location-bar:not(.collapsed):not(.dock-pinned) + .dock-popover-content, + #command-dock.dock-has-pinned-tray + #control-panel:not(.collapsed):not(.dock-pinned) + .dock-popover-content { bottom: calc(100% + 3rem + var(--dock-pinned-stack-height)); } @@ -224,8 +240,12 @@ top tray drops var(--dock-lower-pinned-height), landing on top of its pinned sibling. Naming the panel makes it (2,5,0), ahead of both the ordinary (2,1,0) and single-pinned (2,4,0) rules. */ - #command-dock.dock-has-two-pinned-trays #location-bar.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content, - #command-dock.dock-has-two-pinned-trays #control-panel.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content { + #command-dock.dock-has-two-pinned-trays + #location-bar.dock-pinned-top.dock-pinned:not(.collapsed) + .dock-popover-content, + #command-dock.dock-has-two-pinned-trays + #control-panel.dock-pinned-top.dock-pinned:not(.collapsed) + .dock-popover-content { bottom: calc(100% + 3rem + var(--dock-lower-pinned-height)); } } @@ -242,14 +262,22 @@ bottom: calc(100% + 2vh + 2.5rem); } - #command-dock.dock-has-pinned-tray #location-bar:not(.collapsed):not(.dock-pinned) .dock-popover-content, - #command-dock.dock-has-pinned-tray #control-panel:not(.collapsed):not(.dock-pinned) .dock-popover-content { + #command-dock.dock-has-pinned-tray + #location-bar:not(.collapsed):not(.dock-pinned) + .dock-popover-content, + #command-dock.dock-has-pinned-tray + #control-panel:not(.collapsed):not(.dock-pinned) + .dock-popover-content { bottom: calc(100% + 2vh + 2.5rem + var(--dock-pinned-stack-height)); } /* Panel IDs for the same specificity reason as the max-width:900px block. */ - #command-dock.dock-has-two-pinned-trays #location-bar.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content, - #command-dock.dock-has-two-pinned-trays #control-panel.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content { + #command-dock.dock-has-two-pinned-trays + #location-bar.dock-pinned-top.dock-pinned:not(.collapsed) + .dock-popover-content, + #command-dock.dock-has-two-pinned-trays + #control-panel.dock-pinned-top.dock-pinned:not(.collapsed) + .dock-popover-content { bottom: calc(100% + 2vh + 2.5rem + var(--dock-lower-pinned-height)); } } @@ -327,24 +355,39 @@ height: var(--audio-level, 5px); opacity: var(--audio-opacity, 0.56); animation: none; - transition: height 70ms linear, opacity 90ms linear; + transition: + height 70ms linear, + opacity 90ms linear; } -#command-dock #gev-voice-control[data-status='listening'] .gev-voice-visualizer span, -#command-dock #gev-voice-control[data-status='executing'] .gev-voice-visualizer span { +#command-dock + #gev-voice-control[data-status='listening'] + .gev-voice-visualizer + span, +#command-dock + #gev-voice-control[data-status='executing'] + .gev-voice-visualizer + span { animation: none; } -#command-dock #gev-voice-control[data-speaker='user'] :is(#gev-voice-status, #gev-voice-detail) { +#command-dock + #gev-voice-control[data-speaker='user'] + :is(#gev-voice-status, #gev-voice-detail) { color: #ffbf69; } -#command-dock #gev-voice-control[data-speaker='user'] .gev-voice-visualizer span { +#command-dock + #gev-voice-control[data-speaker='user'] + .gev-voice-visualizer + span { background: linear-gradient(to top, #d98a35, #ffcf8a); box-shadow: 0 0 0.35rem rgba(255, 191, 105, 0.32); } -#command-dock #gev-voice-control[data-speaker='ai'] :is(#gev-voice-status, #gev-voice-detail) { +#command-dock + #gev-voice-control[data-speaker='ai'] + :is(#gev-voice-status, #gev-voice-detail) { color: var(--accent); } @@ -364,11 +407,15 @@ @media (max-width: 900px) { #command-dock #location-bar .dock-popover-content { - left: calc((var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2); + left: calc( + (var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2 + ); } #command-dock #control-panel .dock-popover-content { - right: calc((var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2); + right: calc( + (var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2 + ); } } @@ -417,32 +464,44 @@ box-shadow: 0 0 0 0.18rem rgba(232, 234, 237, 0.04), 0 0 0.75rem rgba(232, 234, 237, 0.08); - transition: background var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast); + transition: + background var(--transition-fast), + border-color var(--transition-fast), + box-shadow var(--transition-fast); } #command-dock .gev-mic-orbit img { filter: grayscale(1) saturate(0) brightness(1.65); opacity: 0.68; - transition: filter var(--transition-fast), opacity var(--transition-fast); + transition: + filter var(--transition-fast), + opacity var(--transition-fast); } #command-dock #gev-voice-button:hover .gev-mic-orbit, -#command-dock #gev-voice-control:is( - [data-status='connecting'], - [data-status='listening']:not([data-microphone='muted']), - [data-status='executing'] -) .gev-mic-orbit { +#command-dock + #gev-voice-control:is( + [data-status='connecting'], + [data-status='listening']:not([data-microphone='muted']), + [data-status='executing'] + ) + .gev-mic-orbit { background: rgba(0, 212, 255, 0.06); border-color: rgba(0, 212, 255, 0.48); - box-shadow: 0 0 0 0.18rem rgba(0, 212, 255, 0.06), 0 0 0.9rem rgba(0, 212, 255, 0.12); + box-shadow: + 0 0 0 0.18rem rgba(0, 212, 255, 0.06), + 0 0 0.9rem rgba(0, 212, 255, 0.12); } #command-dock #gev-voice-button:hover .gev-mic-orbit img, -#command-dock #gev-voice-control:is( - [data-status='connecting'], - [data-status='listening']:not([data-microphone='muted']), - [data-status='executing'] -) .gev-mic-orbit img { +#command-dock + #gev-voice-control:is( + [data-status='connecting'], + [data-status='listening']:not([data-microphone='muted']), + [data-status='executing'] + ) + .gev-mic-orbit + img { filter: none; opacity: 1; } @@ -470,7 +529,6 @@ background: var(--glass-bg); border-color: var(--glass-border-hover); color: var(--accent); - } #command-dock > :is(#location-bar, #control-panel).dock-pinned { @@ -590,7 +648,9 @@ } #gev-voice-control:not([data-status='error']):hover .gev-voice-help-tray, -#gev-voice-control:not([data-status='error']) #gev-voice-button:focus-visible ~ .gev-voice-help-tray { +#gev-voice-control:not([data-status='error']) + #gev-voice-button:focus-visible + ~ .gev-voice-help-tray { visibility: visible; opacity: 1; transform: translate(-50%, 0) scale(1); @@ -622,10 +682,17 @@ } #command-dock #location-bar .location-toolbar::after { - background: linear-gradient(90deg, rgba(232, 234, 237, 0.24), rgba(232, 234, 237, 0)); + background: linear-gradient( + 90deg, + rgba(232, 234, 237, 0.24), + rgba(232, 234, 237, 0) + ); } #command-dock #control-panel .panel-header::after { - background: linear-gradient(90deg, rgba(232, 234, 237, 0), rgba(232, 234, 237, 0.24)); + background: linear-gradient( + 90deg, + rgba(232, 234, 237, 0), + rgba(232, 234, 237, 0.24) + ); } - diff --git a/src/ui/styles/command-dock-trays.css b/src/ui/styles/command-dock-trays.css index 63130d794..e1f75955f 100644 --- a/src/ui/styles/command-dock-trays.css +++ b/src/ui/styles/command-dock-trays.css @@ -5,7 +5,9 @@ --dock-presets-pinned-height: 0px; --dock-pinned-stack-height: 0px; --dock-lower-pinned-height: 0px; - --dock-unified-width: calc(var(--dock-side-compact) * 2 + var(--dock-voice-width)); + --dock-unified-width: calc( + var(--dock-side-compact) * 2 + var(--dock-voice-width) + ); --dock-location-open: max(34rem, calc(var(--dock-unified-width) + 0.75rem)); --dock-presets-open: max(34rem, calc(var(--dock-unified-width) + 0.75rem)); } @@ -33,7 +35,9 @@ -webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; - transition: background-color 160ms ease, filter 160ms ease; + transition: + background-color 160ms ease, + filter 160ms ease; } #command-dock .dock-label-icon-location { @@ -53,12 +57,10 @@ } #command-dock :is(#location-bar, #control-panel):hover .location-toolbar-label, -#command-dock :is(#location-bar, #control-panel):hover .panel-title{ +#command-dock :is(#location-bar, #control-panel):hover .panel-title { color: var(--accent); } - - #command-dock #control-panel .panel-header { justify-content: center; } @@ -173,7 +175,9 @@ width: 0.9rem; height: 0.9rem; opacity: 0.58; - transition: opacity 160ms ease, transform 160ms ease; + transition: + opacity 160ms ease, + transform 160ms ease; } #command-dock #location-bar .dock-pin-btn { @@ -188,12 +192,18 @@ transform: rotate(-12deg) scale(1.08); } -#command-dock.dock-has-pinned-tray #location-bar:not(.collapsed):not(.dock-pinned) .dock-popover-content, -#command-dock.dock-has-pinned-tray #control-panel:not(.collapsed):not(.dock-pinned) .dock-popover-content { +#command-dock.dock-has-pinned-tray + #location-bar:not(.collapsed):not(.dock-pinned) + .dock-popover-content, +#command-dock.dock-has-pinned-tray + #control-panel:not(.collapsed):not(.dock-pinned) + .dock-popover-content { bottom: calc(100% + 1.2rem + var(--dock-pinned-stack-height)); } -#command-dock.dock-has-two-pinned-trays .dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content { +#command-dock.dock-has-two-pinned-trays + .dock-pinned-top.dock-pinned:not(.collapsed) + .dock-popover-content { bottom: calc(100% + 1.2rem + var(--dock-lower-pinned-height)); } @@ -264,7 +274,9 @@ background: rgba(0, 212, 255, 0.06); border: 1px solid rgba(0, 212, 255, 0.48); border-radius: 50%; - box-shadow: 0 0 0 0.18rem rgba(0, 212, 255, 0.06), 0 0 0.9rem rgba(0, 212, 255, 0.12); + box-shadow: + 0 0 0 0.18rem rgba(0, 212, 255, 0.06), + 0 0 0.9rem rgba(0, 212, 255, 0.12); } #command-dock .gev-mic-label { @@ -288,9 +300,13 @@ transition: opacity 180ms ease; } -#command-dock #gev-voice-control[data-status='connecting'] .gev-voice-visualizer, +#command-dock + #gev-voice-control[data-status='connecting'] + .gev-voice-visualizer, #command-dock #gev-voice-control[data-status='listening'] .gev-voice-visualizer, -#command-dock #gev-voice-control[data-status='executing'] .gev-voice-visualizer { +#command-dock + #gev-voice-control[data-status='executing'] + .gev-voice-visualizer { visibility: visible; opacity: 1; transform: none; @@ -362,16 +378,23 @@ background: rgba(34, 10, 16, 0.96); border: 1px solid rgba(255, 92, 110, 0.5); border-radius: 0.42rem; - box-shadow: 0 0.9rem 2.2rem rgba(0, 0, 0, 0.62), 0 0 1.4rem rgba(255, 92, 110, 0.08); + box-shadow: + 0 0.9rem 2.2rem rgba(0, 0, 0, 0.62), + 0 0 1.4rem rgba(255, 92, 110, 0.08); backdrop-filter: blur(28px) saturate(1.25); -webkit-backdrop-filter: blur(28px) saturate(1.25); pointer-events: none; transform: translate(-50%, 0.55rem) scale(0.985); transform-origin: bottom center; - transition: opacity 180ms ease, visibility 180ms ease, transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); + transition: + opacity 180ms ease, + visibility 180ms ease, + transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); } -#command-dock #gev-voice-control[data-status='error']:not(.error-dismissed) .gev-voice-error-tray { +#command-dock + #gev-voice-control[data-status='error']:not(.error-dismissed) + .gev-voice-error-tray { visibility: visible; opacity: 1; pointer-events: auto; @@ -421,7 +444,10 @@ /* The compact dock leaves both lower corners free for required attribution and telemetry. */ body:not(.ui-clean-view):not(.recording-mode) #cesium-credits, -body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minimal'].active) #cesium-credits { +body:not(.ui-clean-view):not(.recording-mode):has( + #intel-hud[data-variant='minimal'].active + ) + #cesium-credits { right: auto; bottom: 0.75rem; left: 1.5rem; @@ -452,7 +478,10 @@ body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minim @media (max-width: 900px) { body:not(.ui-clean-view):not(.recording-mode) #cesium-credits, - body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minimal'].active) #cesium-credits { + body:not(.ui-clean-view):not(.recording-mode):has( + #intel-hud[data-variant='minimal'].active + ) + #cesium-credits { bottom: calc(2vh + 5rem); left: 0.75rem; max-width: calc(100vw - 1.5rem); @@ -480,4 +509,3 @@ body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minim bottom: calc(50vh + 8px); } } - diff --git a/src/ui/styles/command-dock.css b/src/ui/styles/command-dock.css index eb15e2884..3c6c8e531 100644 --- a/src/ui/styles/command-dock.css +++ b/src/ui/styles/command-dock.css @@ -7,7 +7,10 @@ z-index: 145; display: grid; grid-template-areas: 'locations voice presets'; - grid-template-columns: minmax(300px, 1fr) minmax(230px, 270px) minmax(390px, 1.15fr); + grid-template-columns: minmax(300px, 1fr) minmax(230px, 270px) minmax( + 390px, + 1.15fr + ); align-items: stretch; width: min(94vw, 1240px); padding: 6px; @@ -22,7 +25,9 @@ backdrop-filter: blur(28px) saturate(1.35); -webkit-backdrop-filter: blur(28px) saturate(1.35); transform: translateX(-50%); - transition: opacity var(--transition-smooth), visibility var(--transition-smooth); + transition: + opacity var(--transition-smooth), + visibility var(--transition-smooth); } #command-dock > #location-bar, @@ -214,7 +219,10 @@ body:not(.ui-clean-view):not(.recording-mode) #cesium-credits { bottom: calc(2vh + 7rem); } -body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minimal'].active) #cesium-credits { +body:not(.ui-clean-view):not(.recording-mode):has( + #intel-hud[data-variant='minimal'].active + ) + #cesium-credits { right: 36px; left: auto; width: min(340px, calc(100vw - 72px)) !important; diff --git a/src/ui/styles/controls.css b/src/ui/styles/controls.css index b40a2ee8b..9e3e71bb6 100644 --- a/src/ui/styles/controls.css +++ b/src/ui/styles/controls.css @@ -12,7 +12,11 @@ #control-panel .panel-glow { position: absolute; inset: -30px; - background: radial-gradient(ellipse at center bottom, var(--accent-dim) 0%, transparent 60%); + background: radial-gradient( + ellipse at center bottom, + var(--accent-dim) 0%, + transparent 60% + ); filter: blur(30px); opacity: 0.4; pointer-events: none; @@ -185,7 +189,11 @@ content: ''; position: absolute; inset: 0; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, transparent 100%); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.04) 0%, + transparent 100% + ); opacity: 0; transition: opacity var(--transition-fast); } @@ -264,7 +272,9 @@ display: flex; align-items: center; justify-content: center; - transition: opacity 0.8s ease, visibility 0.8s ease; + transition: + opacity 0.8s ease, + visibility 0.8s ease; } #loading-screen.hidden { @@ -287,8 +297,15 @@ } @keyframes logo-pulse { - 0%, 100% { opacity: 0.72; transform: scale(0.98); } - 50% { opacity: 1; transform: scale(1.02); } + 0%, + 100% { + opacity: 0.72; + transform: scale(0.98); + } + 50% { + opacity: 1; + transform: scale(1.02); + } } .loader-content h2 { @@ -309,8 +326,13 @@ } @keyframes pulse { - 0%, 100% { opacity: 0.4; } - 50% { opacity: 1; } + 0%, + 100% { + opacity: 0.4; + } + 50% { + opacity: 1; + } } /* ── Post-Processing Toggles ─────────────────── */ @@ -332,7 +354,9 @@ border: 1px solid var(--glass-border); border-radius: var(--panel-radius); background: var(--glass-bg); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.04); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.42), + inset 0 1px 0 rgba(255, 255, 255, 0.04); backdrop-filter: blur(24px) saturate(1.4); -webkit-backdrop-filter: blur(24px) saturate(1.4); } @@ -389,7 +413,9 @@ border: 1px solid var(--glass-border); border-radius: var(--panel-radius); background: var(--glass-bg); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.04); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.42), + inset 0 1px 0 rgba(255, 255, 255, 0.04); backdrop-filter: blur(24px) saturate(1.4); -webkit-backdrop-filter: blur(24px) saturate(1.4); cursor: default; @@ -461,7 +487,11 @@ font-size: 14px; font-weight: 400; font-style: normal; - font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; + font-variation-settings: + 'FILL' 0, + 'wght' 400, + 'GRAD' 0, + 'opsz' 20; } .pp-label { @@ -600,7 +630,7 @@ outline-offset: 2px; } .gev-quantitative-slider:disabled { - opacity: .42; + opacity: 0.42; cursor: not-allowed; } .gev-slider-value { @@ -608,7 +638,7 @@ color: var(--accent); font-family: var(--font-mono); font-size: 9px; - letter-spacing: .5px; + letter-spacing: 0.5px; text-align: right; flex-shrink: 0; } @@ -793,7 +823,9 @@ } /* Proximity / All segmented control under the 3D toggle */ -#models3d-mode-row { min-width: 184px; } +#models3d-mode-row { + min-width: 184px; +} .pp-mode-seg { display: flex; flex: 1; @@ -812,11 +844,21 @@ letter-spacing: 0.6px; text-transform: uppercase; cursor: pointer; - transition: background-color var(--transition-fast), color var(--transition-fast); + transition: + background-color var(--transition-fast), + color var(--transition-fast); +} +.pp-mode-btn + .pp-mode-btn { + border-left: 1px solid rgba(255, 255, 255, 0.14); +} +.pp-mode-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); +} +.pp-mode-btn.active { + background: var(--accent); + color: #000; } -.pp-mode-btn + .pp-mode-btn { border-left: 1px solid rgba(255, 255, 255, 0.14); } -.pp-mode-btn:hover { background: rgba(255, 255, 255, 0.08); color: var(--text-primary); } -.pp-mode-btn.active { background: var(--accent); color: #000; } /* These controls also move into Cockpit Display; keep focus on the control itself and inside clipped button groups, independent of selection color. */ @@ -913,8 +955,13 @@ body.ui-clean-view #scene-runtime.active { } @keyframes detection-pulse { - 0%, 100% { box-shadow: 0 0 12px rgba(0, 255, 80, 0.15); } - 50% { box-shadow: 0 0 22px rgba(0, 255, 80, 0.35); } + 0%, + 100% { + box-shadow: 0 0 12px rgba(0, 255, 80, 0.15); + } + 50% { + box-shadow: 0 0 22px rgba(0, 255, 80, 0.35); + } } /* ── Parameter Slider Panel ──────────────────── */ @@ -942,7 +989,10 @@ body.ui-clean-view #scene-runtime.active { opacity: 0; visibility: hidden; transform: translateY(6px); - transition: opacity var(--transition-smooth), visibility var(--transition-smooth), transform var(--transition-smooth); + transition: + opacity var(--transition-smooth), + visibility var(--transition-smooth), + transform var(--transition-smooth); } #param-slider-panel::-webkit-scrollbar { @@ -988,7 +1038,12 @@ body.ui-clean-view #scene-runtime.active { } #param-slider-panel:not(.collapsed) .param-panel-divider { - background: linear-gradient(90deg, rgb(0 212 255 / 28%), rgba(0, 212, 255, 0.18) 58%, transparent); + background: linear-gradient( + 90deg, + rgb(0 212 255 / 28%), + rgba(0, 212, 255, 0.18) 58%, + transparent + ); } .param-slider-row { @@ -1058,4 +1113,3 @@ body.ui-clean-view #scene-runtime.active { text-align: right; flex-shrink: 0; } - diff --git a/src/ui/styles/first-run.css b/src/ui/styles/first-run.css index d4467f74e..d91f24fbe 100644 --- a/src/ui/styles/first-run.css +++ b/src/ui/styles/first-run.css @@ -31,7 +31,9 @@ -webkit-backdrop-filter: blur(28px) saturate(1.25); opacity: 0; transform: translate(-50%, calc(-50% + 0.8rem)) scale(0.985); - transition: opacity 220ms ease, transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1); + transition: + opacity 220ms ease, + transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1); /* Inert until revealed AND again while fading out, so a click during the dismiss animation cannot launch a second mission behind the fade. */ pointer-events: none; @@ -55,7 +57,12 @@ position: absolute; inset: 0 0 auto; height: 2px; - background: linear-gradient(90deg, transparent, rgba(54, 220, 255, 0.8), transparent); + background: linear-gradient( + 90deg, + transparent, + rgba(54, 220, 255, 0.8), + transparent + ); box-shadow: 0 0 1rem rgba(54, 220, 255, 0.45); pointer-events: none; } @@ -73,7 +80,9 @@ text-transform: uppercase; } -.first-run-kicker { color: rgba(54, 220, 255, 0.86); } +.first-run-kicker { + color: rgba(54, 220, 255, 0.86); +} #first-run-launcher h2 { margin-top: 0.6rem; @@ -107,7 +116,9 @@ scrollbar-gutter: stable; } -.first-run-choices::-webkit-scrollbar { width: 6px; } +.first-run-choices::-webkit-scrollbar { + width: 6px; +} .first-run-choices::-webkit-scrollbar-thumb { background: rgba(54, 220, 255, 0.45); border-radius: 3px; @@ -127,7 +138,10 @@ border: 1px solid rgba(100, 200, 220, 0.18); border-radius: 0.42rem; cursor: pointer; - transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast); + transition: + background var(--transition-fast), + border-color var(--transition-fast), + transform var(--transition-fast); } .first-run-choices button:hover, @@ -173,7 +187,9 @@ .first-run-arrow { color: rgba(54, 220, 255, 0.4); font-size: 1rem; - transition: transform var(--transition-fast), color var(--transition-fast); + transition: + transform var(--transition-fast), + color var(--transition-fast); } .first-run-choices button:hover .first-run-arrow, @@ -212,7 +228,9 @@ cursor: pointer; } -.first-run-suppress:hover { color: rgba(225, 244, 248, 0.92); } +.first-run-suppress:hover { + color: rgba(225, 244, 248, 0.92); +} /* The live region carries mission progress and failures, so it owns a full row and reserves its height — otherwise the card resizes as it speaks. */ @@ -249,15 +267,31 @@ body.scene-playback-mode #first-run-launcher { transform: translate(-50%, 0.8rem) scale(0.985); } - #first-run-launcher.visible { transform: translate(-50%, 0) scale(1); } + #first-run-launcher.visible { + transform: translate(-50%, 0) scale(1); + } /* Five tiles have to clear the dock on a 375 x 812 phone, so the rows tighten and the redundant "ESC to dismiss" hint (no ESC key on a phone) drops. */ - #first-run-launcher h2 { margin-top: 0.45rem; } - #first-run-description { margin-top: 0.25rem; font-size: 0.72rem; } - .first-run-choices { margin-top: 0.75rem; } - .first-run-choices button { min-height: 3rem; padding: 0.45rem 0.6rem; } - .first-run-choices small { font-size: 0.62rem; } - .first-run-footer > span:last-child { display: none; } + #first-run-launcher h2 { + margin-top: 0.45rem; + } + #first-run-description { + margin-top: 0.25rem; + font-size: 0.72rem; + } + .first-run-choices { + margin-top: 0.75rem; + } + .first-run-choices button { + min-height: 3rem; + padding: 0.45rem 0.6rem; + } + .first-run-choices small { + font-size: 0.62rem; + } + .first-run-footer > span:last-child { + display: none; + } } /* Short viewports — landscape phones, small laptop windows. The chrome tightens @@ -266,15 +300,38 @@ body.scene-playback-mode #first-run-launcher { fits. (It was dropped here while there were five; the tile that went is exactly why it can come back.) */ @media (max-height: 620px) { - #first-run-launcher { padding: 0.75rem 0.85rem; } - #first-run-launcher h2 { margin-top: 0.3rem; font-size: 1.1rem; } - #first-run-description { margin-top: 0.2rem; font-size: 0.7rem; line-height: 1.4; } - .first-run-choices { margin-top: 0.55rem; } - .first-run-choices button { min-height: 2.7rem; padding: 0.36rem 0.6rem; } - .first-run-choices strong { font-size: 0.68rem; } - .first-run-choices small { font-size: 0.6rem; } - .first-run-footer { margin-top: 0.5rem; } - .first-run-note { margin-top: 0.3rem; min-height: 0; } + #first-run-launcher { + padding: 0.75rem 0.85rem; + } + #first-run-launcher h2 { + margin-top: 0.3rem; + font-size: 1.1rem; + } + #first-run-description { + margin-top: 0.2rem; + font-size: 0.7rem; + line-height: 1.4; + } + .first-run-choices { + margin-top: 0.55rem; + } + .first-run-choices button { + min-height: 2.7rem; + padding: 0.36rem 0.6rem; + } + .first-run-choices strong { + font-size: 0.68rem; + } + .first-run-choices small { + font-size: 0.6rem; + } + .first-run-footer { + margin-top: 0.5rem; + } + .first-run-note { + margin-top: 0.3rem; + min-height: 0; + } } /* A soft edge on the list, so a tile cut by the fold reads as "more below" @@ -283,8 +340,16 @@ body.scene-playback-mode #first-run-launcher { promise content that is not there, which is the opposite of the affordance it is meant to be. Decorative only: the list scrolls under it. */ .first-run-choices[data-scrollable='true'] { - -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.4rem), transparent 100%); - mask-image: linear-gradient(to bottom, #000 calc(100% - 1.4rem), transparent 100%); + -webkit-mask-image: linear-gradient( + to bottom, + #000 calc(100% - 1.4rem), + transparent 100% + ); + mask-image: linear-gradient( + to bottom, + #000 calc(100% - 1.4rem), + transparent 100% + ); } /* Scrolled to the end, there is nothing left to promise, so the fade lifts. @@ -308,8 +373,11 @@ body.scene-playback-mode #first-run-launcher { @media (prefers-reduced-motion: reduce) { #first-run-launcher, .first-run-choices button, - .first-run-arrow { transition: none; } + .first-run-arrow { + transition: none; + } /* Scrolling a tile into view must not animate either. */ - .first-run-choices { scroll-behavior: auto; } + .first-run-choices { + scroll-behavior: auto; + } } - diff --git a/src/ui/styles/foundation.css b/src/ui/styles/foundation.css index f7de3b17d..cbb5a5eac 100644 --- a/src/ui/styles/foundation.css +++ b/src/ui/styles/foundation.css @@ -69,7 +69,8 @@ outline-offset: 2px; } -html, body { +html, +body { width: 100%; height: 100%; overflow: hidden; @@ -185,7 +186,9 @@ body.cockpit-mode #cockpit-cloud-effects.active { pointer-events: none; opacity: 0; visibility: hidden; - transition: opacity 300ms ease, visibility 300ms ease; + transition: + opacity 300ms ease, + visibility 300ms ease; } .celestial-ring-overlay.visible { @@ -222,19 +225,27 @@ body.cockpit-mode #cockpit-cloud-effects.active { font-weight: 400; font-style: normal; line-height: 1; - font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; + font-variation-settings: + 'FILL' 0, + 'wght' 400, + 'GRAD' 0, + 'opsz' 20; transition: opacity 120ms linear; user-select: none; } .celestial-sun { color: rgba(231, 194, 72, 0.92); - text-shadow: 0 0 7px rgba(231, 194, 72, 0.72), 0 0 18px rgba(231, 194, 72, 0.3); + text-shadow: + 0 0 7px rgba(231, 194, 72, 0.72), + 0 0 18px rgba(231, 194, 72, 0.3); } .celestial-moon { color: rgba(55, 211, 239, 0.84); - text-shadow: 0 0 7px rgba(55, 211, 239, 0.65), 0 0 17px rgba(55, 211, 239, 0.25); + text-shadow: + 0 0 7px rgba(55, 211, 239, 0.65), + 0 0 17px rgba(55, 211, 239, 0.25); } #celestial-toggle:disabled { @@ -499,7 +510,7 @@ body.cockpit-mode #cockpit-cloud-effects.active { #title-bar .title-logo::before, #title-bar .title-logo::after { - --radio-broadcast-opacity: .28; + --radio-broadcast-opacity: 0.28; content: ''; position: absolute; z-index: 1; @@ -517,7 +528,7 @@ body.cockpit-mode #cockpit-cloud-effects.active { } #title-bar .title-logo::after { - --radio-broadcast-opacity: .17; + --radio-broadcast-opacity: 0.17; top: -15px; width: 27px; height: 13px; @@ -529,12 +540,15 @@ body.cockpit-mode #cockpit-cloud-effects.active { animation: title-radio-broadcast 1.8s ease-in-out infinite; } -#title-bar.radio-broadcasting .title-logo::after { animation-delay: .28s; } +#title-bar.radio-broadcasting .title-logo::after { + animation-delay: 0.28s; +} @keyframes title-radio-broadcast { - 0%, 100% { - opacity: calc(var(--radio-broadcast-opacity) * .55); - transform: translateX(-50%) scale(.94); + 0%, + 100% { + opacity: calc(var(--radio-broadcast-opacity) * 0.55); + transform: translateX(-50%) scale(0.94); } 50% { opacity: var(--radio-broadcast-opacity); @@ -608,4 +622,3 @@ body.cockpit-mode #cockpit-cloud-effects.active { text-shadow: 0 0 20px var(--accent-glow); transition: var(--transition-smooth); } - diff --git a/src/ui/styles/layers.css b/src/ui/styles/layers.css index f661b1e45..87153ebd5 100644 --- a/src/ui/styles/layers.css +++ b/src/ui/styles/layers.css @@ -11,7 +11,9 @@ align-items: flex-start; gap: 8px; pointer-events: none; - transition: top var(--transition-fast), bottom var(--transition-fast); + transition: + top var(--transition-fast), + bottom var(--transition-fast); } #data-panel { @@ -26,7 +28,10 @@ opacity: 0; visibility: hidden; transform: translateY(8px); - transition: opacity var(--transition-smooth), visibility var(--transition-smooth), transform var(--transition-smooth); + transition: + opacity var(--transition-smooth), + visibility var(--transition-smooth), + transform var(--transition-smooth); } #data-panel.active { @@ -38,7 +43,11 @@ #data-panel .panel-glow { position: absolute; inset: -20px; - background: radial-gradient(ellipse at center, var(--accent-dim) 0%, transparent 60%); + background: radial-gradient( + ellipse at center, + var(--accent-dim) 0%, + transparent 60% + ); filter: blur(20px); opacity: 0.25; pointer-events: none; @@ -56,7 +65,10 @@ 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.03) inset, 0 1px 0 rgba(255, 255, 255, 0.06) inset; - max-height: min(620px, calc(100vh - var(--left-stack-top) - var(--left-stack-gap) - 36px)); + max-height: min( + 620px, + calc(100vh - var(--left-stack-top) - var(--left-stack-gap) - 36px) + ); display: flex; flex-direction: column; min-height: 0; @@ -87,7 +99,11 @@ } .data-toggle-list::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, rgba(0, 212, 255, 0.65), rgba(0, 212, 255, 0.2)); + background: linear-gradient( + 180deg, + rgba(0, 212, 255, 0.65), + rgba(0, 212, 255, 0.2) + ); border-radius: 999px; } @@ -123,7 +139,9 @@ align-items: flex-end; gap: 8px; pointer-events: none; - transition: top var(--transition-fast), max-height var(--transition-fast); + transition: + top var(--transition-fast), + max-height var(--transition-fast); } #right-context-rail > #pp-toggles, #right-context-rail > #cctv-panel, @@ -156,7 +174,9 @@ } #right-context-rail > #pp-toggles.collapsed .pp-header-row, #right-context-rail #cctv-panel.collapsed .cctv-panel-inner, -#right-context-rail #global-context-panel.collapsed .global-context-panel-inner { +#right-context-rail + #global-context-panel.collapsed + .global-context-panel-inner { height: 50px; min-height: 50px; box-sizing: border-box; @@ -166,8 +186,12 @@ overflow-y: auto; scrollbar-gutter: stable; } -#right-context-rail .global-context-panel-inner { max-height: 100%; } -#right-context-rail #global-context-panel:not(.collapsed) .global-context-panel-inner { +#right-context-rail .global-context-panel-inner { + max-height: 100%; +} +#right-context-rail + #global-context-panel:not(.collapsed) + .global-context-panel-inner { max-height: 100%; overflow-y: auto; scrollbar-gutter: stable; @@ -178,7 +202,9 @@ stranded at the rail's expanded-width origin. */ margin-left: auto; } -#right-context-rail #global-context-panel.collapsed .global-context-panel-inner { +#right-context-rail + #global-context-panel.collapsed + .global-context-panel-inner { min-height: 42px; box-sizing: border-box; justify-content: center; @@ -215,8 +241,12 @@ scrollbar-width: thin; scrollbar-color: rgba(0, 212, 255, 0.4) transparent; } -#right-context-rail.layout-focus > #cctv-panel:not(.collapsed) .cctv-panel-inner, -#right-context-rail.layout-focus > #global-context-panel:not(.collapsed) .global-context-panel-inner { +#right-context-rail.layout-focus + > #cctv-panel:not(.collapsed) + .cctv-panel-inner, +#right-context-rail.layout-focus + > #global-context-panel:not(.collapsed) + .global-context-panel-inner { height: 100%; box-sizing: border-box; } @@ -241,8 +271,10 @@ margin-top: 8px; pointer-events: auto; } -#space-mission-panel-host:has(#space-mission-panel:not([hidden])) > .context-mode-standby, -#space-mission-panel-host:has(#space-mission-panel:not([hidden])) > .space-mission-roster { +#space-mission-panel-host:has(#space-mission-panel:not([hidden])) + > .context-mode-standby, +#space-mission-panel-host:has(#space-mission-panel:not([hidden])) + > .space-mission-roster { display: none; } #space-mission-panel-host .space-mission-roster { @@ -250,9 +282,9 @@ min-height: 0; gap: 8px; padding: 9px; - border: 1px solid rgba(98, 181, 255, .2); + border: 1px solid rgba(98, 181, 255, 0.2); border-radius: 7px; - background: rgba(4, 17, 31, .28); + background: rgba(4, 17, 31, 0.28); } #space-mission-panel-host .space-mission-roster-header { display: flex; @@ -260,7 +292,7 @@ justify-content: space-between; gap: 10px; padding: 1px 3px 7px; - border-bottom: 1px solid rgba(34, 230, 230, .14); + border-bottom: 1px solid rgba(34, 230, 230, 0.14); font-family: 'JetBrains Mono', monospace; } #space-mission-panel-host .space-mission-roster-header > div { @@ -271,18 +303,18 @@ #space-mission-panel-host .space-mission-roster-header strong { color: #8fcfff; font-size: 9px; - letter-spacing: .16em; + letter-spacing: 0.16em; } #space-mission-panel-host .space-mission-roster-header span { - color: rgba(190, 230, 230, .46); + color: rgba(190, 230, 230, 0.46); font-size: 7px; - letter-spacing: .1em; + letter-spacing: 0.1em; } #space-mission-panel-host .space-mission-roster-header output { flex: 0 0 auto; color: #22e6e6; font-size: 9px; - letter-spacing: .08em; + letter-spacing: 0.08em; } #space-mission-panel-host .space-mission-roster-list { display: grid; @@ -291,7 +323,7 @@ overflow-x: hidden; overflow-y: auto; padding-right: 3px; - scrollbar-color: rgba(34, 230, 230, .38) transparent; + scrollbar-color: rgba(34, 230, 230, 0.38) transparent; scrollbar-width: thin; } #space-mission-panel-host .space-mission-roster-item { @@ -303,14 +335,17 @@ width: 100%; min-height: 42px; padding: 7px 7px; - border: 1px solid rgba(98, 181, 255, .13); + border: 1px solid rgba(98, 181, 255, 0.13); border-radius: 4px; color: #d8ffff; - background: rgba(42, 113, 171, .045); + background: rgba(42, 113, 171, 0.045); font-family: 'JetBrains Mono', monospace; text-align: left; cursor: pointer; - transition: border-color .15s ease, background .15s ease, transform .15s ease; + transition: + border-color 0.15s ease, + background 0.15s ease, + transform 0.15s ease; } #space-mission-panel-host .space-mission-roster-item::before { content: ''; @@ -327,15 +362,15 @@ linear-gradient(#22e6e6, #22e6e6) left bottom / 1px 9px no-repeat, linear-gradient(#22e6e6, #22e6e6) right bottom / 9px 1px no-repeat, linear-gradient(#22e6e6, #22e6e6) right bottom / 1px 9px no-repeat; - filter: drop-shadow(0 0 4px rgba(34, 230, 230, .45)); + filter: drop-shadow(0 0 4px rgba(34, 230, 230, 0.45)); pointer-events: none; - transition: opacity .12s ease; + transition: opacity 0.12s ease; } #space-mission-panel-host .space-mission-roster-item:hover, #space-mission-panel-host .space-mission-roster-item:focus-visible { - border-color: rgba(34, 230, 230, .58); + border-color: rgba(34, 230, 230, 0.58); outline: none; - background: rgba(34, 230, 230, .09); + background: rgba(34, 230, 230, 0.09); transform: translateX(2px); } #space-mission-panel-host .space-mission-roster-item:hover::before, @@ -352,7 +387,8 @@ justify-self: center; border-radius: 50%; background: var(--mission-roster-color, #22e6e6); - box-shadow: 0 0 7px color-mix(in srgb, var(--mission-roster-color, #22e6e6) 70%, transparent); + box-shadow: 0 0 7px + color-mix(in srgb, var(--mission-roster-color, #22e6e6) 70%, transparent); } #space-mission-panel-host .space-mission-roster-copy { display: grid; @@ -369,31 +405,35 @@ color: #d8ffff; font-size: 9px; font-weight: 500; - letter-spacing: .07em; + letter-spacing: 0.07em; } #space-mission-panel-host .space-mission-roster-copy small { - color: rgba(190, 230, 230, .48); + color: rgba(190, 230, 230, 0.48); font-size: 7px; - letter-spacing: .035em; + letter-spacing: 0.035em; } #space-mission-panel-host .space-mission-roster-chevron { - color: rgba(34, 230, 230, .54); + color: rgba(34, 230, 230, 0.54); font-size: 16px; line-height: 1; } #space-mission-panel-host .space-mission-roster-empty { padding: 18px 10px; - color: rgba(190, 230, 230, .48); - font: 8px/1.5 'JetBrains Mono', monospace; - letter-spacing: .09em; + color: rgba(190, 230, 230, 0.48); + font: + 8px/1.5 'JetBrains Mono', + monospace; + letter-spacing: 0.09em; text-align: center; } #space-mission-panel-host .space-mission-roster-focus-continuation { margin: 0; padding: 3px; - color: rgba(190, 230, 230, .58); - font: 7px/1.4 'JetBrains Mono', monospace; - letter-spacing: .08em; + color: rgba(190, 230, 230, 0.58); + font: + 7px/1.4 'JetBrains Mono', + monospace; + letter-spacing: 0.08em; text-align: center; } #space-mission-panel .space-mission-view-header { @@ -401,18 +441,20 @@ align-items: center; justify-content: space-between; padding: 8px 0 2px; - border-top: 1px solid rgba(34, 230, 230, .18); - color: rgba(190, 230, 230, .58); - font: 8px 'JetBrains Mono', monospace; - letter-spacing: .18em; + border-top: 1px solid rgba(34, 230, 230, 0.18); + color: rgba(190, 230, 230, 0.58); + font: + 8px 'JetBrains Mono', + monospace; + letter-spacing: 0.18em; } #space-mission-panel .space-mission-view-header button { width: 24px; height: 24px; - border: 1px solid rgba(98, 181, 255, .24); + border: 1px solid rgba(98, 181, 255, 0.24); border-radius: 5px; - color: rgba(190, 230, 230, .7); - background: rgba(42, 113, 171, .08); + color: rgba(190, 230, 230, 0.7); + background: rgba(42, 113, 171, 0.08); cursor: pointer; } #space-mission-panel .space-mission-view-header button:hover { @@ -424,58 +466,77 @@ gap: 8px; padding: 12px 0; color: var(--hud-cyan, #22e6e6); - font: 11px 'JetBrains Mono', monospace; - letter-spacing: .08em; + font: + 11px 'JetBrains Mono', + monospace; + letter-spacing: 0.08em; +} +#space-mission-panel .space-mission-detail strong { + font-size: 14px; +} +#space-mission-panel .space-mission-detail span { + color: rgba(190, 230, 230, 0.7); +} +#space-mission-panel .space-mission-detail b { + color: #d8ffff; + font-weight: 400; } -#space-mission-panel .space-mission-detail strong { font-size: 14px; } -#space-mission-panel .space-mission-detail span { color: rgba(190, 230, 230, .7); } -#space-mission-panel .space-mission-detail b { color: #d8ffff; font-weight: 400; } #space-mission-panel .mission-data-section { margin: 0 0 11px; - border-top: 1px solid rgba(34, 230, 230, .18); + border-top: 1px solid rgba(34, 230, 230, 0.18); padding-top: 9px; } #space-mission-panel .mission-data-section h4 { margin: 0 0 6px; color: #22e6e6; - font: 10px 'JetBrains Mono', monospace; - letter-spacing: .17em; + font: + 10px 'JetBrains Mono', + monospace; + letter-spacing: 0.17em; +} +#space-mission-panel .mission-table-scroll { + overflow-x: auto; } -#space-mission-panel .mission-table-scroll { overflow-x: auto; } #space-mission-panel .mission-data-table { width: 100%; border-collapse: collapse; table-layout: fixed; color: #d8ffff; - font: 9px 'JetBrains Mono', monospace; - letter-spacing: .025em; + font: + 9px 'JetBrains Mono', + monospace; + letter-spacing: 0.025em; } #space-mission-panel .mission-data-table th { padding: 4px 5px; - border-bottom: 1px solid rgba(34, 230, 230, .22); - color: rgba(190, 230, 230, .58); + border-bottom: 1px solid rgba(34, 230, 230, 0.22); + color: rgba(190, 230, 230, 0.58); font-weight: 400; text-align: left; } #space-mission-panel .mission-data-table td { overflow-wrap: anywhere; padding: 6px 5px; - border-bottom: 1px solid rgba(34, 230, 230, .1); + border-bottom: 1px solid rgba(34, 230, 230, 0.1); vertical-align: top; } #space-mission-panel .mission-data-table th:nth-child(1), -#space-mission-panel .mission-data-table td:nth-child(1) { width: 34%; } +#space-mission-panel .mission-data-table td:nth-child(1) { + width: 34%; +} #space-mission-panel .mission-data-table th:nth-child(2), -#space-mission-panel .mission-data-table td:nth-child(2) { width: 25%; } +#space-mission-panel .mission-data-table td:nth-child(2) { + width: 25%; +} #space-mission-panel .mission-data-table small { display: block; margin-top: 3px; - color: rgba(190, 230, 230, .56); + color: rgba(190, 230, 230, 0.56); font-size: 8px; line-height: 1.35; } #space-mission-panel .mission-table-empty { - color: rgba(190, 230, 230, .56); + color: rgba(190, 230, 230, 0.56); text-align: center; } #space-mission-panel .mission-replay-speed-control { @@ -483,18 +544,22 @@ gap: 5px; margin: 1px 0 10px; padding: 8px 10px 6px; - border: 1px solid rgba(34, 230, 230, .2); + border: 1px solid rgba(34, 230, 230, 0.2); border-radius: 5px; - background: rgba(3, 21, 31, .36); + background: rgba(3, 21, 31, 0.36); +} +#space-mission-panel .mission-replay-speed-control[hidden] { + display: none; } -#space-mission-panel .mission-replay-speed-control[hidden] { display: none; } #space-mission-panel .mission-replay-speed-header { display: flex; align-items: center; justify-content: space-between; - color: rgba(190, 230, 230, .68); - font: 8px 'JetBrains Mono', monospace; - letter-spacing: .16em; + color: rgba(190, 230, 230, 0.68); + font: + 8px 'JetBrains Mono', + monospace; + letter-spacing: 0.16em; } #space-mission-panel .mission-replay-speed-header output { min-width: 42px; @@ -506,9 +571,11 @@ #space-mission-panel .mission-replay-speed-scale { display: flex; justify-content: space-between; - color: rgba(190, 230, 230, .42); - font: 7px 'JetBrains Mono', monospace; - letter-spacing: .08em; + color: rgba(190, 230, 230, 0.42); + font: + 7px 'JetBrains Mono', + monospace; + letter-spacing: 0.08em; } #space-mission-panel .mission-action-row { @@ -521,19 +588,29 @@ flex: 1; min-width: 0; padding: 9px 12px; - border: 1px solid rgba(255, 159, 67, .62); + border: 1px solid rgba(255, 159, 67, 0.62); border-radius: 5px; color: #ffc078; - background: linear-gradient(90deg, rgba(255, 159, 67, .08), rgba(34, 230, 230, .08)); - font: 9px 'JetBrains Mono', monospace; - letter-spacing: .16em; + background: linear-gradient( + 90deg, + rgba(255, 159, 67, 0.08), + rgba(34, 230, 230, 0.08) + ); + font: + 9px 'JetBrains Mono', + monospace; + letter-spacing: 0.16em; cursor: pointer; - transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; + transition: + border-color 0.15s ease, + color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; } #space-mission-panel .mission-focus-button { - border-color: rgba(34, 230, 230, .58); + border-color: rgba(34, 230, 230, 0.58); color: #22e6e6; - background: rgba(34, 230, 230, .07); + background: rgba(34, 230, 230, 0.07); } #space-mission-panel .mission-replay-button:hover, #space-mission-panel .mission-replay-button:focus-visible, @@ -542,18 +619,18 @@ border-color: #ffb45f; color: #fff; outline: none; - box-shadow: 0 0 14px rgba(255, 159, 67, .14); + box-shadow: 0 0 14px rgba(255, 159, 67, 0.14); } #space-mission-panel .mission-focus-button:hover, #space-mission-panel .mission-focus-button:focus-visible { border-color: #22e6e6; - box-shadow: 0 0 14px rgba(34, 230, 230, .14); + box-shadow: 0 0 14px rgba(34, 230, 230, 0.14); } #space-mission-panel .mission-replay-button.active { border-color: #22e6e6; color: #d8ffff; - background: rgba(34, 230, 230, .13); - box-shadow: inset 0 0 16px rgba(34, 230, 230, .08); + background: rgba(34, 230, 230, 0.13); + box-shadow: inset 0 0 16px rgba(34, 230, 230, 0.08); } #space-mission-panel .mission-replay-transport { flex: 1; @@ -562,62 +639,104 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 5px; } -#space-mission-panel .mission-replay-transport[hidden] { display: none; } +#space-mission-panel .mission-replay-transport[hidden] { + display: none; +} #space-mission-panel .mission-replay-transport button { min-width: 0; min-height: 34px; padding: 0; - border: 1px solid rgba(34, 230, 230, .5); + border: 1px solid rgba(34, 230, 230, 0.5); border-radius: 4px; color: #22e6e6; - background: rgba(34, 230, 230, .08); - font: 15px/1 'JetBrains Mono', monospace; + background: rgba(34, 230, 230, 0.08); + font: + 15px/1 'JetBrains Mono', + monospace; cursor: pointer; - transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease, opacity .15s ease; + transition: + border-color 0.15s ease, + color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease, + opacity 0.15s ease; } #space-mission-panel .mission-replay-transport button:hover:not(:disabled), #space-mission-panel .mission-replay-transport button:focus-visible { border-color: #d8ffff; color: #fff; outline: none; - background: rgba(34, 230, 230, .16); - box-shadow: 0 0 12px rgba(34, 230, 230, .14); + background: rgba(34, 230, 230, 0.16); + box-shadow: 0 0 12px rgba(34, 230, 230, 0.14); } #space-mission-panel .mission-replay-transport button:disabled { - opacity: .28; + opacity: 0.28; cursor: default; } #space-mission-panel .mission-replay-transport button.cancel { - border-color: rgba(255, 159, 67, .58); + border-color: rgba(255, 159, 67, 0.58); color: #ffb15c; - background: rgba(255, 159, 67, .08); + background: rgba(255, 159, 67, 0.08); } -#space-mission-panel .mission-replay-transport.is-paused [data-mission-replay-play] { +#space-mission-panel + .mission-replay-transport.is-paused + [data-mission-replay-play] { border-color: #22e6e6; color: #d8ffff; - background: rgba(34, 230, 230, .18); - box-shadow: inset 0 0 12px rgba(34, 230, 230, .1); + background: rgba(34, 230, 230, 0.18); + box-shadow: inset 0 0 12px rgba(34, 230, 230, 0.1); +} +#space-mission-panel .space-mission-nav { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; } -#space-mission-panel .space-mission-nav { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; } #space-mission-panel .mission-nav-button { min-width: 94px; padding: 7px 10px; - border: 1px solid rgba(34, 230, 230, .55); - background: rgba(8, 34, 42, .72); + border: 1px solid rgba(34, 230, 230, 0.55); + background: rgba(8, 34, 42, 0.72); color: #22e6e6; - font: 11px 'JetBrains Mono', monospace; - letter-spacing: .12em; + font: + 11px 'JetBrains Mono', + monospace; + letter-spacing: 0.12em; + cursor: pointer; + transition: + background 0.15s ease, + border-color 0.15s ease, + opacity 0.15s ease; +} +#space-mission-panel .mission-nav-button:hover:not(:disabled) { + background: rgba(34, 230, 230, 0.18); + border-color: #22e6e6; +} +#space-mission-panel .mission-nav-button:disabled { + cursor: not-allowed; + opacity: 0.28; +} +#space-mission-panel .mission-nav-button span { + font-size: 18px; + line-height: 0; + vertical-align: -2px; +} +#space-mission-panel .mission-nav-index { + min-width: 58px; + text-align: center; + color: #d8ffff; + font: + 11px 'JetBrains Mono', + monospace; + letter-spacing: 0.1em; +} +#space-mission-panel button { cursor: pointer; - transition: background .15s ease, border-color .15s ease, opacity .15s ease; } -#space-mission-panel .mission-nav-button:hover:not(:disabled) { background: rgba(34, 230, 230, .18); border-color: #22e6e6; } -#space-mission-panel .mission-nav-button:disabled { cursor: not-allowed; opacity: .28; } -#space-mission-panel .mission-nav-button span { font-size: 18px; line-height: 0; vertical-align: -2px; } -#space-mission-panel .mission-nav-index { min-width: 58px; text-align: center; color: #d8ffff; font: 11px 'JetBrains Mono', monospace; letter-spacing: .1em; } -#space-mission-panel button { cursor: pointer; } .mission-replay-vehicle-overlay { - --mission-replay-vehicle-scale: .50; + --mission-replay-vehicle-scale: 0.5; position: absolute; top: 0; left: 0; @@ -631,7 +750,8 @@ .mission-replay-flight-symbol { position: absolute; inset: 0; - transform: rotate(var(--mission-replay-rotation, 0deg)) scale(var(--mission-replay-vehicle-scale)); + transform: rotate(var(--mission-replay-rotation, 0deg)) + scale(var(--mission-replay-vehicle-scale)); transform-origin: 46px 69px; will-change: transform; } @@ -645,10 +765,11 @@ height: 8px; border-radius: 50%; background: #22e6e6; - box-shadow: 0 0 7px rgba(34, 230, 230, .78); + box-shadow: 0 0 7px rgba(34, 230, 230, 0.78); } -.mission-replay-vehicle-overlay[data-mode='orbit'] .mission-replay-flight-symbol { +.mission-replay-vehicle-overlay[data-mode='orbit'] + .mission-replay-flight-symbol { display: none; } @@ -663,7 +784,7 @@ width: 48px; height: 72px; overflow: visible; - filter: drop-shadow(0 0 6px rgba(34, 230, 230, .66)); + filter: drop-shadow(0 0 6px rgba(34, 230, 230, 0.66)); } .mission-replay-rocket-body { @@ -690,7 +811,7 @@ width: 72px; height: 72px; overflow: visible; - filter: drop-shadow(0 0 4px rgba(34, 230, 230, .9)); + filter: drop-shadow(0 0 4px rgba(34, 230, 230, 0.9)); } .mission-replay-vehicle-overlay.is-thrusting .mission-replay-thrust { @@ -699,16 +820,16 @@ .mission-replay-thrust ellipse { fill: none; - stroke: rgba(34, 230, 230, .98); + stroke: rgba(34, 230, 230, 0.98); stroke-width: 2; opacity: 0; vector-effect: non-scaling-stroke; - animation: mission-thrust-wave .9s ease-in-out infinite; - animation-delay: calc(var(--thrust-index) * .15s); + animation: mission-thrust-wave 0.9s ease-in-out infinite; + animation-delay: calc(var(--thrust-index) * 0.15s); } .mission-replay-thrust ellipse:nth-child(2n) { - stroke: rgba(216, 255, 255, .78); + stroke: rgba(216, 255, 255, 0.78); } .mission-replay-vehicle-overlay.is-paused .mission-replay-thrust ellipse { @@ -721,9 +842,13 @@ left: 70px; min-width: 188px; color: #fff; - font: 9px/1.38 'JetBrains Mono', monospace; - letter-spacing: .075em; - text-shadow: 0 1px 2px #001018, 0 0 6px #001018; + font: + 9px/1.38 'JetBrains Mono', + monospace; + letter-spacing: 0.075em; + text-shadow: + 0 1px 2px #001018, + 0 0 6px #001018; white-space: nowrap; } @@ -739,19 +864,28 @@ } .mission-replay-overlay-callout span { - color: rgba(255, 255, 255, .92); + color: rgba(255, 255, 255, 0.92); white-space: pre-line; } -.mission-replay-vehicle-overlay[data-mode='orbit'] .mission-replay-overlay-callout { +.mission-replay-vehicle-overlay[data-mode='orbit'] + .mission-replay-overlay-callout { top: 55px; left: 58px; } @keyframes mission-thrust-wave { - 0%, 18% { opacity: 0; } - 24% { opacity: 1; } - 32%, 100% { opacity: 0; } + 0%, + 18% { + opacity: 0; + } + 24% { + opacity: 1; + } + 32%, + 100% { + opacity: 0; + } } @media (prefers-reduced-motion: reduce) { @@ -762,7 +896,11 @@ #global-context-panel .panel-glow { position: absolute; inset: -20px; - background: radial-gradient(ellipse at center, rgba(98, 181, 255, 0.2) 0%, transparent 62%); + background: radial-gradient( + ellipse at center, + rgba(98, 181, 255, 0.2) 0%, + transparent 62% + ); filter: blur(20px); opacity: 0.24; pointer-events: none; @@ -776,7 +914,9 @@ border: 1px solid var(--glass-border); border-radius: var(--panel-radius); background: var(--glass-bg); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.05); backdrop-filter: blur(24px) saturate(1.4); -webkit-backdrop-filter: blur(24px) saturate(1.4); } @@ -791,14 +931,20 @@ height: 28px; place-items: center; padding: 0; - border: 1px solid rgba(98, 181, 255, .22); + border: 1px solid rgba(98, 181, 255, 0.22); border-radius: 6px; - color: rgba(143, 207, 255, .58); - background: rgba(42, 113, 171, .08); + color: rgba(143, 207, 255, 0.58); + background: rgba(42, 113, 171, 0.08); cursor: pointer; - transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; + transition: + border-color 0.15s ease, + color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; +} +.context-radio-toggle .material-symbols-outlined { + font-size: 17px; } -.context-radio-toggle .material-symbols-outlined { font-size: 17px; } .context-radio-toggle:hover, .context-radio-toggle:focus-visible { border-color: #79c2ff; @@ -806,12 +952,17 @@ outline: none; } .context-radio-toggle.active { - border-color: rgba(101, 247, 255, .82); + border-color: rgba(101, 247, 255, 0.82); color: #eaffff; - background: rgba(0, 212, 255, .16); - box-shadow: inset 0 0 12px rgba(0, 212, 255, .1), 0 0 12px rgba(0, 212, 255, .2); + background: rgba(0, 212, 255, 0.16); + box-shadow: + inset 0 0 12px rgba(0, 212, 255, 0.1), + 0 0 12px rgba(0, 212, 255, 0.2); +} +.context-radio-toggle:disabled { + opacity: 0.5; + cursor: wait; } -.context-radio-toggle:disabled { opacity: .5; cursor: wait; } .context-radio-mini { position: absolute; z-index: 5; @@ -822,14 +973,18 @@ box-sizing: border-box; gap: 8px; padding: 10px; - border: 1px solid rgba(65, 220, 231, .28); + border: 1px solid rgba(65, 220, 231, 0.28); border-radius: 8px; - background: rgba(2, 15, 20, .96); - box-shadow: 0 12px 32px rgba(0, 0, 0, .62), inset 0 1px 0 rgba(255, 255, 255, .04); + background: rgba(2, 15, 20, 0.96); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.62), + inset 0 1px 0 rgba(255, 255, 255, 0.04); backdrop-filter: blur(22px) saturate(1.35); -webkit-backdrop-filter: blur(22px) saturate(1.35); } -.context-radio-mini[hidden] { display: none !important; } +.context-radio-mini[hidden] { + display: none !important; +} .context-radio-dock.disclosure-open .context-radio-mini { display: grid; } @@ -840,7 +995,7 @@ align-items: center; color: #65f7ff; font: 7px/1.2 var(--font-mono); - letter-spacing: .12em; + letter-spacing: 0.12em; } .context-radio-mini-heading button { display: grid; @@ -848,10 +1003,10 @@ height: 26px; place-items: center; padding: 0; - border: 1px solid rgba(65, 220, 231, .28); + border: 1px solid rgba(65, 220, 231, 0.28); border-radius: 5px; color: #8feaf2; - background: rgba(14, 69, 78, .16); + background: rgba(14, 69, 78, 0.16); cursor: pointer; } .context-radio-mini-heading button:hover, @@ -864,8 +1019,12 @@ display: flex; gap: 5px; } -.context-radio-mini-heading .material-symbols-outlined { font-size: 16px; } -.context-radio-mini > .panel-layer-toggle { width: 100%; } +.context-radio-mini-heading .material-symbols-outlined { + font-size: 16px; +} +.context-radio-mini > .panel-layer-toggle { + width: 100%; +} .context-radio-mini-heading strong { overflow: hidden; color: #e8feff; @@ -883,10 +1042,10 @@ .context-radio-mini-transport button { min-height: 28px; padding: 5px; - border: 1px solid rgba(65, 220, 231, .28); + border: 1px solid rgba(65, 220, 231, 0.28); border-radius: 5px; color: #8feaf2; - background: rgba(14, 69, 78, .16); + background: rgba(14, 69, 78, 0.16); font: 8px/1 var(--font-mono); cursor: pointer; } @@ -897,7 +1056,10 @@ color: #fff; outline: none; } -.context-radio-mini-transport button:disabled { opacity: .38; cursor: not-allowed; } +.context-radio-mini-transport button:disabled { + opacity: 0.38; + cursor: not-allowed; +} .context-radio-mini-volume { display: grid; grid-template-columns: auto minmax(0, 1fr) 30px; @@ -905,22 +1067,36 @@ align-items: center; color: var(--text-dim); font: 7px/1 var(--font-mono); - letter-spacing: .08em; + letter-spacing: 0.08em; +} +.context-radio-mini-volume input { + width: 100%; } -.context-radio-mini-volume input { width: 100%; } .global-context-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 6px; margin: 0 0 8px; } -.global-context-actions:has(#installations-search-btn) { grid-template-columns: minmax(0, 1fr); } -.global-context-actions:has(#cockpit-entry:not([hidden])) { grid-template-columns: repeat(2, minmax(0, 1fr)); } -.global-context-actions:has(#cockpit-entry[hidden]) { grid-template-columns: 1fr; } +.global-context-actions:has(#installations-search-btn) { + grid-template-columns: minmax(0, 1fr); +} +.global-context-actions:has(#cockpit-entry:not([hidden])) { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.global-context-actions:has(#cockpit-entry[hidden]) { + grid-template-columns: 1fr; +} /* TR-3B reclassify toggle rides an `auto` trailing column so the chip never resizes the COCKPIT / SEARCH buttons beside it. */ -.global-context-actions:has(#tr3b-toggle:not([hidden])) { grid-template-columns: repeat(2, minmax(0, 1fr)) auto; } -.global-context-actions:has(#cockpit-entry[hidden]):has(#tr3b-toggle:not([hidden])) { grid-template-columns: minmax(0, 1fr) auto; } +.global-context-actions:has(#tr3b-toggle:not([hidden])) { + grid-template-columns: repeat(2, minmax(0, 1fr)) auto; +} +.global-context-actions:has(#cockpit-entry[hidden]):has( + #tr3b-toggle:not([hidden]) + ) { + grid-template-columns: minmax(0, 1fr) auto; +} .tr3b-toggle { width: 26px; min-height: 30px; @@ -931,19 +1107,29 @@ /* The saucer is a COLOR-font glyph: `color` and `text-shadow` cannot touch it, so the dim/lit states are carried by `filter` alongside the border and fill. 14px sits inside the 26px chip with breathing room. */ - font: 14px/1 ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 14px/1 ui-monospace, + SFMono-Regular, + Menlo, + monospace; filter: grayscale(0.6) opacity(0.62); cursor: pointer; - transition: filter 160ms ease, border-color 160ms ease, background 160ms ease; + transition: + filter 160ms ease, + border-color 160ms ease, + background 160ms ease; } -.tr3b-toggle[hidden] { display: none; } -.tr3b-toggle:hover, .tr3b-toggle:focus-visible { +.tr3b-toggle[hidden] { + display: none; +} +.tr3b-toggle:hover, +.tr3b-toggle:focus-visible { border-color: rgba(127, 200, 255, 0.6); background: rgba(66, 148, 211, 0.18); filter: grayscale(0.15) opacity(0.95); outline: none; } -.tr3b-toggle[aria-pressed="true"] { +.tr3b-toggle[aria-pressed='true'] { border-color: rgba(190, 220, 255, 0.75); background: rgba(20, 23, 28, 0.9); /* Active: full-colour saucer with a soft halo. drop-shadow works on an emoji @@ -965,16 +1151,22 @@ justify-content: center; gap: 6px; padding: 7px 8px; - border: 1px solid rgba(98, 181, 255, .28); + border: 1px solid rgba(98, 181, 255, 0.28); border-radius: 6px; color: #8fcfff; - background: rgba(42, 113, 171, .09); + background: rgba(42, 113, 171, 0.09); font: 8px/1.15 var(--font-mono); - letter-spacing: .09em; + letter-spacing: 0.09em; cursor: pointer; - transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; + transition: + border-color 0.15s ease, + color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; +} +.context-mode-button .material-symbols-outlined { + font-size: 16px; } -.context-mode-button .material-symbols-outlined { font-size: 16px; } .context-mode-button:hover, .context-mode-button:focus-visible { border-color: #79c2ff; @@ -988,33 +1180,42 @@ } .context-mode-button.active { - border-color: rgba(0, 212, 255, .75); + border-color: rgba(0, 212, 255, 0.75); color: #eaffff; - background: rgba(0, 212, 255, .14); - box-shadow: inset 0 0 16px rgba(0, 212, 255, .08), 0 0 12px rgba(0, 212, 255, .12); + background: rgba(0, 212, 255, 0.14); + box-shadow: + inset 0 0 16px rgba(0, 212, 255, 0.08), + 0 0 12px rgba(0, 212, 255, 0.12); +} +.context-mode-button:disabled { + opacity: 0.55; + cursor: wait; } -.context-mode-button:disabled { opacity: .55; cursor: wait; } .context-mode-view[hidden], -.context-mode-standby[hidden] { display: none !important; } +.context-mode-standby[hidden] { + display: none !important; +} .context-mode-standby { display: grid; min-height: 72px; place-content: center; gap: 6px; padding: 12px; - border: 1px dashed rgba(98, 181, 255, .2); + border: 1px dashed rgba(98, 181, 255, 0.2); border-radius: 7px; - color: rgba(190, 230, 230, .5); + color: rgba(190, 230, 230, 0.5); text-align: center; font: 8px/1.45 var(--font-mono); - letter-spacing: .1em; + letter-spacing: 0.1em; } .context-mode-standby strong { color: #8fcfff; font-size: 9px; - letter-spacing: .16em; + letter-spacing: 0.16em; +} +.context-mode-view { + min-height: 0; } -.context-mode-view { min-height: 0; } #right-context-rail #global-context-panel:not(.collapsed) .context-mode-view, #right-context-rail #global-context-panel:not(.collapsed) #radio-panel { /* These sections share the Context panel's existing scroller. Keep their @@ -1033,7 +1234,8 @@ letter-spacing: 0.1em; cursor: pointer; } -.panel-layer-toggle:hover, .panel-layer-toggle:focus-visible { +.panel-layer-toggle:hover, +.panel-layer-toggle:focus-visible { border-color: #79c2ff; color: #fff; outline: none; @@ -1044,7 +1246,10 @@ background: rgba(0, 212, 255, 0.14); box-shadow: 0 0 12px rgba(0, 212, 255, 0.14); } -.panel-layer-toggle:disabled { opacity: 0.45; cursor: wait; } +.panel-layer-toggle:disabled { + opacity: 0.45; + cursor: wait; +} .global-context-actions .panel-layer-toggle { width: 100%; min-height: 30px; @@ -1062,6 +1267,9 @@ font-size: 8px; box-shadow: none; } -#global-context-panel #cockpit-entry .material-symbols-outlined { font-size: 15px; } -#global-context-panel #cockpit-entry kbd { padding: 2px 4px; } - +#global-context-panel #cockpit-entry .material-symbols-outlined { + font-size: 15px; +} +#global-context-panel #cockpit-entry kbd { + padding: 2px 4px; +} diff --git a/src/ui/styles/location.css b/src/ui/styles/location.css index 4a8aa5576..281e4b08f 100644 --- a/src/ui/styles/location.css +++ b/src/ui/styles/location.css @@ -12,7 +12,11 @@ #location-bar .panel-glow { position: absolute; inset: -20px; - background: radial-gradient(ellipse at center, var(--accent-dim) 0%, transparent 60%); + background: radial-gradient( + ellipse at center, + var(--accent-dim) 0%, + transparent 60% + ); filter: blur(20px); opacity: 0.25; pointer-events: none; @@ -137,9 +141,10 @@ max-height: 0; overflow: hidden; opacity: 0; - transition: max-height 250ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 200ms ease, - padding 250ms cubic-bezier(0.4, 0, 0.2, 1); + transition: + max-height 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 200ms ease, + padding 250ms cubic-bezier(0.4, 0, 0.2, 1); padding: 0 4px; } @@ -155,7 +160,9 @@ border-top: 1px solid var(--glass-border); margin: 0 4px; opacity: 0; - transition: opacity 200ms ease, margin 250ms ease; + transition: + opacity 200ms ease, + margin 250ms ease; } .location-bar-divider.visible { @@ -272,7 +279,9 @@ font-family: var(--font-sans); font-size: 13px; letter-spacing: 0.3px; - transition: width var(--transition-smooth), padding var(--transition-smooth); + transition: + width var(--transition-smooth), + padding var(--transition-smooth); caret-color: var(--accent); } @@ -289,4 +298,3 @@ #location-search.searching { opacity: 0.5; } - diff --git a/src/ui/styles/overlays.css b/src/ui/styles/overlays.css index 87315b093..227d2d6d0 100644 --- a/src/ui/styles/overlays.css +++ b/src/ui/styles/overlays.css @@ -40,7 +40,8 @@ #safe-frame-box::before { background: - linear-gradient(to right, + linear-gradient( + to right, transparent 33.333%, rgba(255, 255, 255, 0.2) 33.333%, rgba(255, 255, 255, 0.2) 33.7%, @@ -50,7 +51,8 @@ rgba(255, 255, 255, 0.2) 66.7%, transparent 66.7% ), - linear-gradient(to bottom, + linear-gradient( + to bottom, transparent 33.333%, rgba(255, 255, 255, 0.2) 33.333%, rgba(255, 255, 255, 0.2) 33.7%, @@ -77,7 +79,9 @@ user-select: none; opacity: 0; visibility: hidden; - transition: opacity var(--transition-smooth), visibility var(--transition-smooth); + transition: + opacity var(--transition-smooth), + visibility var(--transition-smooth); } #orbit-indicator.active { @@ -99,7 +103,9 @@ pointer-events: none; opacity: 0; visibility: hidden; - transition: opacity 0.4s ease, visibility 0.4s ease; + transition: + opacity 0.4s ease, + visibility 0.4s ease; --hud-color: rgba(0, 255, 255, 0.6); --hud-glow: rgba(0, 255, 255, 0.4); --hud-border: rgba(0, 255, 255, 0.15); @@ -322,7 +328,11 @@ #intel-hud[data-variant='operator'] .hud-bottom-bar { border-color: rgba(255, 255, 255, 0.2); - background: linear-gradient(90deg, rgba(0, 0, 0, 0.52), rgba(8, 22, 18, 0.52)); + background: linear-gradient( + 90deg, + rgba(0, 0, 0, 0.52), + rgba(8, 22, 18, 0.52) + ); } #intel-hud[data-variant='operator'] .hud-corner { @@ -361,4 +371,3 @@ #intel-hud[data-variant='minimal'] .hud-summary-wrap { max-width: 280px; } - diff --git a/src/ui/styles/provider-settings.css b/src/ui/styles/provider-settings.css index 432512bd0..b68593990 100644 --- a/src/ui/styles/provider-settings.css +++ b/src/ui/styles/provider-settings.css @@ -17,7 +17,11 @@ font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.09rem; - background: linear-gradient(145deg, rgba(10, 27, 37, 0.92), rgba(7, 13, 22, 0.9)); + background: linear-gradient( + 145deg, + rgba(10, 27, 37, 0.92), + rgba(7, 13, 22, 0.9) + ); border: 1px solid rgba(54, 220, 255, 0.38); border-radius: 0.42rem; box-shadow: @@ -26,12 +30,19 @@ backdrop-filter: blur(18px) saturate(1.2); -webkit-backdrop-filter: blur(18px) saturate(1.2); cursor: pointer; - transition: border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast); + transition: + border-color var(--transition-fast), + background var(--transition-fast), + transform var(--transition-fast); } #key-setup-chip:hover, #key-setup-chip:focus-visible { - background: linear-gradient(145deg, rgba(14, 38, 51, 0.95), rgba(9, 18, 28, 0.93)); + background: linear-gradient( + 145deg, + rgba(14, 38, 51, 0.95), + rgba(9, 18, 28, 0.93) + ); border-color: rgba(54, 220, 255, 0.7); outline: none; transform: translateY(-1px); @@ -42,7 +53,9 @@ font-size: 0.95rem; } -#key-setup-chip[hidden] { display: none; } +#key-setup-chip[hidden] { + display: none; +} #key-setup { position: fixed; @@ -70,12 +83,16 @@ -webkit-backdrop-filter: blur(28px) saturate(1.25); opacity: 0; transform: translate(-50%, calc(-50% + 0.8rem)) scale(0.985); - transition: opacity 220ms ease, transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1); + transition: + opacity 220ms ease, + transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1); pointer-events: none; } /* Same cascade note as the launcher: an author display beats [hidden]. */ -#key-setup[hidden] { display: none; } +#key-setup[hidden] { + display: none; +} #key-setup.visible { opacity: 1; @@ -87,7 +104,12 @@ position: absolute; inset: 0 0 auto; height: 2px; - background: linear-gradient(90deg, transparent, rgba(54, 220, 255, 0.8), transparent); + background: linear-gradient( + 90deg, + transparent, + rgba(54, 220, 255, 0.8), + transparent + ); box-shadow: 0 0 1rem rgba(54, 220, 255, 0.45); pointer-events: none; } @@ -105,7 +127,9 @@ text-transform: uppercase; } -.key-setup-kicker { color: rgba(54, 220, 255, 0.86); } +.key-setup-kicker { + color: rgba(54, 220, 255, 0.86); +} .key-setup-close { display: grid; @@ -116,7 +140,9 @@ border: 1px solid transparent; border-radius: 0.3rem; cursor: pointer; - transition: color var(--transition-fast), border-color var(--transition-fast); + transition: + color var(--transition-fast), + border-color var(--transition-fast); } .key-setup-close:hover, @@ -126,7 +152,9 @@ outline: none; } -.key-setup-close .material-symbols-outlined { font-size: 1rem; } +.key-setup-close .material-symbols-outlined { + font-size: 1rem; +} #key-setup h2 { margin-top: 0.6rem; @@ -163,7 +191,9 @@ scrollbar-gutter: stable; } -.key-setup-rows::-webkit-scrollbar { width: 6px; } +.key-setup-rows::-webkit-scrollbar { + width: 6px; +} .key-setup-rows::-webkit-scrollbar-thumb { background: rgba(54, 220, 255, 0.45); border-radius: 3px; @@ -209,7 +239,9 @@ box-shadow: 0 0 0.5rem rgba(84, 227, 156, 0.66); } -.key-setup-tier { font-size: 0.55rem; } +.key-setup-tier { + font-size: 0.55rem; +} .key-setup-exposed { padding: 0.05rem 0.3rem; @@ -264,9 +296,13 @@ outline: none; } -.key-setup-fields input::placeholder { color: rgba(148, 178, 188, 0.42); } +.key-setup-fields input::placeholder { + color: rgba(148, 178, 188, 0.42); +} -.key-setup-footer { margin-top: 0.85rem; } +.key-setup-footer { + margin-top: 0.85rem; +} .key-setup-apply { padding: 0.5rem 0.95rem; @@ -318,7 +354,9 @@ body.scene-playback-mode #key-setup { @media (max-width: 620px) { /* Clear the dock. Desktop-first app; this is a courtesy, not a layout. */ - #key-setup-chip { bottom: 5.8rem; } + #key-setup-chip { + bottom: 5.8rem; + } } /* Provider Settings additions: externally-managed badge + per-row remove. */ @@ -344,7 +382,9 @@ body.scene-playback-mode #key-setup { border: 1px solid rgba(255, 170, 150, 0.3); border-radius: 0.32rem; cursor: pointer; - transition: color var(--transition-fast), border-color var(--transition-fast); + transition: + color var(--transition-fast), + border-color var(--transition-fast); } .key-setup-remove:hover, diff --git a/src/ui/styles/radio.css b/src/ui/styles/radio.css index c2d16f1b1..77b08eee1 100644 --- a/src/ui/styles/radio.css +++ b/src/ui/styles/radio.css @@ -5,7 +5,7 @@ min-height: 0; margin-top: 10px; padding-top: 10px; - border-top: 1px solid rgba(65, 220, 231, .16); + border-top: 1px solid rgba(65, 220, 231, 0.16); } .radio-panel-inner { position: relative; @@ -14,20 +14,24 @@ flex-direction: column; gap: 9px; padding: 10px; - border: 1px solid rgba(65, 220, 231, .18); + border: 1px solid rgba(65, 220, 231, 0.18); border-radius: 8px; - background: rgba(2, 15, 20, .42); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .035); + background: rgba(2, 15, 20, 0.42); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035); scrollbar-width: thin; - scrollbar-color: rgba(0, 212, 255, .46) rgba(255, 255, 255, .06); + scrollbar-color: rgba(0, 212, 255, 0.46) rgba(255, 255, 255, 0.06); +} +#radio-panel .panel-header { + margin-bottom: 0; } -#radio-panel .panel-header { margin-bottom: 0; } .radio-layer-state { color: var(--text-dim); font: 8px/1 var(--font-mono); - letter-spacing: .12em; + letter-spacing: 0.12em; +} +.radio-layer-state.active { + color: #65f7ff; } -.radio-layer-state.active { color: #65f7ff; } .radio-directory-row { display: grid; grid-template-columns: auto minmax(0, 1fr); @@ -38,37 +42,42 @@ grid-column: 2; color: var(--text-dim); font: 7px/1 var(--font-mono); - letter-spacing: .12em; + letter-spacing: 0.12em; +} +#radio-enable-btn { + grid-row: 1 / span 2; + min-height: 36px; } -#radio-enable-btn { grid-row: 1 / span 2; min-height: 36px; } #radio-filter { min-width: 0; padding: 7px 24px 7px 8px; - border: 1px solid rgba(65, 220, 231, .28); + border: 1px solid rgba(65, 220, 231, 0.28); border-radius: 5px; color: #c5faff; - background: rgba(5, 24, 31, .88); + background: rgba(5, 24, 31, 0.88); font: 9px/1.2 var(--font-mono); } .radio-station-card { display: grid; gap: 4px; padding: 10px; - border: 1px solid rgba(65, 220, 231, .18); + border: 1px solid rgba(65, 220, 231, 0.18); border-radius: 7px; - background: rgba(2, 15, 20, .48); + background: rgba(2, 15, 20, 0.48); overflow-wrap: anywhere; } .radio-station-card strong { color: #e8feff; font: 11px/1.25 var(--font-mono); - letter-spacing: .05em; + letter-spacing: 0.05em; } .radio-station-card span { color: var(--text-dim); font: 8px/1.35 var(--font-mono); } -#radio-station-tags { color: #72dce6; } +#radio-station-tags { + color: #72dce6; +} .radio-tuner { display: grid; width: 100%; @@ -77,12 +86,18 @@ gap: 5px; padding: 8px; overflow: hidden; - border: 1px solid rgba(65, 220, 231, .22); + border: 1px solid rgba(65, 220, 231, 0.22); border-radius: 7px; - background: linear-gradient(180deg, rgba(1, 9, 13, .92), rgba(4, 24, 30, .72)); - box-shadow: inset 0 0 18px rgba(0, 212, 255, .045); + background: linear-gradient( + 180deg, + rgba(1, 9, 13, 0.92), + rgba(4, 24, 30, 0.72) + ); + box-shadow: inset 0 0 18px rgba(0, 212, 255, 0.045); +} +.radio-tuner[hidden] { + display: none; } -.radio-tuner[hidden] { display: none; } .radio-tuner-header, .radio-tuner-readout { display: flex; @@ -90,7 +105,7 @@ gap: 8px; color: #6daeb5; font: 7px/1.2 var(--font-mono); - letter-spacing: .1em; + letter-spacing: 0.1em; } #radio-tuner-value { min-width: 0; @@ -108,10 +123,22 @@ height: 51px; overflow: hidden; contain: paint; - border-block: 1px solid rgba(72, 225, 237, .3); + border-block: 1px solid rgba(72, 225, 237, 0.3); background: - linear-gradient(180deg, transparent 23px, rgba(65, 220, 231, .8) 24px, transparent 25px, transparent 28px, rgba(65, 220, 231, .6) 29px, transparent 30px), - radial-gradient(ellipse at center, rgba(39, 177, 191, .12), transparent 70%); + linear-gradient( + 180deg, + transparent 23px, + rgba(65, 220, 231, 0.8) 24px, + transparent 25px, + transparent 28px, + rgba(65, 220, 231, 0.6) 29px, + transparent 30px + ), + radial-gradient( + ellipse at center, + rgba(39, 177, 191, 0.12), + transparent 70% + ); } .radio-tuner-scale { position: absolute; @@ -119,7 +146,7 @@ overflow: hidden; color: #58cbd6; font: 9px/1 var(--font-mono); - text-shadow: 0 0 7px rgba(65, 220, 231, .5); + text-shadow: 0 0 7px rgba(65, 220, 231, 0.5); pointer-events: none; } .radio-tuner-tick { @@ -127,7 +154,7 @@ top: 6px; bottom: 0; min-width: 1px; - border-left: 1px solid rgba(85, 224, 235, .38); + border-left: 1px solid rgba(85, 224, 235, 0.38); color: #58cbd6; padding-left: 3px; white-space: nowrap; @@ -135,7 +162,7 @@ transition: left 80ms linear; } .radio-tuner-tick.is-current { - border-left-color: rgba(168, 247, 255, .9); + border-left-color: rgba(168, 247, 255, 0.9); color: #a8f7ff; } .radio-tuner-needle { @@ -147,13 +174,17 @@ width: 3px; border-radius: 1px; background: #ff6f4f; - box-shadow: 0 0 4px #ff6f4f, 0 0 10px rgba(255, 77, 50, .65); + box-shadow: + 0 0 4px #ff6f4f, + 0 0 10px rgba(255, 77, 50, 0.65); pointer-events: none; transform: translateX(-50%); - transition: left .18s ease-out; + transition: left 0.18s ease-out; } .radio-tuner.is-dragging .radio-tuner-needle, -.radio-tuner.is-dragging .radio-tuner-tick { transition: none; } +.radio-tuner.is-dragging .radio-tuner-tick { + transition: none; +} #radio-tuner-slider { position: absolute; z-index: 2; @@ -184,7 +215,10 @@ background: transparent; box-shadow: none; } -#radio-tuner-slider::-moz-range-track { height: 100%; background: transparent; } +#radio-tuner-slider::-moz-range-track { + height: 100%; + background: transparent; +} #radio-tuner-slider::-moz-range-thumb { width: 14px; height: 47px; @@ -194,9 +228,11 @@ box-shadow: none; } .radio-tuner.is-static .radio-tuner-dial { - box-shadow: inset 0 0 14px rgba(255, 111, 79, .1); + box-shadow: inset 0 0 14px rgba(255, 111, 79, 0.1); +} +.radio-tuner.is-static #radio-tuner-station { + color: #ff9b7e; } -.radio-tuner.is-static #radio-tuner-station { color: #ff9b7e; } .radio-tuner-readout span:first-child { min-width: 0; color: #7fe6ef; @@ -216,12 +252,12 @@ .radio-transport button { min-height: 30px; padding: 6px 3px; - border: 1px solid rgba(65, 220, 231, .28); + border: 1px solid rgba(65, 220, 231, 0.28); border-radius: 5px; color: #8feaf2; - background: rgba(14, 69, 78, .14); + background: rgba(14, 69, 78, 0.14); font: 8px/1 var(--font-mono); - letter-spacing: .06em; + letter-spacing: 0.06em; cursor: pointer; } .radio-transport button:hover:not(:disabled), @@ -231,12 +267,15 @@ outline: none; } .radio-transport button:disabled, -#radio-filter:disabled { opacity: .42; cursor: not-allowed; } +#radio-filter:disabled { + opacity: 0.42; + cursor: not-allowed; +} #radio-play-btn.active { color: #fff; - border-color: rgba(101, 247, 255, .8); - background: rgba(65, 220, 231, .18); - box-shadow: 0 0 12px rgba(65, 220, 231, .12); + border-color: rgba(101, 247, 255, 0.8); + background: rgba(65, 220, 231, 0.18); + box-shadow: 0 0 12px rgba(65, 220, 231, 0.12); } .radio-volume-row { display: grid; @@ -245,15 +284,19 @@ align-items: center; color: var(--text-dim); font: 7px/1 var(--font-mono); - letter-spacing: .1em; + letter-spacing: 0.1em; +} +.radio-volume-row input { + width: 100%; } -.radio-volume-row input { width: 100%; } .radio-playback-state { min-height: 12px; color: #79d7df; font: 8px/1.35 var(--font-mono); } -.radio-playback-state.error { color: #ff9d8f; } +.radio-playback-state.error { + color: #ff9d8f; +} .radio-links { display: flex; flex-wrap: wrap; @@ -263,13 +306,16 @@ .radio-links a { color: #64cbd5; font: 7px/1.3 var(--font-mono); - letter-spacing: .06em; + letter-spacing: 0.06em; text-decoration: none; } -.radio-links a:hover, .radio-links a:focus-visible { color: #fff; } +.radio-links a:hover, +.radio-links a:focus-visible { + color: #fff; +} .radio-privacy { margin: 0; - color: rgba(190, 230, 230, .48); + color: rgba(190, 230, 230, 0.48); font: 7px/1.4 var(--font-mono); } #military-awareness-panel { @@ -281,36 +327,129 @@ color: #dceeff; background: transparent; border: 0; - font: 11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 11px/1.35 ui-monospace, + SFMono-Regular, + Menlo, + monospace; } -#military-awareness-panel[hidden] { display: none; } -.military-awareness-subject { margin: 0 0 7px; color: #fff; } -.military-awareness-controls { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 5px; margin: 0 0 8px; } -.military-awareness-controls button { border: 1px solid rgba(98,181,255,0.34); border-radius: 4px; padding: 4px 3px; color: #a9d9ff; background: rgba(42,113,171,0.12); font: 9px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.07em; cursor: pointer; } -.military-awareness-controls button:hover, .military-awareness-controls button:focus-visible { color: #fff; border-color: rgba(127,200,255,0.85); background: rgba(66,148,211,0.24); outline: none; } -.military-awareness-controls button:disabled { color: #627181; border-color: rgba(98,181,255,0.12); background: transparent; cursor: default; } -.military-awareness-row { border-top: 1px solid rgba(255,255,255,0.1); padding: 7px 0; } -.military-awareness-row > div { display: flex; justify-content: space-between; } -.military-awareness-row b { color: #7fc8ff; } -.military-awareness-row.unknown b { color: #8fcfff; } -.military-awareness-row small, .military-awareness-note { color: #a9b7c7; } -.military-awareness-row ul { margin: 4px 0 0; padding: 0; list-style: none; } -.military-awareness-row li { display: block; } -.military-awareness-target { display: flex; justify-content: space-between; gap: 10px; width: 100%; border: 0; padding: 1px 0; color: #dceeff; background: transparent; font: inherit; text-align: left; cursor: pointer; } -.military-awareness-target.unavailable { color: var(--text-dim); cursor: default; } -.military-awareness-target:hover, .military-awareness-target:focus-visible { color: #fff; outline: none; text-shadow: 0 0 8px rgba(114, 202, 255, 0.85); } -.military-awareness-target:focus-visible { box-shadow: inset 0 -1px 0 #7fc8ff; } -.military-awareness-target span { color: #7fc8ff; white-space: nowrap; } -.military-awareness-note { margin: 8px 0 0; font-size: 10px; } +#military-awareness-panel[hidden] { + display: none; +} +.military-awareness-subject { + margin: 0 0 7px; + color: #fff; +} +.military-awareness-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 5px; + margin: 0 0 8px; +} +.military-awareness-controls button { + border: 1px solid rgba(98, 181, 255, 0.34); + border-radius: 4px; + padding: 4px 3px; + color: #a9d9ff; + background: rgba(42, 113, 171, 0.12); + font: + 9px/1.2 ui-monospace, + SFMono-Regular, + Menlo, + monospace; + letter-spacing: 0.07em; + cursor: pointer; +} +.military-awareness-controls button:hover, +.military-awareness-controls button:focus-visible { + color: #fff; + border-color: rgba(127, 200, 255, 0.85); + background: rgba(66, 148, 211, 0.24); + outline: none; +} +.military-awareness-controls button:disabled { + color: #627181; + border-color: rgba(98, 181, 255, 0.12); + background: transparent; + cursor: default; +} +.military-awareness-row { + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding: 7px 0; +} +.military-awareness-row > div { + display: flex; + justify-content: space-between; +} +.military-awareness-row b { + color: #7fc8ff; +} +.military-awareness-row.unknown b { + color: #8fcfff; +} +.military-awareness-row small, +.military-awareness-note { + color: #a9b7c7; +} +.military-awareness-row ul { + margin: 4px 0 0; + padding: 0; + list-style: none; +} +.military-awareness-row li { + display: block; +} +.military-awareness-target { + display: flex; + justify-content: space-between; + gap: 10px; + width: 100%; + border: 0; + padding: 1px 0; + color: #dceeff; + background: transparent; + font: inherit; + text-align: left; + cursor: pointer; +} +.military-awareness-target.unavailable { + color: var(--text-dim); + cursor: default; +} +.military-awareness-target:hover, +.military-awareness-target:focus-visible { + color: #fff; + outline: none; + text-shadow: 0 0 8px rgba(114, 202, 255, 0.85); +} +.military-awareness-target:focus-visible { + box-shadow: inset 0 -1px 0 #7fc8ff; +} +.military-awareness-target span { + color: #7fc8ff; + white-space: nowrap; +} +.military-awareness-note { + margin: 8px 0 0; + font-size: 10px; +} .military-awareness-standby { display: grid; gap: 7px; padding: 12px 4px 4px; border-top: 1px solid rgba(98, 181, 255, 0.16); } -.military-awareness-standby strong { color: #79c2ff; font-size: 10px; letter-spacing: 0.1em; } -.military-awareness-standby span { color: #8192a6; font-size: 9px; letter-spacing: 0.05em; } +.military-awareness-standby strong { + color: #79c2ff; + font-size: 10px; + letter-spacing: 0.1em; +} +.military-awareness-standby span { + color: #8192a6; + font-size: 9px; + letter-spacing: 0.05em; +} .military-awareness-standby button { justify-self: start; padding: 4px 12px; @@ -318,27 +457,51 @@ border-radius: 4px; color: #a9d9ff; background: rgba(42, 113, 171, 0.12); - font: 9px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 9px/1.2 ui-monospace, + SFMono-Regular, + Menlo, + monospace; letter-spacing: 0.08em; cursor: pointer; } -#military-awareness-direction-overlay { position: absolute; inset: 0; z-index: 4; overflow: hidden; pointer-events: none; } -#military-awareness-direction-overlay[hidden] { display: none; } +#military-awareness-direction-overlay { + position: absolute; + inset: 0; + z-index: 4; + overflow: hidden; + pointer-events: none; +} +#military-awareness-direction-overlay[hidden] { + display: none; +} .military-awareness-compass-ring { --compass-rotation: 0deg; position: absolute; box-sizing: border-box; - border: 1px solid rgba(111,196,255,0.2); - outline: 1px dashed rgba(111,196,255,0.28); + border: 1px solid rgba(111, 196, 255, 0.2); + outline: 1px dashed rgba(111, 196, 255, 0.28); outline-offset: -5px; border-radius: 50%; box-shadow: - inset 0 0 0 5px rgba(34,128,196,0.018), - inset 0 0 24px rgba(34,128,196,0.035), - 0 0 14px rgba(34,128,196,0.05); + inset 0 0 0 5px rgba(34, 128, 196, 0.018), + inset 0 0 24px rgba(34, 128, 196, 0.035), + 0 0 14px rgba(34, 128, 196, 0.05); +} +.military-awareness-compass-ring::before { + content: ''; + position: absolute; + left: 50%; + top: -7px; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 8px solid rgba(127, 200, 255, 0.88); + transform: translateX(-50%); + filter: drop-shadow(0 0 5px rgba(87, 184, 255, 0.7)); } -.military-awareness-compass-ring::before { content: ''; position: absolute; left: 50%; top: -7px; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 8px solid rgba(127,200,255,0.88); transform: translateX(-50%); filter: drop-shadow(0 0 5px rgba(87,184,255,0.7)); } .military-awareness-compass-ring::after { content: ''; position: absolute; @@ -349,22 +512,86 @@ rgba(125, 205, 255, 0.38) 0deg 0.7deg, transparent 0.7deg 15deg ); - -webkit-mask: radial-gradient(circle, transparent calc(100% - 8px), #000 calc(100% - 8px) calc(100% - 4px), transparent calc(100% - 4px)); - mask: radial-gradient(circle, transparent calc(100% - 8px), #000 calc(100% - 8px) calc(100% - 4px), transparent calc(100% - 4px)); + -webkit-mask: radial-gradient( + circle, + transparent calc(100% - 8px), + #000 calc(100% - 8px) calc(100% - 4px), + transparent calc(100% - 4px) + ); + mask: radial-gradient( + circle, + transparent calc(100% - 8px), + #000 calc(100% - 8px) calc(100% - 4px), + transparent calc(100% - 4px) + ); transform: rotate(var(--compass-rotation)); transition: transform 80ms linear; } -.military-awareness-compass-label, .military-awareness-compass-heading { position: absolute; transform: translate(-50%, -50%); color: rgba(156,213,255,0.7); font: 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.08em; text-shadow: 0 0 7px rgba(53,161,235,0.55); } -.military-awareness-compass-label.cardinal-n { color: rgba(255,188,87,0.92); } -.military-awareness-compass-heading { color: rgba(190,225,249,0.76); font-size: 8px; } -.military-awareness-direction-marker { position: absolute; width: 1px; height: 1px; transform: translate(-50%, -50%); transition: opacity 100ms linear; } -.military-awareness-direction-marker[hidden] { display: none; } -.military-awareness-direction-arrow { position: absolute; left: 50%; top: 50%; color: #74dbff; font: 23px/1 sans-serif; transform-origin: 50% 50%; text-shadow: 0 0 5px rgba(87,223,255,0.9), 0 0 13px rgba(87,223,255,0.35); } -.military-awareness-direction-label { position: absolute; left: 50%; top: 50%; padding: 3px 6px; border: 1px solid rgba(116,219,255,0.24); border-radius: 3px; color: #bceeff; background: rgba(5,12,20,0.68); font: 8px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center; white-space: pre; text-shadow: 0 0 5px rgba(116,219,255,0.38); } +.military-awareness-compass-label, +.military-awareness-compass-heading { + position: absolute; + transform: translate(-50%, -50%); + color: rgba(156, 213, 255, 0.7); + font: + 9px/1 ui-monospace, + SFMono-Regular, + Menlo, + monospace; + letter-spacing: 0.08em; + text-shadow: 0 0 7px rgba(53, 161, 235, 0.55); +} +.military-awareness-compass-label.cardinal-n { + color: rgba(255, 188, 87, 0.92); +} +.military-awareness-compass-heading { + color: rgba(190, 225, 249, 0.76); + font-size: 8px; +} +.military-awareness-direction-marker { + position: absolute; + width: 1px; + height: 1px; + transform: translate(-50%, -50%); + transition: opacity 100ms linear; +} +.military-awareness-direction-marker[hidden] { + display: none; +} +.military-awareness-direction-arrow { + position: absolute; + left: 50%; + top: 50%; + color: #74dbff; + font: 23px/1 sans-serif; + transform-origin: 50% 50%; + text-shadow: + 0 0 5px rgba(87, 223, 255, 0.9), + 0 0 13px rgba(87, 223, 255, 0.35); +} +.military-awareness-direction-label { + position: absolute; + left: 50%; + top: 50%; + padding: 3px 6px; + border: 1px solid rgba(116, 219, 255, 0.24); + border-radius: 3px; + color: #bceeff; + background: rgba(5, 12, 20, 0.68); + font: + 8px/1.3 ui-monospace, + SFMono-Regular, + Menlo, + monospace; + text-align: center; + white-space: pre; + text-shadow: 0 0 5px rgba(116, 219, 255, 0.38); +} .data-toggle-row.gev-voice-focus { background: rgba(92, 255, 198, 0.1); - box-shadow: inset 0 0 0 1px rgba(92, 255, 198, 0.72), 0 0 18px rgba(92, 255, 198, 0.18); + box-shadow: + inset 0 0 0 1px rgba(92, 255, 198, 0.72), + 0 0 18px rgba(92, 255, 198, 0.18); } .data-toggle-top { @@ -573,4 +800,3 @@ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18); flex: none; } - diff --git a/src/ui/styles/recording.css b/src/ui/styles/recording.css index ebfed290b..e00f2bd1f 100644 --- a/src/ui/styles/recording.css +++ b/src/ui/styles/recording.css @@ -30,4 +30,3 @@ body.recording-mode #scene-runtime.active { body.scene-playback-mode :is(#clear-selected-layers, #reset-globe-view) { display: none !important; } - diff --git a/src/ui/styles/responsive.css b/src/ui/styles/responsive.css index 2c61dc278..f07e5c1ff 100644 --- a/src/ui/styles/responsive.css +++ b/src/ui/styles/responsive.css @@ -213,4 +213,3 @@ text-align: center; } } - diff --git a/src/ui/styles/scenes.css b/src/ui/styles/scenes.css index cb5e04140..58fdfed81 100644 --- a/src/ui/styles/scenes.css +++ b/src/ui/styles/scenes.css @@ -35,7 +35,9 @@ order: 2; } -#left-panel-stack > #scene-panel { order: 4; } +#left-panel-stack > #scene-panel { + order: 4; +} /* * Runtime focus mode: the layout engine applies this only when the expanded @@ -59,7 +61,8 @@ * When an expanded panel and its collapsed siblings fit together, preserve * the canonical control order and center their combined stack at 50vh. */ -#left-panel-stack.layout-tail > :is(#data-panel, #cctv-panel, #scene-panel):not(.collapsed) { +#left-panel-stack.layout-tail + > :is(#data-panel, #cctv-panel, #scene-panel):not(.collapsed) { box-sizing: border-box; flex: 0 0 var(--left-panel-allocated-height); height: var(--left-panel-allocated-height); @@ -91,19 +94,33 @@ } #left-panel-stack > [data-panel-id]:not(.collapsed) .panel-divider { - background: linear-gradient(90deg, rgb(0 212 255 / 28%), rgba(0, 212, 255, 0.18) 58%, transparent); + background: linear-gradient( + 90deg, + rgb(0 212 255 / 28%), + rgba(0, 212, 255, 0.18) 58%, + transparent + ); box-shadow: 0 0 7px rgba(0, 212, 255, 0.22); } #right-context-rail [data-panel-id]:not(.collapsed) .panel-divider { - background: linear-gradient(90deg, rgb(0 212 255 / 28%), rgba(0, 212, 255, 0.18) 58%, transparent); + background: linear-gradient( + 90deg, + rgb(0 212 255 / 28%), + rgba(0, 212, 255, 0.18) 58%, + transparent + ); box-shadow: 0 0 7px rgba(0, 212, 255, 0.22); } #scene-panel .panel-glow { position: absolute; inset: -18px; - background: radial-gradient(ellipse at center, rgba(0, 212, 255, 0.16) 0%, transparent 65%); + background: radial-gradient( + ellipse at center, + rgba(0, 212, 255, 0.16) 0%, + transparent 65% + ); filter: blur(16px); opacity: 0.25; pointer-events: none; @@ -209,7 +226,9 @@ border-radius: 9px; padding: 7px 8px; background: rgba(255, 255, 255, 0.03); - transition: border-color var(--transition-fast), background var(--transition-fast); + transition: + border-color var(--transition-fast), + background var(--transition-fast); } .scene-shot-row.active { @@ -300,7 +319,11 @@ #scene-progress-fill { width: 0%; height: 100%; - background: linear-gradient(90deg, rgba(0, 212, 255, 0.2), rgba(0, 212, 255, 0.75)); + background: linear-gradient( + 90deg, + rgba(0, 212, 255, 0.2), + rgba(0, 212, 255, 0.75) + ); color: #d5f7ff; font-family: var(--font-mono); font-size: 8px; @@ -329,7 +352,9 @@ z-index: 105; opacity: 0; visibility: hidden; - transition: opacity 180ms ease, visibility 180ms ease; + transition: + opacity 180ms ease, + visibility 180ms ease; font-family: var(--font-mono); font-size: 11px; letter-spacing: 2px; @@ -354,7 +379,9 @@ #global-context-panel.collapsed .global-context-actions, #global-context-panel.collapsed .global-context-modes, #global-context-panel.collapsed .context-mode-view, -#global-context-panel.collapsed > .global-context-panel-inner > .context-mode-standby, +#global-context-panel.collapsed + > .global-context-panel-inner + > .context-mode-standby, #radio-panel.collapsed .radio-panel-inner > :not(.panel-header), #cctv-panel.collapsed .cctv-controls, #cctv-panel.collapsed .cctv-calibration-block, @@ -380,7 +407,9 @@ width: var(--left-collapsed-width); } -#global-context-panel #radio-panel.collapsed { width: 100%; } +#global-context-panel #radio-panel.collapsed { + width: 100%; +} #data-panel.collapsed .panel-title, #cctv-panel.collapsed .panel-title, @@ -409,4 +438,3 @@ #param-slider-panel.collapsed .param-panel-header { margin-bottom: 2px; } - diff --git a/src/ui/styles/status.css b/src/ui/styles/status.css index c6aca6c96..2700d3dfd 100644 --- a/src/ui/styles/status.css +++ b/src/ui/styles/status.css @@ -74,10 +74,14 @@ letter-spacing: 1.35px; text-transform: uppercase; transform: translateX(-50%); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34), 0 0 14px rgba(0, 212, 255, 0.08); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.34), + 0 0 14px rgba(0, 212, 255, 0.08); pointer-events: none; } -#global-loading-status[hidden] { display: none; } +#global-loading-status[hidden] { + display: none; +} #global-loading-label { color: rgba(184, 244, 255, 0.92); } @@ -92,25 +96,49 @@ text-overflow: ellipsis; white-space: nowrap; } -#global-loading-status:is([data-state='complete'], [data-state='cancelled'], [data-state='error']) { +#global-loading-status:is( + [data-state='complete'], + [data-state='cancelled'], + [data-state='error'] +) { justify-content: center; text-align: center; } -#global-loading-status:is([data-state='complete'], [data-state='cancelled'], [data-state='error']) #global-loading-detail { +#global-loading-status:is( + [data-state='complete'], + [data-state='cancelled'], + [data-state='error'] + ) + #global-loading-detail { display: none; } -#global-loading-status[data-state='error'] { border-color: rgba(255, 98, 82, 0.55); color: #ff9b8e; } +#global-loading-status[data-state='error'] { + border-color: rgba(255, 98, 82, 0.55); + color: #ff9b8e; +} #global-loading-status[data-state='error'] #global-loading-label, -#global-loading-status[data-state='error'] #global-loading-detail { color: #ff9b8e; } -#global-loading-status[data-state='cancelled'] { color: #ffd488; } -#global-loading-status[data-state='retry'] { border-color: rgba(255, 212, 136, 0.55); } +#global-loading-status[data-state='error'] #global-loading-detail { + color: #ff9b8e; +} +#global-loading-status[data-state='cancelled'] { + color: #ffd488; +} +#global-loading-status[data-state='retry'] { + border-color: rgba(255, 212, 136, 0.55); +} #global-loading-status[data-state='retry'] #global-loading-label, -#global-loading-status[data-state='retry'] #global-loading-detail { color: #ffd488; } +#global-loading-status[data-state='retry'] #global-loading-detail { + color: #ffd488; +} #global-loading-status[data-state='cancelled'] #global-loading-label, -#global-loading-status[data-state='cancelled'] #global-loading-detail { color: #ffd488; } +#global-loading-status[data-state='cancelled'] #global-loading-detail { + color: #ffd488; +} #global-loading-status[data-state='acquiring'] { border-color: rgba(0, 212, 255, 0.42); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34), 0 0 18px rgba(0, 212, 255, 0.12); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.34), + 0 0 18px rgba(0, 212, 255, 0.12); } .share-icon { @@ -141,8 +169,12 @@ text-transform: uppercase; opacity: 0; pointer-events: none; - transition: opacity 0.22s ease, transform 0.22s ease; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34), 0 0 14px rgba(0, 212, 255, 0.08); + transition: + opacity 0.22s ease, + transform 0.22s ease; + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.34), + 0 0 14px rgba(0, 212, 255, 0.08); } /* Stacked below the traffic chip so both can show at once */ @@ -270,21 +302,43 @@ them. */ .gev-flap-sizing { display: inline-block; - transition: width var(--gev-flap-total, 500ms) cubic-bezier(0.2, 0.75, 0.25, 1); + transition: width var(--gev-flap-total, 500ms) + cubic-bezier(0.2, 0.75, 0.25, 1); } /* The old leaf hinges down and away on the cell's midline. */ @keyframes gev-flap-out { - 0% { transform: rotateX(0deg); opacity: 1; } - 48% { transform: rotateX(-88deg); opacity: 0.5; } - 100% { transform: rotateX(-90deg); opacity: 0; } + 0% { + transform: rotateX(0deg); + opacity: 1; + } + 48% { + transform: rotateX(-88deg); + opacity: 0.5; + } + 100% { + transform: rotateX(-90deg); + opacity: 0; + } } /* The new leaf waits for the old one to clear, then drops in and settles with a single small overshoot — the mechanical clack. */ @keyframes gev-flap-in { - 0% { transform: rotateX(90deg); opacity: 0; } - 46% { transform: rotateX(90deg); opacity: 0; } - 78% { transform: rotateX(-7deg); opacity: 1; } - 100% { transform: rotateX(0deg); opacity: 1; } + 0% { + transform: rotateX(90deg); + opacity: 0; + } + 46% { + transform: rotateX(90deg); + opacity: 0; + } + 78% { + transform: rotateX(-7deg); + opacity: 1; + } + 100% { + transform: rotateX(0deg); + opacity: 1; + } } /* Matches the .cockpit-roll-track treatment: collapse the duration rather than dropping the animation, so the final frame still lands the glyph. @@ -322,7 +376,10 @@ opacity: 0; visibility: hidden; pointer-events: none; - transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease; + transition: + opacity 0.3s ease, + visibility 0.3s ease, + transform 0.3s ease; box-shadow: 0 0 20px rgba(0, 212, 255, 0.15); } @@ -331,4 +388,3 @@ visibility: visible; transform: translateX(-50%) translateY(0); } - diff --git a/src/ui/styles/voice-cost.css b/src/ui/styles/voice-cost.css index 57c55e281..44dd087f3 100644 --- a/src/ui/styles/voice-cost.css +++ b/src/ui/styles/voice-cost.css @@ -28,7 +28,9 @@ letter-spacing: 0.045rem; line-height: 1; cursor: pointer; - transition: color var(--transition-fast), border-color var(--transition-fast), + transition: + color var(--transition-fast), + border-color var(--transition-fast), background var(--transition-fast); } @@ -75,4 +77,3 @@ #command-dock.tray-open .gev-voice-cost { display: none; } -