Merge main into the Utilities panel, rebuilt on the Vue panel rail

main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.

- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
  item (handyman). LayerPanel renders the Directions rows into its list with
  LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
  which render what the draw tool publishes to the display store and call
  its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
  drawMarkList.js is gone. A row's fly-to is an immediate navigation like
  Import's ('drawing' gets its Cockpit refusal), the name field keeps a
  draft through v-field-value, and the colours are one radio group with a
  single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
  buttons.
- The catalogs take main's languages with this branch's strings, Arabic
  included, and the tests follow the views to their new homes.
This commit is contained in:
Said Turksever committed 2026-11-02 16:43:29 +00:00
commit 4ebb287d36
699 files changed
+46673 -20476

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);
+4 -4
View File
@@ -17,10 +17,10 @@
display: inline-flex;
align-items: center;
gap: 0.42rem;
color: rgba(206, 232, 239, 0.72);
font-size: 0.58rem;
font-weight: 500;
letter-spacing: 0.13rem;
color: var(--dock-label-color);
font-size: var(--dock-label-font-size);
font-weight: var(--dock-label-font-weight);
letter-spacing: var(--dock-label-letter-spacing);
}
#command-dock .dock-label-icon {
+179 -30
View File
@@ -81,16 +81,104 @@
white-space: nowrap;
}
/* A longer translated title in a collapsed rail launcher tightens its
tracking rather than pushing the disclosure button out of the card. */
html:not([lang='en'])
#right-context-rail
.panel-collapsible.collapsed
.panel-title {
/* Side panel titles read like the command dock's labels: one type for every
panel, collapsed or open, and bright enough to read over the globe. The
dock's type is in rem, but a collapsed launcher is a fixed 176px, so the
rail caps it at its 16px-root size: a larger browser font must not cut
Recent Imagery short. */
:is(#left-panel-stack, #right-context-rail)
:is(.panel-header > .panel-title, .pp-header-label) {
font-family: var(--font-mono);
font-size: min(var(--dock-label-font-size), 9.28px);
font-weight: var(--dock-label-font-weight);
letter-spacing: min(var(--dock-label-letter-spacing), 2.08px);
text-transform: uppercase;
}
/* A hovered panel sets --rail-label-hover on itself (see below); its own
header picks it up. Radio sits inside Context and keeps its own colour. */
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
:is(
.panel-header > .panel-title,
.pp-header-label,
:is(.panel-header, .pp-header-row) > .panel-collapse-btn
) {
color: var(--rail-label-hover, var(--dock-label-color));
}
#radio-panel {
--rail-label-hover: initial;
}
/* A title never pushes its toggle out of the card: one that does not fit
ends in an ellipsis. Cyber draws a marker left of the title, which a clip
would cut, and keeps its own header layout. */
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
:is(.panel-header > .panel-title, .pp-header-label) {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 1.5px;
}
/* A collapsed launcher is 176px wide and its divider has no room to draw,
so the header drops it and packs closer. English titles such as Recent
Imagery then fit whole. Only the rail's own launchers: Radio, nested in
Context, keeps its divider. */
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
> .panel-collapsible.collapsed
> [class$='-panel-inner']
> .panel-header,
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
> #pp-toggles.collapsed
> .pp-header-row {
gap: 8px;
}
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
> .panel-collapsible.collapsed
> [class$='-panel-inner']
> .panel-header
> .panel-divider,
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
> #pp-toggles.collapsed
> .pp-header-row
> .panel-divider {
display: none;
}
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
> .panel-collapsible.collapsed
> [class$='-panel-inner']
> .panel-header
> .panel-collapse-btn,
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
> #pp-toggles.collapsed
> .pp-header-row
> .panel-collapse-btn {
margin-left: auto;
}
/* Translated titles run longer (Aktuelle Bilddaten), so a collapsed rail
launcher sets them in compact type rather than pushing the disclosure
button out of the card or cutting the word short. Chinese and Japanese
titles fit at full size and stay at or above their 9px floor (cjk.css);
Radio, nested in Context, is not a launcher. */
html:not([lang='en']):not(:lang(zh)):not(:lang(ja))
:is(#left-panel-stack, #right-context-rail)
> .panel-collapsible.collapsed
> [class$='-panel-inner']
> .panel-header
> .panel-title {
font-size: 8px;
letter-spacing: 1px;
}
.panel-divider {
@@ -109,6 +197,8 @@ html:not([lang='en'])
font-family: var(--font-mono);
font-size: 13px;
line-height: 1;
/* A long title or a header action must not squeeze the toggle. */
flex-shrink: 0;
cursor: pointer;
transition:
background-color var(--transition-fast),
@@ -116,17 +206,52 @@ html:not([lang='en'])
color var(--transition-fast);
}
/* A long translated title must not squeeze the toggle into a sliver. */
html:not([lang='en']) .panel-collapse-btn {
flex-shrink: 0;
}
.panel-collapse-btn:hover {
border-color: var(--glass-border-hover);
color: var(--text-primary);
background: rgba(255, 255, 255, 0.07);
}
/* A side panel answers the pointer as the command dock's trays do: a
brighter edge and an accent title and toggle. Touch screens have no hover,
so a tap must not leave a panel lit. */
@media (hover: hover) and (pointer: fine) {
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
:where(
.data-panel-inner,
.utilities-panel-inner,
.scene-panel-inner,
.cctv-panel-inner,
.weather-panel-inner,
.recent-imagery-panel-inner,
.street-level-panel-inner,
.global-context-panel-inner,
#pp-toggles:not(.collapsed),
#pp-toggles.collapsed > .pp-header-row
) {
transition: border-color var(--transition-fast);
}
:root:not([data-ui-theme='cyber'])
:is(#left-panel-stack, #right-context-rail)
:where(
.data-panel-inner,
.utilities-panel-inner,
.scene-panel-inner,
.cctv-panel-inner,
.weather-panel-inner,
.recent-imagery-panel-inner,
.street-level-panel-inner,
.global-context-panel-inner,
#pp-toggles:not(.collapsed),
#pp-toggles.collapsed > .pp-header-row
):hover {
--rail-label-hover: var(--accent);
border-color: var(--glass-border-hover);
}
}
/* ── Button Grid ──────────────────────────────── */
.button-grid {
display: flex;
@@ -431,12 +556,21 @@ html:not([lang='en']) .panel-collapse-btn {
}
#pp-toggles .pp-header-label {
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 1.4px;
color: var(--text-dim);
}
/* Display's title and toggle sit where the other panels' do: 14px of panel
padding plus a 4px header inset. The cyber theme sets its own insets. */
:root:not([data-ui-theme='cyber']) #pp-toggles.collapsed > .pp-header-row {
padding: 8px 18px;
}
:root:not([data-ui-theme='cyber'])
#pp-toggles:not(.collapsed)
> .pp-header-row {
padding: 0 10px;
}
#pp-toggles > .pp-header-row .pp-collapse-btn {
width: 20px;
height: 20px;
@@ -445,6 +579,16 @@ html:not([lang='en']) .panel-collapse-btn {
padding: 0;
}
/* In the default theme Display's toggle matches the other panels' 22px. */
:root:not([data-ui-theme='cyber'])
#pp-toggles
> .pp-header-row
.pp-collapse-btn {
width: 22px;
height: 22px;
font-size: 13px;
}
#pp-toggles.collapsed {
gap: 0;
}
@@ -783,21 +927,6 @@ html:not([lang='en']) .pp-slider-row > .pp-slider-mini-label {
border-radius: 50%;
cursor: pointer;
}
#utilities-panel .draw-color[data-color='primary'] {
background: #8be9ff;
}
#utilities-panel .draw-color[data-color='amber'] {
background: #ffb547;
}
#utilities-panel .draw-color[data-color='cyan'] {
background: #39d0ff;
}
#utilities-panel .draw-color[data-color='green'] {
background: #5dff9f;
}
#utilities-panel .draw-color[data-color='red'] {
background: #ff6b6b;
}
/* The swatch keeps its own colour in every theme; only a ring marks it. */
#utilities-panel .draw-color:is(.active, :hover, :focus-visible) {
border-color: rgba(0, 0, 0, 0.45);
@@ -859,6 +988,8 @@ html:not([lang='en']) .pp-slider-row > .pp-slider-mini-label {
opacity: 0.45;
}
.draw-mark-colors {
/* Right after its swatch for keyboard and screen readers, shown last. */
order: 1;
flex-basis: 100%;
padding: 2px 0 4px 16px;
}
@@ -1207,3 +1338,21 @@ html:not([lang='en']) .param-slider-row {
flex: 1;
min-width: 0;
}
/* Touch: Draw's swatches keep their small dots but answer a 24px target. */
@media (pointer: coarse) {
#utilities-panel .draw-color,
#utilities-panel .draw-mark-swatch {
position: relative;
}
#utilities-panel .draw-color::before,
#utilities-panel .draw-mark-swatch::before {
position: absolute;
inset: 50% auto auto 50%;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
content: '';
}
}
+52 -17
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;
@@ -203,6 +201,7 @@
.recent-imagery-panel-inner,
.street-level-panel-inner,
.scene-panel-inner,
.utilities-panel-inner,
.global-context-panel-inner,
.radio-panel-inner,
#pp-toggles:not(.collapsed),
@@ -224,6 +223,7 @@
.recent-imagery-panel-inner,
.street-level-panel-inner,
.scene-panel-inner,
.utilities-panel-inner,
.global-context-panel-inner,
.radio-panel-inner
)
@@ -247,7 +247,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:
@@ -306,6 +306,7 @@
.recent-imagery-panel-inner,
.street-level-panel-inner,
.scene-panel-inner,
.utilities-panel-inner,
.global-context-panel-inner,
.radio-panel-inner
)
@@ -322,6 +323,7 @@
.recent-imagery-panel-inner,
.street-level-panel-inner,
.scene-panel-inner,
.utilities-panel-inner,
.global-context-panel-inner,
.radio-panel-inner
)
@@ -346,6 +348,7 @@
.recent-imagery-panel-inner,
.street-level-panel-inner,
.scene-panel-inner,
.utilities-panel-inner,
.global-context-panel-inner,
.radio-panel-inner
)
@@ -804,6 +807,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;
@@ -1658,12 +1667,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']
@@ -1682,6 +1706,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, #utilities-panel, #scene-panel) {
@@ -1698,7 +1729,7 @@
:root[data-ui-theme='cyber']
#left-panel-stack
> :is(#data-panel, #utilities-panel, #scene-panel).collapsed
:is(.data-panel-inner, .scene-panel-inner) {
:is(.data-panel-inner, .utilities-panel-inner, .scene-panel-inner) {
box-sizing: border-box;
height: 50px;
min-height: 50px;
@@ -1735,8 +1766,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
@@ -2507,15 +2541,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);
}
@@ -2730,10 +2762,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
@@ -126,20 +126,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 {
+17 -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;
@@ -21,9 +24,17 @@
--panel-radius: 16px;
--btn-radius: 10px;
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
/* Label type shared by the command dock and the side panel titles. */
--dock-label-color: rgba(206, 232, 239, 0.72);
--dock-label-font-size: 0.58rem;
--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;
@@ -492,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;
@@ -501,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%);
@@ -626,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 {
+181
View File
@@ -0,0 +1,181 @@
/* 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;
}
/* A collapsible rail shows each item as an icon and its label while no panel
is open, and folds the labels away once one is. The collapsed item keeps
the plain rail's 32px square. */
.ui-rail--collapsible {
transition: border-radius var(--transition-fast);
}
.ui-rail--collapsible .ui-rail__item {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
min-width: 32px;
padding: 0 6px;
transition:
color var(--transition-fast),
background var(--transition-fast),
border-color var(--transition-fast),
border-radius var(--transition-fast);
}
.ui-rail__label {
max-width: 0;
overflow: hidden;
opacity: 0;
white-space: nowrap;
text-overflow: ellipsis;
font: var(--dock-label-font-weight) var(--dock-label-font-size) / 1
var(--font-sans);
letter-spacing: var(--dock-label-letter-spacing);
transition:
max-width var(--transition-fast),
margin var(--transition-fast),
opacity var(--transition-fast);
}
.ui-rail--expanded {
border-radius: 21px;
}
.ui-rail--expanded .ui-rail__item {
border-radius: 16px;
}
.ui-rail--expanded .ui-rail__label {
max-width: 220px;
margin-inline: 8px 8px;
opacity: 1;
}
.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;
}
+56 -53
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;
@@ -40,7 +40,8 @@
transform: translateY(0);
}
#data-panel .panel-glow {
#data-panel .panel-glow,
#utilities-panel .panel-glow {
position: absolute;
inset: -20px;
background: radial-gradient(
@@ -53,7 +54,8 @@
pointer-events: none;
}
.data-panel-inner {
.data-panel-inner,
.utilities-panel-inner {
position: relative;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
@@ -84,29 +86,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 +179,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 +222,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
@@ -249,7 +231,7 @@
box-sizing: border-box;
justify-content: center;
overflow: visible;
padding: 8px 10px 8px 14px;
padding: 8px 14px;
}
#right-context-rail #global-context-panel.collapsed .panel-header {
margin: 0;
@@ -260,6 +242,42 @@
#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);
}
/* Each rail keeps its outer edge at the screen-side clearance, so a rail that
widens (labelled pills while its side has no open panel) grows toward the
middle of the screen. Collapsed, it sits 6px outside its stack. */
.gev-panel-rail--left {
inset-inline-start: calc(-1 * var(--panel-rail-clearance));
}
.gev-panel-rail--right {
inset-inline-end: calc(-1 * var(--panel-rail-clearance));
}
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 +299,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 +401,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 +431,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 +445,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:
@@ -989,13 +1003,6 @@
#global-context-panel .panel-glow {
display: none;
}
/* Expanded, its section titles (Context, Radio) stay legible over bright
imagery. Cyber keeps its own red titles; collapsed launchers match siblings. */
:root:not([data-ui-theme='cyber'])
#global-context-panel:not(.collapsed)
.panel-title {
color: var(--text-secondary);
}
.global-context-panel-inner {
position: relative;
display: flex;
@@ -1011,6 +1018,15 @@
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
}
/* Data Layers and Context share the 14px side padding of the other default
theme panels, so every title and toggle sits 19px from its edge. Cyber
keeps its own gutters. */
:root:not([data-ui-theme='cyber'])
:is(.data-panel-inner, .global-context-panel-inner) {
padding-right: 14px;
padding-left: 14px;
}
.global-context-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
@@ -1254,19 +1270,6 @@ body.recording-mode #right-context-rail > [data-panel-id] {
width: var(--panel-expanded-width);
}
#utilities-panel .panel-glow {
position: absolute;
inset: -20px;
background: radial-gradient(
ellipse at center,
var(--accent-dim) 0%,
transparent 60%
);
filter: blur(20px);
opacity: 0.25;
pointer-events: none;
}
/* The layer list takes its natural height; the panel scrolls as a whole. */
.utilities-list {
flex: 0 0 auto;
+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 ────────────────────────── */
@@ -403,7 +404,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;
-7
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,
@@ -56,10 +54,6 @@
#recent-imagery-split-line[hidden] {
display: none !important;
}
#recent-imagery-panel.collapsed .panel-title {
font-size: 8px;
letter-spacing: 1.8px;
}
#recent-imagery-panel.collapsed .panel-header {
margin-bottom: 2px;
}
@@ -172,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;
@@ -247,7 +247,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;
}
+122 -42
View File
@@ -1,6 +1,8 @@
/* ── Scene Director Panel ───────────────────── */
#scene-panel {
--panel-expanded-width: 360px;
/* Secondary text in Authoring: dimmer than labels, but legible on the globe. */
--scene-quiet: rgba(232, 234, 237, 0.62);
position: fixed;
top: calc(var(--left-stack-top) + (var(--left-stack-gap) * 2));
left: var(--left-stack-x);
@@ -160,15 +162,15 @@
flex-wrap: wrap;
}
/* The scene's name takes its own row; NEW SCENE / DELETE share the next. */
#scene-select {
flex: 1;
flex: 1 1 100%;
min-width: 0;
min-height: 32px;
font-size: 12px;
}
/* A translated scene name needs the width: the select takes its own row,
NEW / DEL share the next one. */
html:not([lang='en']) #scene-select {
flex: 1 1 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -178,8 +180,7 @@ html:not([lang='en']) #scene-select {
}
/* A translated action longer than its button wraps inside the button instead
of running past it. English keeps its one-line buttons ("EXPORT PRESETS"
included). */
of running past it. English keeps its one-line buttons. */
html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
white-space: normal;
overflow-wrap: break-word;
@@ -190,6 +191,70 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
min-width: 72px;
}
#scene-panel .scene-btn {
font-size: 10px;
padding: 8px 10px;
}
/* Authoring's steps: SCENE, SHOTS, PREVIEW, SAVE & SHARE. */
.scene-step-heading {
display: flex;
align-items: center;
gap: 8px;
margin: 10px 0 6px;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 1.6px;
color: var(--scene-quiet);
}
.scene-step-heading::after {
content: '';
flex: 1;
height: 1px;
background: rgba(255, 255, 255, 0.08);
}
.panel-header + .scene-step-heading {
margin-top: 2px;
}
/* One primary action per step: CAPTURE SHOT and PLAY. The panel id keeps
this above Cyber's shared button surface. */
#scene-panel .scene-btn-primary:not(:disabled) {
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
/* The file row is housekeeping; it stays quieter than the steps above. */
#scene-panel .scene-file-controls .scene-btn {
font-size: 9px;
/* Room for one long translated word (IMPORTIEREN, ІМПОРТУВАТИ) per line. */
padding: 8px 4px;
letter-spacing: 0.6px;
background: transparent;
}
/* Keep the shared hover on the restyled actions. */
#scene-panel .scene-btn-primary:hover:not(:disabled),
#scene-panel .scene-file-controls .scene-btn:hover:not(:disabled) {
color: var(--accent);
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.scene-shot-hint {
margin: 0 0 8px;
font-size: 11px;
line-height: 1.4;
color: var(--scene-quiet);
}
#scene-panel:not(.has-shots) .scene-shot-hint {
display: none;
}
#scene-start-btn,
#scene-stop-btn,
#scene-next-btn {
@@ -200,39 +265,34 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 160px;
min-height: 96px;
max-height: 260px;
/* Two shot rows stay visible; the panel scrolls when space is short. */
min-height: 128px;
flex: 1;
overflow-y: auto;
padding: 4px 2px 6px;
margin-bottom: 8px;
}
.scene-shot-list::-webkit-scrollbar {
width: 6px;
}
.scene-shot-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.16);
border-radius: 999px;
margin-bottom: 4px;
counter-reset: scene-shot;
}
.scene-shot-empty {
border: 1px dashed rgba(255, 255, 255, 0.14);
border-radius: 8px;
padding: 10px 8px;
padding: 16px 12px;
text-align: center;
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 1px;
color: var(--text-dim);
font-size: 11px;
letter-spacing: 0.3px;
line-height: 1.5;
color: var(--scene-quiet);
}
.scene-shot-row {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 9px;
padding: 7px 8px;
padding: 8px 10px;
background: rgba(255, 255, 255, 0.03);
counter-increment: scene-shot;
transition:
border-color var(--transition-fast),
background var(--transition-fast);
@@ -253,12 +313,38 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
.scene-shot-label {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 1px;
font-size: 12px;
letter-spacing: 0.4px;
color: var(--text-primary);
}
/* While renaming, the label holds only the editor at full width. */
.scene-shot-label:has(.scene-shot-rename) {
overflow: visible;
white-space: normal;
}
.scene-shot-label:has(.scene-shot-rename)::before {
content: none;
}
/* Shots are numbered 01, 02… in playback order. */
.scene-shot-label::before {
content: counter(scene-shot, decimal-leading-zero);
margin-right: 8px;
font-size: 10px;
color: var(--scene-quiet);
}
.scene-shot-row.active .scene-shot-label::before {
color: var(--accent);
}
.scene-shot-rename {
width: 100%;
min-width: 0;
@@ -270,11 +356,11 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
}
.scene-shot-meta {
margin-top: 4px;
margin-top: 5px;
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.8px;
color: var(--text-dim);
font-size: 10px;
letter-spacing: 0.4px;
color: var(--scene-quiet);
}
.scene-shot-actions {
@@ -285,7 +371,7 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
.scene-progress {
margin-top: 8px;
width: 100%;
height: 18px;
height: 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
@@ -315,9 +401,10 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
#scene-status {
margin-top: 8px;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 1px;
color: var(--text-secondary);
font-size: 11px;
letter-spacing: 0.4px;
line-height: 1.4;
color: var(--scene-quiet);
}
#scene-runtime {
@@ -367,6 +454,8 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
#cctv-panel.collapsed .cctv-summary-label,
#cctv-panel.collapsed #cctv-summary,
#scene-panel.collapsed .scene-controls,
#scene-panel.collapsed .scene-step-heading,
#scene-panel.collapsed .scene-shot-hint,
#scene-panel.collapsed .scene-shot-list,
#scene-panel.collapsed .scene-progress,
#scene-panel.collapsed #scene-status,
@@ -391,15 +480,6 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
width: 100%;
}
#data-panel.collapsed .panel-title,
#cctv-panel.collapsed .panel-title,
#radio-panel.collapsed .panel-title,
#utilities-panel.collapsed .panel-title,
#scene-panel.collapsed .panel-title {
font-size: 8px;
letter-spacing: 1.8px;
}
#control-panel.collapsed .panel-inner,
#data-panel.collapsed .data-panel-inner,
#cctv-panel.collapsed .cctv-panel-inner,
+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 {
-6
View File
@@ -42,18 +42,12 @@
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],
.weather-readout[hidden] {
display: none !important;
}
#weather-panel.collapsed .panel-title {
font-size: 8px;
letter-spacing: 1.8px;
}
#weather-panel.collapsed .panel-header {
margin-bottom: 2px;
}