merge main 07094d9 into feat/voice-outlines

This commit is contained in:
manjunath committed 2026-11-02 11:05:08 +05:30
commit c5aa0d28c3
593 files changed
+42386 -19629

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
/* ── Screen-space annotations (src/annotations/screenAnnotationRenderer.js) ── */
.gev-screen-whiteboard { position: fixed; inset: 0; pointer-events: none; z-index: 90; }
.gev-screen-whiteboard-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* No CSS opacity transition here on purpose: group opacity is driven per-frame in JS
(the 260ms fade-in via computeAlpha, and the tracked-entity z-order ease). A CSS
transition fought those per-frame writes and made the tracked-fade opacity oscillate
(flicker). Fade-OUT keeps its own transition via .gev-anno.gev-out below. */
.gev-anno-ring { filter: drop-shadow(0 0 6px currentColor); }
.gev-anno-dot { filter: drop-shadow(0 0 5px rgba(255,255,255,0.6)); }
.gev-anno-area { filter: drop-shadow(0 0 5px currentColor); }
.gev-anno-arrow { filter: drop-shadow(0 0 4px currentColor); stroke-linecap: round; }
.gev-anno-leader { stroke-dasharray: 2 3; }
.gev-anno-card { fill: rgba(8,18,28,0.78); stroke: rgba(255,255,255,0.14); stroke-width: 1; }
.gev-anno-text { fill: #eaf6ff; font: 600 13px "JetBrains Mono", ui-monospace, monospace; letter-spacing: 0.02em; }
/* draw-on: outlined shapes reveal their stroke */
.gev-draw { stroke-dasharray: 1400; stroke-dashoffset: 1400; }
.gev-anno.gev-in .gev-draw { transition: stroke-dashoffset 900ms ease-out; stroke-dashoffset: 0; }
/* opacity-only fade-in: a CSS transform here would override the SVG transform
attribute used to POSITION the callout and snap it to 0,0. */
.gev-anno-callout { opacity: 0; }
.gev-anno.gev-in .gev-anno-callout { animation: gev-pop 320ms ease-out both; }
.gev-anno.gev-out { opacity: 0 !important; transition: opacity 320ms ease; }
.gev-pulse { animation: gev-ring-pulse 1.8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
@keyframes gev-ring-pulse { 0%,100% { opacity: 0.9; stroke-width: 2; } 50% { opacity: 0.35; stroke-width: 3.5; } }
@keyframes gev-pop { from { opacity: 0; } to { opacity: 1; } }
-2
View File
@@ -106,8 +106,6 @@
min-height: 0;
overflow: hidden;
background: #03080d;
scrollbar-width: thin;
scrollbar-color: rgba(107, 232, 255, 0.36) transparent;
}
.bhote-embedded-callout.is-youtube {
-21
View File
@@ -35,8 +35,6 @@
gap: 8px;
max-height: calc(100vh - 24px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) rgba(255, 255, 255, 0.06);
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
box-shadow:
@@ -45,25 +43,6 @@
0 1px 0 rgba(255, 255, 255, 0.06) inset;
}
.cctv-panel-inner::-webkit-scrollbar {
width: 8px;
}
.cctv-panel-inner::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.04);
border-radius: 999px;
}
.cctv-panel-inner::-webkit-scrollbar-thumb {
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;
}
.cctv-controls {
display: flex;
gap: 8px;
+43 -58
View File
@@ -969,23 +969,50 @@ body.cockpit-mode #view-switcher button {
.cockpit-context-window[data-state='lost'] {
border-top-color: rgba(213, 168, 99, 0.72);
}
.cockpit-context-header {
/* The Cockpit cards' headers (Contact, the briefing) share their frame: a
title column over a hairline, a kicker in the accent, and a row of square
actions. Each card sets only its spacing and sizes. */
.cockpit-card-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);
}
.cockpit-context-header > div {
.cockpit-card-header > div {
display: grid;
gap: 2px;
}
.cockpit-context-kicker {
.cockpit-card-kicker {
color: var(--cockpit-accent);
font-size: 8px;
font-weight: 700;
}
.cockpit-card-actions {
display: flex;
}
.cockpit-card-actions button {
display: grid;
height: 24px;
place-items: center;
padding: 0;
border: 1px solid color-mix(in srgb, var(--cockpit-accent) 30%, transparent);
border-radius: 5px;
background: rgba(0, 7, 12, 0.72);
color: color-mix(in srgb, var(--cockpit-accent) 82%, white);
cursor: pointer;
}
.cockpit-card-actions button:hover,
.cockpit-card-actions button:focus-visible {
border-color: var(--cockpit-accent);
color: #fff;
outline: none;
}
.cockpit-context-header {
gap: 5px 10px;
padding: 0 5px 7px;
}
.cockpit-context-kicker {
font-size: 8px;
letter-spacing: 0.2em;
}
#cockpit-context-subject {
@@ -1041,26 +1068,10 @@ body.cockpit-mode #view-switcher button {
outline: none;
}
.cockpit-context-actions {
display: flex;
gap: 4px;
}
.cockpit-context-actions button {
display: grid;
width: 28px;
height: 24px;
place-items: center;
padding: 0;
border: 1px solid color-mix(in srgb, var(--cockpit-accent) 30%, transparent);
border-radius: 5px;
background: rgba(0, 7, 12, 0.72);
color: color-mix(in srgb, var(--cockpit-accent) 82%, white);
cursor: pointer;
}
.cockpit-context-actions button:hover,
.cockpit-context-actions button:focus-visible {
border-color: var(--cockpit-accent);
color: #fff;
outline: none;
}
.cockpit-context-actions button:disabled {
border-color: rgba(244, 251, 255, 0.1);
@@ -1082,7 +1093,7 @@ body.cockpit-mode #view-switcher button {
gap: 3px 8px;
min-width: 0;
padding: 6px 9px;
border-left: 1px solid
border-inline-start: 1px solid
color-mix(in srgb, var(--cockpit-accent) 40%, transparent);
background: rgba(0, 7, 12, 0.34);
}
@@ -1108,7 +1119,7 @@ body.cockpit-mode #view-switcher button {
white-space: nowrap;
}
.cockpit-context-cohorts > div.unknown {
border-left-color: rgba(213, 168, 99, 0.58);
border-inline-start-color: rgba(213, 168, 99, 0.58);
}
.cockpit-context-cohorts > div.unknown strong {
color: #d5a863;
@@ -1500,17 +1511,8 @@ body.cockpit-mode #view-switcher button {
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);
}
.cockpit-signal-header > div {
display: grid;
gap: 2px;
}
html:not([lang='en']) .cockpit-signal-header > div {
min-width: 0;
@@ -1526,9 +1528,7 @@ html:not([lang='en']) .cockpit-signal-kicker {
letter-spacing: 0.1em;
}
.cockpit-signal-kicker {
color: var(--cockpit-accent);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.18em;
}
.cockpit-signal-kicker i {
@@ -1542,7 +1542,6 @@ html:not([lang='en']) .cockpit-signal-kicker {
animation: cockpit-live 1.6s ease-in-out infinite;
}
.cockpit-brief-actions {
display: flex;
align-items: center;
gap: 3px;
}
@@ -1552,22 +1551,7 @@ html:not([lang='en']) .cockpit-brief-actions > * {
flex-shrink: 0;
}
.cockpit-brief-actions button {
display: grid;
width: 23px;
height: 24px;
place-items: center;
padding: 0;
border: 1px solid color-mix(in srgb, var(--cockpit-accent) 30%, transparent);
border-radius: 5px;
background: rgba(0, 7, 12, 0.72);
color: color-mix(in srgb, var(--cockpit-accent) 82%, white);
cursor: pointer;
}
.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;
@@ -1627,8 +1611,9 @@ html:not([lang='en']) #cockpit-brief-auto {
display: grid;
grid-template-columns: 54px minmax(0, 1fr);
gap: 8px;
padding: 7px 7px 7px 9px;
border-left: 1px solid rgba(244, 251, 255, 0.2);
padding-block: 7px;
padding-inline: 9px 7px;
border-inline-start: 1px solid rgba(244, 251, 255, 0.2);
background: rgba(0, 7, 12, 0.3);
}
.cockpit-signal-list li.actionable {
@@ -1638,7 +1623,7 @@ html:not([lang='en']) #cockpit-brief-auto {
}
.cockpit-signal-list li.actionable:hover,
.cockpit-signal-list li.actionable:focus-within {
border-left-color: var(--cockpit-accent);
border-inline-start-color: var(--cockpit-accent);
background: color-mix(
in srgb,
var(--cockpit-accent) 10%,
@@ -1647,16 +1632,16 @@ html:not([lang='en']) #cockpit-brief-auto {
}
.cockpit-signal-list li.track,
.cockpit-signal-list li.current {
border-left-color: var(--cockpit-accent);
border-inline-start-color: var(--cockpit-accent);
}
.cockpit-signal-list li.nearby {
border-left-color: #fff;
border-inline-start-color: #fff;
}
.cockpit-signal-list li.military {
border-left-color: #d5a863;
border-inline-start-color: #d5a863;
}
.cockpit-signal-list li.warning {
border-left-color: #d5a863;
border-inline-start-color: #d5a863;
}
.cockpit-signal-list time {
color: rgba(244, 251, 255, 0.38);
+46 -16
View File
@@ -41,8 +41,6 @@
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(165, 188, 199, 0.35) transparent;
}
:root[data-ui-theme='cyber'] .cctv-panel-inner > .cyber-panel-body {
@@ -68,8 +66,6 @@
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(165, 188, 199, 0.35) transparent;
}
:root[data-ui-theme='cyber'] #pp-toggles > .pp-panel-body > * {
@@ -82,6 +78,8 @@
:root[data-ui-theme='cyber'] {
color-scheme: dark;
--scroll-thumb: rgba(165, 188, 199, 0.35);
--scroll-track: transparent;
--cyber-red: #c45454;
--cyber-red-bright: #e16666;
--cyber-slate: #26343a;
@@ -247,7 +245,7 @@
.street-level-panel-inner,
.global-context-panel-inner
) {
padding-right: 8px;
padding-inline-end: 8px;
}
/* Collapsed right-rail panels otherwise inherit different legacy gutters:
@@ -796,6 +794,12 @@
accent-color: var(--cyber-red);
}
/* Street Level's range wears Cyber's slider accent like every other range. */
:root[data-ui-theme='cyber'] .sl-range {
--accent: inherit;
--accent-glow: inherit;
}
:root[data-ui-theme='cyber']
:where(button, a[href], input, select, textarea):focus-visible {
outline-color: var(--cyber-red-bright) !important;
@@ -1650,12 +1654,27 @@
}
:root[data-ui-theme='cyber'] #left-panel-stack {
/* The stack holds still, so its rail needs no offset (leftPanelRail.js). */
--panel-stack-top-pinned: 1;
top: var(--cyber-left-rail-top) !important;
bottom: calc(2vh + 13rem);
left: 54px;
inset-inline-start: calc(54px + var(--panel-rail-clearance));
width: 300px;
padding-left: 10px;
border-left: 1px solid rgba(196, 84, 84, 0.72);
padding-inline-start: 10px;
border-inline-start: 1px solid transparent;
}
/* An edge line marks each side's open panel, mirrored left and right. */
:root[data-ui-theme='cyber']
body:not(.cockpit-mode)
#left-panel-stack:has(> [data-panel-id]:not(.collapsed, [hidden])) {
border-inline-start-color: rgba(196, 84, 84, 0.72);
}
:root[data-ui-theme='cyber']
body:not(.cockpit-mode)
#right-context-rail:has(> [data-panel-id]:not(.collapsed, [hidden])) {
border-inline-end-color: rgba(196, 84, 84, 0.72);
}
:root[data-ui-theme='cyber']
@@ -1674,6 +1693,13 @@
flex: 0 0 auto;
}
/* The panel rail lists the collapsed panels. */
:root[data-ui-theme='cyber']
#left-panel-stack.layout-focus.layout-exclusive
> [data-panel-id].collapsed {
display: none;
}
:root[data-ui-theme='cyber']
#left-panel-stack
> :is(#data-panel, #scene-panel) {
@@ -1727,8 +1753,11 @@
:root[data-ui-theme='cyber'] #right-context-rail {
--right-display-collapsed-width: var(--cyber-panel-collapsed-width);
--right-context-collapsed-width: var(--cyber-panel-collapsed-width);
right: 62px;
width: 312px;
/* The edge line and its gap keep the panels where they were. */
right: calc(51px + var(--panel-rail-clearance));
width: 323px;
padding-inline-end: 10px;
border-inline-end: 1px solid transparent;
}
/* The map rails now use one visible expanded width. Keeping the rail itself
@@ -2499,15 +2528,13 @@
);
}
:root[data-ui-theme='cyber']
body.cockpit-mode
:is(.cockpit-context-header, .cockpit-signal-header) {
:root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-card-header {
border-bottom-color: rgba(105, 203, 209, 0.25);
}
:root[data-ui-theme='cyber']
body.cockpit-mode
:is(.cockpit-context-kicker, .cockpit-signal-kicker, .cockpit-route-kicker) {
:is(.cockpit-card-kicker, .cockpit-route-kicker) {
color: var(--cockpit-caution);
}
@@ -2722,10 +2749,13 @@
:root[data-ui-theme='cyber'] body.cockpit-mode #left-panel-stack {
top: clamp(164px, 19vh, 204px) !important;
bottom: auto;
/* The cockpit keeps its physical sides in every language (rtl.css): the
utility controls hold the right edge. */
right: auto;
left: 30px;
width: var(--cyber-panel-width);
padding-left: 0;
border-left: 0;
padding-inline-start: 0;
border-inline-start: 0;
}
:root[data-ui-theme='cyber'] body.cockpit-mode #left-panel-stack > #data-panel {
+3 -14
View File
@@ -128,20 +128,9 @@ test('cockpit brief controls keep their English sizes', () => {
test('the count-line class is set only outside English', async () => {
const { setLocale } = await import('../../i18n/index.js');
const { LayerPanel } = await import('../layerPanel.js');
const classes = () => {
const toggled = new Map();
const node = () => ({
textContent: '',
classList: { toggle: (name, on) => toggled.set(name, on) },
appendChild() {},
});
LayerPanel.prototype._syncCount.call(LayerPanel.prototype, node(), node(), {
countLabel: '693 nearby · 93 on screen',
});
return toggled.get('data-toggle-row--count-line');
};
globalThis.document ??= { createElement: () => ({}) };
const { presentLayerCount } = await import('../layerPanel.js');
const classes = () =>
presentLayerCount({ countLabel: '693 nearby · 93 on screen' }).countLine;
try {
setLocale('en');
assert.equal(classes(), false);
+3 -12
View File
@@ -110,20 +110,11 @@
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
/* A visible, non-overlaying affordance that the list continues. */
scrollbar-width: thin;
scrollbar-color: rgba(54, 220, 255, 0.45) transparent;
/* A visible, non-overlaying affordance that the list continues; the
scrollbar itself is the kit's .ui-scroll. */
scrollbar-gutter: stable;
}
.first-run-choices::-webkit-scrollbar {
width: 6px;
}
.first-run-choices::-webkit-scrollbar-thumb {
background: rgba(54, 220, 255, 0.45);
border-radius: 3px;
}
.first-run-choices button {
display: grid;
grid-template-columns: 1.7rem 1fr 1.3rem;
@@ -133,7 +124,7 @@
min-height: 3.55rem;
padding: 0.6rem 0.7rem;
color: rgba(225, 244, 248, 0.92);
text-align: left;
text-align: start;
background: rgba(29, 69, 81, 0.18);
border: 1px solid rgba(100, 200, 220, 0.18);
border-radius: 0.42rem;
+19
View File
@@ -10,6 +10,20 @@
font-family: var(--font-sans);
font-size: 12px;
outline: none;
/* Native number spinners and the open select list follow the dark skin. */
color-scheme: dark;
}
/* A select draws the field's own chevron, not the platform's arrow, so every
dropdown in the app reads the same. The open list stays native. */
select.gev-field {
-webkit-appearance: none;
appearance: none;
padding-inline-end: 22px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='rgba(255,255,255,0.62)' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 10px 6px;
}
.gev-field:focus-visible {
@@ -30,6 +44,11 @@
padding: 2px 6px;
}
select.gev-field--compact {
padding-inline-end: 18px;
background-position: right 6px center;
}
/* Compact inline editors share density and hide native number spinners.
Their containing feature sets width to fit the surrounding readout. */
.gev-field--inline {
+12 -6
View File
@@ -9,6 +9,9 @@
--glass-border: rgba(255, 255, 255, 0.08);
--glass-border-hover: rgba(255, 255, 255, 0.15);
--accent: #00d4ff;
/* Every scrolling surface's scrollbar (the kit's .ui-scroll). */
--scroll-thumb: rgba(0, 212, 255, 0.46);
--scroll-track: rgba(255, 255, 255, 0.06);
--accent-dim: rgba(0, 212, 255, 0.15);
--accent-glow: rgba(0, 212, 255, 0.4);
--text-primary: #e8eaed;
@@ -27,8 +30,11 @@
--dock-label-font-weight: 500;
--dock-label-letter-spacing: 0.13rem;
--transition-smooth: 300ms cubic-bezier(0.4, 0, 0.2, 1);
--left-stack-x: 52px;
--right-rail-x: 52px;
/* The side stacks clear the 52px edge gutter that holds the vertical HUD
readout, plus the floating panel rail (42px) and its 6px gap. */
--panel-rail-clearance: 48px;
--left-stack-x: calc(52px + var(--panel-rail-clearance));
--right-rail-x: calc(52px + var(--panel-rail-clearance));
--left-stack-top: 26vh;
--left-stack-gap: 72px;
--left-collapsed-width: 176px;
@@ -497,7 +503,7 @@ body.cockpit-mode #cockpit-cloud-effects.active {
#title-bar {
position: fixed;
top: 32px;
left: 36px;
inset-inline-start: 36px;
z-index: 100;
pointer-events: none;
user-select: none;
@@ -506,7 +512,7 @@ body.cockpit-mode #cockpit-cloud-effects.active {
#title-bar .title-glow {
position: absolute;
top: -20px;
left: -20px;
inset-inline-start: -20px;
width: 200px;
height: 80px;
background: radial-gradient(ellipse, var(--accent-dim) 0%, transparent 70%);
@@ -631,11 +637,11 @@ body.cockpit-mode #cockpit-cloud-effects.active {
#style-indicator {
position: fixed;
top: 32px;
right: 36px;
inset-inline-end: 36px;
z-index: 100;
pointer-events: none;
user-select: none;
text-align: right;
text-align: end;
}
#style-indicator .indicator-label {
+136
View File
@@ -0,0 +1,136 @@
/* Generic components in src/ui/kit. The surfaces use this application's
glass, accent and type tokens. */
/* The thin themed scrollbar every scrolling surface wears (.ui-scroll). Without it a surface draws the platform's light scrollbar,
because the page declares no dark color-scheme. */
/* Cesium builds its credit list and the rail card builder its lines, so
those two surfaces are named here instead of carrying the class. */
.ui-scroll,
.cesium-credit-lightbox > ul,
.weather-card [data-line-id='status'] {
scrollbar-width: thin;
scrollbar-color: var(--scroll-thumb) var(--scroll-track);
}
.ui-scroll::-webkit-scrollbar,
.cesium-credit-lightbox > ul::-webkit-scrollbar,
.weather-card [data-line-id='status']::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.ui-scroll::-webkit-scrollbar-thumb,
.cesium-credit-lightbox > ul::-webkit-scrollbar-thumb,
.weather-card [data-line-id='status']::-webkit-scrollbar-thumb {
border-radius: 999px;
background: var(--scroll-thumb);
}
.ui-scroll::-webkit-scrollbar-track,
.cesium-credit-lightbox > ul::-webkit-scrollbar-track,
.weather-card [data-line-id='status']::-webkit-scrollbar-track {
background: var(--scroll-track);
}
.ui-icon {
display: inline-block;
line-height: 1;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 20;
user-select: none;
}
.ui-tooltip {
position: relative;
display: inline-flex;
}
.ui-tooltip__tip {
position: absolute;
top: 50%;
z-index: 1;
padding: 4px 8px;
border: 1px solid var(--glass-border);
border-radius: 6px;
background: var(--menu-bg);
color: var(--dock-label-color);
font: var(--dock-label-font-weight) var(--dock-label-font-size) / 1.4
var(--font-sans);
letter-spacing: var(--dock-label-letter-spacing);
white-space: nowrap;
pointer-events: none;
transform: translateY(-50%);
}
/* Sides are for left-to-right; a right-to-left page mirrors them with the
rails, so a tip still opens toward the middle of the screen. */
.ui-tooltip__tip--right {
inset-inline-start: calc(100% + 10px);
}
.ui-tooltip__tip--left {
inset-inline-end: calc(100% + 10px);
}
.ui-rail {
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px;
border: 1px solid var(--glass-border);
border-radius: calc(var(--btn-radius) + 4px);
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
pointer-events: auto;
}
.ui-rail__sep {
height: 1px;
margin: 3px 6px;
background: var(--glass-border);
}
.ui-rail__item {
position: relative;
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 1px solid transparent;
border-radius: var(--btn-radius);
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition:
color var(--transition-fast),
background var(--transition-fast),
border-color var(--transition-fast);
}
.ui-rail__item:hover:not(:disabled) {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.ui-rail__item--on,
.ui-rail__item--on:hover:not(:disabled) {
border-color: var(--accent-dim);
background: var(--accent-dim);
color: var(--accent);
}
.ui-rail__item:disabled {
opacity: 0.35;
cursor: default;
}
.ui-rail__badge {
position: absolute;
top: 1px;
inset-inline-end: 1px;
min-width: 14px;
padding: 0 3px;
border-radius: 7px;
background: var(--accent);
color: var(--bg-dark);
font: 600 9px/14px var(--font-mono);
text-align: center;
}
/* Application view hosts (src/ui/views): the view's own elements lay out as
children of the page, as the static markup they replace did. */
.gev-view {
display: contents;
}
+39 -30
View File
@@ -3,7 +3,7 @@
position: fixed;
top: var(--left-stack-safe-top, var(--left-stack-top));
bottom: var(--left-stack-safe-bottom, 4vh);
left: var(--left-stack-x);
inset-inline-start: var(--left-stack-x);
z-index: 100;
width: 360px;
display: flex;
@@ -84,29 +84,9 @@
overflow-y: auto;
overscroll-behavior: contain;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.5) rgba(255, 255, 255, 0.04);
scrollbar-gutter: stable;
}
.data-toggle-list::-webkit-scrollbar {
width: 6px;
}
.data-toggle-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.04);
border-radius: 999px;
}
.data-toggle-list::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(0, 212, 255, 0.65),
rgba(0, 212, 255, 0.2)
);
border-radius: 999px;
}
.data-layer-group-heading {
margin: 8px 10px 2px;
padding-top: 12px;
@@ -197,11 +177,11 @@
#right-context-rail #weather-panel.collapsed,
#right-context-rail #recent-imagery-panel.collapsed {
width: var(--right-display-collapsed-width);
margin-left: auto;
margin-inline-start: auto;
}
#right-context-rail > #pp-toggles.collapsed {
width: var(--right-display-collapsed-width);
margin-left: auto;
margin-inline-start: auto;
}
#right-context-rail > #pp-toggles.collapsed .pp-header-row {
width: 100%;
@@ -240,7 +220,7 @@
width: var(--right-context-collapsed-width);
/* Keep the compact Context tab on the rail edge rather than leaving it
stranded at the rail's expanded-width origin. */
margin-left: auto;
margin-inline-start: auto;
}
#right-context-rail
#global-context-panel.collapsed
@@ -260,6 +240,39 @@
#right-context-rail.layout-exclusive > [data-panel-id].collapsed {
display: none;
}
#left-panel-stack.layout-exclusive > [data-panel-id].collapsed {
display: none;
}
/* Panel rails (src/ui/panelNavigation.js). Each rail lives inside its stack,
so it shows and hides with the stack, and floats in the screen-edge gutter.
The stack's layout offsets it back to its resting top, so it holds still
when an opened panel moves the stack. */
.gev-panel-rail {
position: absolute;
top: var(--panel-rail-offset, 0px);
z-index: 1;
pointer-events: auto;
transition: top var(--transition-fast);
}
.gev-panel-rail--left {
inset-inline-end: calc(100% + 6px);
}
.gev-panel-rail--right {
inset-inline-start: calc(100% + 6px);
}
body.mobile-shell #left-panel-stack > .gev-panel-rail,
body.mobile-shell #right-context-rail > .gev-panel-rail,
body.cockpit-mode .gev-panel-rail {
display: none;
}
/* Clean view and recording hide the panels one by one, not their stacks. */
body.ui-clean-view .gev-panel-rail,
body.recording-mode .gev-panel-rail {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
#right-context-rail.layout-focus > [data-panel-id].collapsed {
display: block;
flex: 0 0 auto;
@@ -281,8 +294,6 @@
#right-context-rail.layout-focus > #pp-toggles:not(.collapsed) {
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.4) transparent;
}
#right-context-rail.layout-focus
> #weather-panel:not(.collapsed)
@@ -385,7 +396,7 @@
gap: 10px;
padding: 1px 3px 7px;
border-bottom: 1px solid rgba(34, 230, 230, 0.14);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
#space-mission-panel-host .space-mission-roster-header > div {
display: grid;
@@ -415,8 +426,6 @@
overflow-x: hidden;
overflow-y: auto;
padding-right: 3px;
scrollbar-color: rgba(34, 230, 230, 0.38) transparent;
scrollbar-width: thin;
}
#space-mission-panel-host .space-mission-roster-item {
position: relative;
@@ -431,7 +440,7 @@
border-radius: 4px;
color: #d8ffff;
background: rgba(42, 113, 171, 0.045);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
text-align: left;
cursor: pointer;
transition:
+1 -1
View File
@@ -1,7 +1,7 @@
/* ============================================================
Bottom-right corner row: the POWER UP chip (provider-settings.css)
and, in the very corner, the interface language picker
(src/ui/localePicker.js). Logical properties throughout, so a
(src/ui/views/panels/LocalePicker.vue). Logical properties throughout, so a
right-to-left page mirrors the row and the list.
============================================================ */
+5 -4
View File
@@ -84,10 +84,10 @@ body.mobile-shell[data-mobile-sheet] #mobile-sheet-backdrop:not([hidden]) {
background: rgba(0, 0, 0, 0.18);
}
/* ── Top bar: logo left, globe actions right ───────────────────────────── */
/* ── Top bar: logo at the start, globe actions at the end ──────────────── */
body.mobile-shell #title-bar {
top: calc(var(--mobile-top-inset) + 10px);
left: 14px;
inset-inline-start: 14px;
}
body.mobile-shell #title-bar .title-logo {
@@ -101,7 +101,8 @@ body.mobile-shell #title-bar .subtitle {
body.mobile-shell #top-center-actions {
top: calc(var(--mobile-top-inset) + 10px);
right: 12px;
inset-inline-start: auto;
inset-inline-end: 12px;
}
/* ── HUD: one compact readout under the top bar ────────────────────────── */
@@ -408,7 +409,7 @@ body.mobile-shell:not(.cockpit-mode) #view-switcher {
body.mobile-shell #corner-actions {
top: calc(var(--mobile-top-inset) + 60px);
right: 12px;
inset-inline-end: 12px;
bottom: auto;
}
+1 -1
View File
@@ -152,7 +152,7 @@
overflow: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: var(--glass-border-hover) transparent;
scrollbar-color: var(--scroll-thumb) var(--scroll-track);
/* Keep an inset focus ring inside the intentional scrolling boundary. */
padding: 3px;
}
-10
View File
@@ -184,19 +184,9 @@
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(54, 220, 255, 0.45) transparent;
scrollbar-gutter: stable;
}
.key-setup-rows::-webkit-scrollbar {
width: 6px;
}
.key-setup-rows::-webkit-scrollbar-thumb {
background: rgba(54, 220, 255, 0.45);
border-radius: 3px;
}
.key-setup-row {
display: grid;
gap: 0.35rem;
+9 -8
View File
@@ -18,8 +18,6 @@
border-radius: 8px;
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, 0.46) rgba(255, 255, 255, 0.06);
}
#radio-panel .panel-header {
margin-bottom: 0;
@@ -51,7 +49,8 @@
#radio-filter {
/* Radio owns its compact density and arrow space; shared fields paint the
palette and disabled state, including this intentional theme exception. */
padding: 7px 24px 7px 8px;
padding-block: 7px;
padding-inline: 8px 24px;
border-radius: 5px;
font: 9px/1.2 var(--font-mono);
--gev-field-border-color: rgba(65, 220, 231, 0.28);
@@ -697,7 +696,7 @@ html:not([lang='en']) .data-toggle-right {
}
/* Row text pieces that must not break inside: a hyphenated code (ADS-B) and
the space before a " · " separator (see setRowText in layerPanel.js). */
the space before a " · " separator (see rowTextRuns in layerRowText.js). */
html:not([lang='en']) .data-keep {
white-space: nowrap;
}
@@ -1092,11 +1091,12 @@ html:not([lang='en']) .data-row-list-text {
.sdr-radio-card select,
.sdr-radio-card input[type='number'] {
min-height: 28px;
border: 1px solid rgba(255, 183, 82, 0.28);
border-radius: 5px;
color: #ffd79a;
background: rgba(38, 25, 8, 0.52);
font: 8px/1 var(--font-mono);
/* The shared field paints the surface (and the select's chevron). */
--gev-field-border-color: rgba(255, 183, 82, 0.28);
--gev-field-color: #ffd79a;
--gev-field-background: rgba(38, 25, 8, 0.52);
}
.sdr-radio-card button {
padding: 6px 8px;
@@ -1129,7 +1129,8 @@ html:not([lang='en']) .data-row-list-text {
.sdr-gain-row select {
flex: 1;
min-width: 0;
padding: 5px 7px;
padding-block: 5px;
padding-inline: 7px 22px;
}
.sdr-adsb-stats {
display: grid;
-3
View File
@@ -46,8 +46,6 @@
max-height: calc(var(--right-stack-max-height, 50svh) - 64px);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) transparent;
}
#recent-imagery-panel.collapsed #recent-imagery-panel-body,
#recent-imagery-panel.collapsed #recent-imagery-panel-count,
@@ -168,7 +166,6 @@
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x proximity;
scrollbar-width: thin;
outline: none;
border-radius: 6px;
}
+3 -3
View File
@@ -25,7 +25,7 @@
#title-bar {
top: 20px;
left: 20px;
inset-inline-start: 20px;
}
#top-center-actions {
@@ -55,7 +55,7 @@
#title-bar .frame-rate-readout {
position: fixed;
top: 52px;
left: 20px;
inset-inline-start: 20px;
margin: 0;
font-size: 10px;
line-height: 12px;
@@ -245,7 +245,7 @@
/* 16px keeps the Radio broadcast waves (15px above the logo) on-screen. */
#title-bar {
top: 16px;
left: 16px;
inset-inline-start: 16px;
}
#title-bar h1 > span:last-child,
+222
View File
@@ -0,0 +1,222 @@
/* Right-to-left languages (Arabic). The chrome mirrors through logical
properties in the component sheets (inset-inline-*, margin-inline-*); this
sheet holds only what logical properties cannot express. */
/* Arabic letters join: letter-spacing tears every word into separate glyphs.
Latin marked translate="no" and the intel HUD's Latin telemetry keep their
tracking; the HUD's translated lines are released below. Unlike cjk.css, a
container that merely holds a translate="no" span is not spared: its own
Arabic text would come apart. */
html:lang(ar)
body
:not(#intel-hud, #intel-hud *, [translate='no'], [translate='no'] *) {
letter-spacing: 0 !important;
}
/* The intel HUD is an instrument overlay: corners, brackets, coordinates and
sensor codes keep the English geometry. Its translated lines still read
right to left inside that frame. */
html[dir='rtl'] #intel-hud {
direction: ltr;
}
html[dir='rtl'] #intel-hud :is(.hud-mode, .hud-summary-label, .hud-summary) {
unicode-bidi: plaintext;
text-align: left;
letter-spacing: 0 !important;
}
/* Font stacks: JetBrains Mono and Inter have no Arabic glyphs, and the
generic monospace fallback is a fixed-pitch Arabic face that spaces every
letter apart. Noto Sans Arabic (loaded with the other Google fonts; its
files download only when the page shows Arabic) leads the mono stack, so
the spaces between Arabic words come from it too and not from the
fixed-pitch Latin face. The intel and cockpit HUDs keep JetBrains Mono for
their Latin telemetry and column-aligned digits. */
html:lang(ar) {
--font-mono:
'Noto Sans Arabic', 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Geeza Pro',
Tahoma, monospace;
--font-sans:
'Inter', 'Noto Sans Arabic', -apple-system, BlinkMacSystemFont, 'Geeza Pro',
Tahoma, sans-serif;
}
/* The Global Context lines and their buttons set ui-monospace through the
`font` shorthand; route them through the Arabic-aware stack too. */
html:lang(ar)
:is(
#military-awareness-panel,
.military-awareness-controls button,
.military-awareness-standby button,
.military-awareness-compass-label,
.military-awareness-compass-heading,
.military-awareness-direction-label,
.tr3b-toggle
) {
font-family: var(--font-mono);
}
html:lang(ar) :is(#intel-hud, #cockpit-hud) {
--font-mono:
'JetBrains Mono', 'SF Mono', 'Fira Code', 'Noto Sans Arabic', 'Geeza Pro',
Tahoma, monospace;
}
/* The Display/Context rail moves to the left edge. It keeps a physical
`right` in layers.css because the attribution-clearance model
(creditAttribution.test.mjs) reads that rule; the swap only applies above
the 720px band, where the rail is not edge-to-edge. */
@media (min-width: 721px) {
html[dir='rtl'] #right-context-rail {
right: auto;
left: var(--right-rail-x);
}
}
@media (min-width: 961px) and (min-height: 650px) {
html[dir='rtl'][data-ui-theme='cyber'] #right-context-rail {
right: auto;
left: calc(51px + var(--panel-rail-clearance));
}
}
/* The dock's wings swap sides (Location on the right, Visual presets on the
left), so each slide-up tray is anchored from the wing's outer edge and
centred on the whole dock from there, as it is in left-to-right. */
html[dir='rtl'] #command-dock #location-bar .dock-popover-content {
right: calc((var(--dock-unified-width) - var(--dock-location-open)) / 2);
left: auto;
transform-origin: bottom right;
}
html[dir='rtl'] #command-dock #control-panel .dock-popover-content {
right: auto;
left: calc((var(--dock-unified-width) - var(--dock-presets-open)) / 2);
transform-origin: bottom left;
}
@media (max-width: 900px) {
html[dir='rtl'] #command-dock #location-bar .dock-popover-content {
right: calc(
(var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2
);
}
html[dir='rtl'] #command-dock #control-panel .dock-popover-content {
left: calc(
(var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2
);
}
}
/* The tray pin sits on the tray's top end corner: top-left here. */
html[dir='rtl'] #command-dock :is(#location-bar, #control-panel) .dock-pin-btn {
right: auto;
left: -1rem;
}
/* The right rail sits on the left: its expand/collapse arrows point the
other way, and the start window's "go" arrows point along the reading
direction. */
html[dir='rtl'] #right-context-rail .panel-collapse-btn,
html[dir='rtl'] .first-run-arrow {
transform: scaleX(-1);
}
html[dir='rtl'] .first-run-choices button:hover .first-run-arrow,
html[dir='rtl'] .first-run-choices button:focus-visible .first-run-arrow {
transform: translateX(-2px) scaleX(-1);
}
/* The cockpit is an aircraft panel, not page chrome: its windows keep their
physical sides, so the data-layer stack goes back to the left edge instead
of landing on the utility controls. Gauges, tapes and the roll counters stay
left to right (speed left, altitude right, -10 not 10-); the cards keep
reading right to left. */
html[dir='rtl'] body.cockpit-mode #left-panel-stack {
left: var(--left-stack-x);
right: auto;
}
html[dir='rtl']
#cockpit-hud
:is(
.cockpit-instrument-row,
.cockpit-compass,
.cockpit-pitch-rail,
.cockpit-altitude-rim,
.cockpit-roll-arc,
.cockpit-vision-controls
) {
direction: ltr;
}
/* Feed text (news headlines, station names) arrives in its own language:
each line takes its direction from its first letter, so an English headline
reads and aligns left to right instead of moving its punctuation. */
html[dir='rtl']
:is(
.cockpit-news-list li *,
#radio-station-name,
#radio-station-tags,
.cockpit-radio-station
) {
unicode-bidi: plaintext;
}
/* Clock readouts (~0:05 / 5:19) are numbers, not prose. */
html[dir='rtl'] #bhote-koshi-event-panel [data-role='time'] {
direction: ltr;
unicode-bidi: isolate;
}
/* Physical controls keep their left-to-right sense: FM seek down/up and the
radio dial. The worded PREV / PLAY / NEXT row follows the reading
direction like any other button row. */
html[dir='rtl'] :is(.sdr-seek-row, .radio-tuner) {
direction: ltr;
}
/* Canvas labels (installations, map cards, detections) are measured and drawn
with left-to-right geometry: a card's text starts at its left padding. A
canvas inherits the page direction, which turned that start into the right
edge and pushed every title out of its card. */
html[dir='rtl'] canvas {
direction: ltr;
}
/* Scene callouts on the screen whiteboard: the card grows to the right of the
text's x, so right-to-left text ends there instead of starting there. The
injected stack (JetBrains Mono) has no Arabic glyphs; use the Arabic-aware
one. */
html[dir='rtl'] .gev-anno-text {
text-anchor: end;
font-family: var(--font-mono);
letter-spacing: 0;
}
/* Position counters built from bare numbers (01 / 25, 1 / 3) read as a number
pair, left to right, as an unspaced 1/3 does; otherwise the slash swaps
them and the current item looks like the total. Translated counters say
«من» instead. */
html[dir='rtl']
:is(
#bhote-koshi-event-panel [data-role='beat-index'],
#cockpit-brief-position,
[data-mission-index]
) {
direction: ltr;
unicode-bidi: isolate;
}
/* The radio tuner header is a left-to-right instrument row; its channel
readout («القناة 01 من 750») still reads in its own language. */
html[dir='rtl'] #radio-tuner-value {
unicode-bidi: plaintext;
}
/* The select chevron sits at the end of the field, where the text stops
(form-field.css pads that side through padding-inline-end). */
html[dir='rtl'] select.gev-field {
background-position: left 8px center;
}
html[dir='rtl'] select.gev-field--compact {
background-position: left 6px center;
}
/* A legend's range is printed under its colour ramp, which always runs low
to high from left to right; the numbers keep that order. */
html[dir='rtl'] .rail-card-scale {
direction: ltr;
}
-9
View File
@@ -265,15 +265,6 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
counter-reset: scene-shot;
}
.scene-shot-list::-webkit-scrollbar {
width: 6px;
}
.scene-shot-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.16);
border-radius: 999px;
}
.scene-shot-empty {
border: 1px dashed rgba(255, 255, 255, 0.14);
border-radius: 8px;
+8 -8
View File
@@ -243,20 +243,20 @@ html:not([lang='en']) #global-loading-detail {
/* ── Split-flap chip text ─────────────────────────────────────────
Per-character departure-board flip when a chip label changes.
Driven entirely by src/splitFlap.js, which rebuilds the label as
one span per character on the change and strips them again once
the cascade lands.
Planned by src/splitFlap.js and rendered by SplitFlapText.vue, which
lays out one cell per character on the change and strips them again
once the cascade lands.
The DOM text is the truth and its node never moves. A chip label is
upgraded once into a permanent shell — a .gev-flap-text span holding
one long-lived Text node, plus an aria-hidden .gev-flap-cells
sibling — and from then on a label change is only `node.data = next`.
a .gev-flap-text span holding one long-lived Text node, plus an
aria-hidden .gev-flap-cells sibling, and a label change is only
`node.data = next`.
During a cascade the host takes .gev-flap-active, which clips the
real text out of view and lets the cells stand in for it. The cells
carry no text at all: both glyphs are generated content (::before =
outgoing, ::after = incoming), and generated content never reaches
textContent. That is what keeps QA pins and the traffic chip's own
`textContent !==` guard honest, and it lets the cells be rebuilt
textContent. That is what keeps QA pins honest, and it lets the
cells be rebuilt
freely while the aria-live region sees only one characterData
change per label update.
+3 -12
View File
@@ -39,8 +39,6 @@
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;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) rgba(255, 255, 255, 0.06);
}
/* Class-qualified, not id-qualified, so a theme's header rule still wins. */
@@ -236,17 +234,10 @@ html:not([lang='en']) .sl-meta-row .sl-nearest-btn {
gap: 4px 10px;
}
/* The shared slider surface, in Street Level's green. */
.sl-range {
width: 100%;
min-width: 0;
margin: 0;
accent-color: var(--sl-green);
cursor: pointer;
}
.sl-range:focus-visible {
outline: 1px solid var(--accent);
outline-offset: 3px;
--accent: var(--sl-green);
--accent-glow: color-mix(in srgb, var(--sl-green) 55%, transparent);
}
/* The value and its note share a line: ANY DATE (by upload date). */
+9 -8
View File
@@ -26,7 +26,7 @@
0 0 1.2rem rgba(0, 212, 255, 0.08);
backdrop-filter: blur(24px) saturate(1.3);
-webkit-backdrop-filter: blur(24px) saturate(1.3);
text-align: left;
text-align: start;
transform: translateX(-50%);
transform-origin: bottom center;
animation: gev-voice-card-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
@@ -95,7 +95,7 @@
}
.gev-voice-card-dismiss {
margin-left: auto;
margin-inline-start: auto;
width: 1.1rem;
height: 1.1rem;
padding: 0;
@@ -288,7 +288,7 @@
content: counter(gev-referent);
display: inline-block;
min-width: 0.9rem;
margin-right: 0.3rem;
margin-inline-end: 0.3rem;
color: rgba(54, 220, 255, 0.72);
font-family: var(--font-mono);
font-size: 0.52rem;
@@ -415,7 +415,7 @@ body.recording-mode .gev-pointer-reticle {
-webkit-backdrop-filter: blur(24px) saturate(1.3);
color-scheme: dark;
font: 0.75rem/1.45 var(--font-sans);
text-align: left;
text-align: start;
transform: translateX(-50%);
}
:root[data-ui-theme='cyber'] #gev-voice-control .gev-voice-panel {
@@ -443,7 +443,7 @@ body.recording-mode .gev-pointer-reticle {
letter-spacing: 0.14em;
}
.gev-voice-panel-tabs {
margin-left: auto;
margin-inline-start: auto;
}
#gev-voice-control .gev-voice-panel-header .gev-voice-panel-tab {
display: inline-flex;
@@ -467,7 +467,8 @@ body.recording-mode .gev-pointer-reticle {
min-height: 2rem;
box-sizing: border-box;
color: var(--text-primary);
background: rgba(255, 255, 255, 0.04);
/* background-color, so a .gev-field select keeps its chevron. */
background-color: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
border-radius: 0.3rem;
font: inherit;
@@ -504,7 +505,6 @@ body.recording-mode .gev-pointer-reticle {
min-height: 0;
overflow: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
padding: 0.55rem 0.75rem 3px 3px;
line-height: 1.45;
overflow-wrap: anywhere;
@@ -553,7 +553,8 @@ body.recording-mode .gev-pointer-reticle {
#gev-voice-timeout-select {
flex: 1 1 auto;
min-width: 0;
padding: 0 0.45rem;
padding-block: 0;
padding-inline: 0.45rem 22px;
}
#gev-voice-timeout-input {
flex: 0 0 4.5rem;
+2 -3
View File
@@ -180,12 +180,11 @@ html:not([lang='en'])
min-width: 0;
width: 100%;
min-height: 30px;
color: var(--text-primary);
background: #09121b;
border: 1px solid var(--glass-border);
border-radius: 4px;
font: inherit;
font-size: 0.65rem;
--gev-field-background: #09121b;
--gev-field-border-color: var(--glass-border);
}
#gev-voice-control:hover .gev-voice-provider-field,
#gev-voice-control:focus-within .gev-voice-provider-field {
-2
View File
@@ -42,8 +42,6 @@
max-height: calc(var(--right-stack-max-height, 50svh) - 64px);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) transparent;
}
#weather-panel.collapsed #weather-panel-body,
#weather-panel[hidden],