Merge branch 'main' into denis/hal-728-dock-icon-only-wings

This commit is contained in:
Sameh Khamis committed 2026-11-02 07:09:23 -08:00
commit 2c007a4ace
782 files changed
+68624 -21392

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; } }
+3 -4
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 {
@@ -518,14 +516,15 @@ html:not([lang='en']) .bhote-event-story-current strong {
-webkit-line-clamp: 2;
}
/* Phase / time / sources run long in translation: allow a second line. */
/* Phase / time / sources / imagery note run long in translation: allow up to
three lines so the last status is not cut off. */
html:not([lang='en']) .bhote-event-story-current small {
display: -webkit-box;
margin-top: 3px;
text-overflow: clip;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
-webkit-line-clamp: 3;
}
.bhote-event-actions {
-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;
+44 -16
View File
@@ -1,6 +1,7 @@
/* Tight tracking keeps Chinese labels readable; preserve Latin tracking for
intel HUD readouts and content marked translate="no", including its containers. */
html:lang(zh)
/* Chinese and Japanese text. Tight tracking keeps CJK labels readable;
preserve Latin tracking for intel HUD readouts and content marked
translate="no", including its containers. */
html:is(:lang(zh), :lang(ja))
body
:not(
#intel-hud,
@@ -12,40 +13,67 @@ html:lang(zh)
letter-spacing: 0.02em !important;
}
/* Keep small Chinese labels legible at a minimum of 9px. */
html:lang(zh) #command-dock .gev-mic-label,
html:lang(zh) #command-dock #gev-voice-detail,
html:lang(zh) .gev-voice-tier-btn,
html:lang(zh) .panel-layer-toggle {
/* Japanese line breaking: never start a line with ー, a small kana or closing
punctuation (プレビュ / ー). */
html:lang(ja) body {
line-break: strict;
}
/* Keep small CJK labels legible at a minimum of 9px. */
html:is(:lang(zh), :lang(ja)) #command-dock .gev-mic-label,
html:is(:lang(zh), :lang(ja)) #command-dock #gev-voice-detail,
html:is(:lang(zh), :lang(ja)) .gev-voice-tier-btn,
html:is(:lang(zh), :lang(ja)) .panel-layer-toggle {
font-size: 9px;
}
/* A two-glyph button label must not break between its glyphs. */
html:lang(zh) .pp-mode-btn {
html:is(:lang(zh), :lang(ja)) .pp-mode-btn {
white-space: nowrap;
}
/* Japanese segment labels are short words (すべて, 平面): keep each whole. */
html:lang(ja) .gev-segment {
white-space: nowrap;
}
/* Keep unit symbols in their catalog case, including the cockpit readouts. */
html:lang(zh) #intel-hud *,
html:lang(zh) #cockpit-hud * {
html:is(:lang(zh), :lang(ja)) #intel-hud *,
html:is(:lang(zh), :lang(ja)) #cockpit-hud * {
text-transform: none;
}
/* Move short travel modes (步行 / 骑行) to the next line as whole words. */
html:lang(zh) [data-layer-id='directions'] .data-toggle-meta {
/* Move short travel modes (步行 / 骑行, 徒歩 / 自転車) to the next line as
whole words. */
html:is(:lang(zh), :lang(ja)) [data-layer-id='directions'] .data-toggle-meta {
word-break: keep-all;
overflow-wrap: break-word;
}
/* Leave room for the complete sensor name and resolution on two lines. */
html:lang(zh) .recent-imagery-readout .ri-strip {
html:is(:lang(zh), :lang(ja)) .recent-imagery-readout .ri-strip {
height: 131px;
}
html:lang(zh) .recent-imagery-readout .ri-card {
html:is(:lang(zh), :lang(ja)) .recent-imagery-readout .ri-card {
height: 123px;
grid-template-rows: 42px 13px 22px 11px 18px;
}
html:lang(zh) .recent-imagery-readout .ri-card-sensor {
html:is(:lang(zh), :lang(ja)) .recent-imagery-readout .ri-card-sensor {
white-space: normal;
text-overflow: clip;
}
/* The Japanese imagery slot line wraps at its · separators, never inside a
word (プレ / ビュー); a run too long for the line still breaks. */
html:lang(ja) .recent-imagery-readout .ri-slot-value {
word-break: keep-all;
overflow-wrap: break-word;
}
/* Japanese layer status lines wrap at their spaces and · separators, so an
age such as 1分以内 stays whole (更新 1 / 分以内); a run too long for the
line still breaks. */
html:lang(ja) .data-toggle-meta {
word-break: keep-all;
overflow-wrap: break-word;
}
+45 -59
View File
@@ -730,7 +730,8 @@ html:not([lang='en']) .cockpit-roll-token.is-separator {
}
.cockpit-vision-controls {
display: grid;
grid-template-columns: 48px 58px 48px;
/* The current style grows for a long translated name (ЗВИЧАЙНИЙ). */
grid-template-columns: 48px minmax(58px, max-content) 48px;
align-items: stretch;
gap: 2px;
margin-top: 2px;
@@ -968,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 {
@@ -1040,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);
@@ -1081,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);
}
@@ -1107,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;
@@ -1499,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;
@@ -1525,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 {
@@ -1541,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;
}
@@ -1551,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;
@@ -1626,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 {
@@ -1637,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%,
@@ -1646,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);
+5 -12
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 {
@@ -463,17 +463,10 @@ body:not(.ui-clean-view):not(.recording-mode):has(
}
#intel-hud[data-variant='tactical'] .hud-bottom-left,
#intel-hud[data-variant='tactical'] .hud-bottom-right,
#intel-hud[data-variant='operator'] .hud-bottom-left,
#intel-hud[data-variant='operator'] .hud-bottom-right {
#intel-hud[data-variant='tactical'] .hud-bottom-right {
bottom: calc(2vh + 6.5rem);
}
#intel-hud[data-variant='operator'] .hud-bottom-bar {
bottom: calc(2vh + 9.8rem);
width: min(48vw, 680px);
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 5.4rem);
}
+157 -15
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,50 @@ 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,
.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,
.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 +554,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 +577,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;
}
+60 -29
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:
@@ -747,12 +745,6 @@
.btn-icon::before {
content: '◐';
}
:root[data-ui-theme='cyber']
#command-dock
.style-btn[data-style='snow']
.btn-icon::before {
content: '❄︎';
}
:root[data-ui-theme='cyber'] :where(#bloom-toggle, #sharpen-toggle) {
font-size: 0;
@@ -802,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;
@@ -1447,6 +1445,15 @@
text-shadow: 0 0 6px var(--cyber-red);
}
/* Cyber frames each mode button itself; the shared segmented track would
draw a third, blue frame around them. */
:root[data-ui-theme='cyber'] .global-context-modes {
gap: 7px;
padding: 0;
border: 0;
background: none;
}
:root[data-ui-theme='cyber'] .dock-tray-toggle,
:root[data-ui-theme='cyber'] .context-mode-button {
background-color: rgba(38, 52, 58, 0.95);
@@ -1558,10 +1565,6 @@
clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%);
}
#intel-hud[data-variant='cyber'] .hud-top-bar {
display: none;
}
/* Cyber keeps the useful style and locality readouts, not fictional IDs. */
#intel-hud[data-variant='cyber'] .hud-top-left .hud-classification,
#intel-hud[data-variant='cyber'] .hud-top-left .hud-system {
@@ -1651,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']
@@ -1675,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) {
@@ -1728,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
@@ -1798,8 +1826,11 @@
isolation: isolate;
min-height: 132px;
flex: 1 1 auto;
border-style: solid;
border-color: rgba(206, 222, 226, 0.14);
place-content: center;
padding: 12px;
border: 1px solid rgba(206, 222, 226, 0.14);
border-radius: 7px;
text-align: center;
overflow: hidden;
background:
linear-gradient(
@@ -1861,7 +1892,6 @@
background-size: 78px 78px;
}
#intel-hud[data-variant='cyber'] .hud-top-bar,
#intel-hud[data-variant='cyber'] .hud-bottom-bar {
display: none;
}
@@ -2498,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);
}
@@ -2721,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 {
+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;
}
+87 -227
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
@@ -249,7 +229,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 +240,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 +297,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 +399,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 +429,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 +443,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:
@@ -985,17 +997,9 @@
animation: none;
}
}
/* Context is read for longer than other panels; it carries no halo. */
#global-context-panel .panel-glow {
position: absolute;
inset: -20px;
background: radial-gradient(
ellipse at center,
rgba(98, 181, 255, 0.2) 0%,
transparent 62%
);
filter: blur(20px);
opacity: 0.24;
pointer-events: none;
display: none;
}
.global-context-panel-inner {
position: relative;
@@ -1012,157 +1016,14 @@
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
}
.context-radio-dock {
position: relative;
display: flex;
align-items: center;
}
.context-radio-toggle {
display: grid;
width: 28px;
height: 28px;
place-items: center;
padding: 0;
border: 1px solid rgba(98, 181, 255, 0.22);
border-radius: 6px;
color: rgba(143, 207, 255, 0.58);
background: rgba(42, 113, 171, 0.08);
cursor: pointer;
transition:
border-color 0.15s ease,
color 0.15s ease,
background 0.15s ease,
box-shadow 0.15s ease;
}
.context-radio-toggle .material-symbols-outlined {
font-size: 17px;
}
.context-radio-toggle:hover,
.context-radio-toggle:focus-visible {
border-color: #79c2ff;
color: #fff;
outline: none;
}
.context-radio-toggle.active {
border-color: rgba(101, 247, 255, 0.82);
color: #eaffff;
background: rgba(0, 212, 255, 0.16);
box-shadow:
inset 0 0 12px rgba(0, 212, 255, 0.1),
0 0 12px rgba(0, 212, 255, 0.2);
}
.context-radio-toggle:disabled {
opacity: 0.5;
cursor: wait;
}
.context-radio-mini {
position: absolute;
z-index: 5;
top: calc(100% + 20px);
right: -38px;
display: none;
width: 244px;
box-sizing: border-box;
gap: 8px;
padding: 10px;
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 8px;
background: rgba(2, 15, 20, 0.96);
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.62),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(22px) saturate(1.35);
-webkit-backdrop-filter: blur(22px) saturate(1.35);
}
.context-radio-mini[hidden] {
display: none !important;
}
.context-radio-dock.disclosure-open .context-radio-mini {
display: grid;
}
.context-radio-mini-heading {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
color: #65f7ff;
font: 7px/1.2 var(--font-mono);
letter-spacing: 0.12em;
}
.context-radio-mini-heading button {
display: grid;
width: 26px;
height: 26px;
place-items: center;
padding: 0;
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 5px;
color: #8feaf2;
background: rgba(14, 69, 78, 0.16);
cursor: pointer;
}
.context-radio-mini-heading button:hover,
.context-radio-mini-heading button:focus-visible {
border-color: #65f7ff;
color: #fff;
outline: none;
}
.context-radio-mini-actions {
display: flex;
gap: 5px;
}
.context-radio-mini-heading .material-symbols-outlined {
font-size: 16px;
}
.context-radio-mini > .panel-layer-toggle {
width: 100%;
}
.context-radio-mini-heading strong {
overflow: hidden;
color: #e8feff;
font-size: 8px;
font-weight: 500;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-radio-mini-transport {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 5px;
}
.context-radio-mini-transport button {
min-height: 28px;
padding: 5px;
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 5px;
color: #8feaf2;
background: rgba(14, 69, 78, 0.16);
font: 8px/1 var(--font-mono);
cursor: pointer;
}
.context-radio-mini-transport button:hover:not(:disabled),
.context-radio-mini-transport button:focus-visible:not(:disabled),
.context-radio-mini-transport button.active {
border-color: #65f7ff;
color: #fff;
outline: none;
}
.context-radio-mini-transport button:disabled {
opacity: 0.38;
cursor: not-allowed;
}
.context-radio-mini-volume {
display: grid;
grid-template-columns: auto minmax(0, 1fr) 30px;
gap: 7px;
align-items: center;
color: var(--text-dim);
font: 7px/1 var(--font-mono);
letter-spacing: 0.08em;
}
.context-radio-mini-volume input {
width: 100%;
/* 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;
@@ -1229,40 +1090,44 @@
rather than relying on the chrome alone. */
filter: drop-shadow(0 0 5px rgba(150, 210, 255, 0.9));
}
/* One segmented control: the two modes read as a single choice. */
.global-context-modes {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
margin: 0 0 10px;
gap: 2px;
margin: 0 0 12px;
padding: 2px;
border: 1px solid rgba(98, 181, 255, 0.22);
border-radius: 7px;
background: rgba(0, 0, 0, 0.22);
}
.context-mode-button {
display: flex;
min-width: 0;
min-height: 36px;
min-height: 34px;
align-items: center;
justify-content: center;
gap: 6px;
padding: 7px 8px;
border: 1px solid rgba(98, 181, 255, 0.28);
border-radius: 6px;
color: #8fcfff;
background: rgba(42, 113, 171, 0.09);
font: 8px/1.15 var(--font-mono);
letter-spacing: 0.09em;
padding: 6px 8px;
border: 1px solid transparent;
border-radius: 5px;
color: rgba(214, 236, 255, 0.72);
background: transparent;
font: 600 10px/1.2 var(--font-mono);
letter-spacing: 0.06em;
cursor: pointer;
transition:
border-color 0.15s ease,
color 0.15s ease,
background 0.15s ease,
box-shadow 0.15s ease;
background 0.15s ease;
}
.context-mode-button .material-symbols-outlined {
font-size: 16px;
}
.context-mode-button:hover,
.context-mode-button:focus-visible {
border-color: #79c2ff;
color: #fff;
background: rgba(98, 181, 255, 0.08);
outline: none;
}
/* Keep focus distinct when the selected tab owns the accent colors. */
@@ -1272,12 +1137,9 @@
}
.context-mode-button.active {
border-color: rgba(0, 212, 255, 0.75);
color: #eaffff;
background: rgba(0, 212, 255, 0.14);
box-shadow:
inset 0 0 16px rgba(0, 212, 255, 0.08),
0 0 12px rgba(0, 212, 255, 0.12);
border-color: rgba(0, 212, 255, 0.55);
color: #fff;
background: rgba(0, 212, 255, 0.16);
}
.context-mode-button:disabled {
opacity: 0.55;
@@ -1287,23 +1149,20 @@
.context-mode-standby[hidden] {
display: none !important;
}
/* Plain guidance under the tabs, not a placeholder box. */
.context-mode-standby {
display: grid;
min-height: 72px;
place-content: center;
gap: 6px;
padding: 12px;
border: 1px dashed rgba(98, 181, 255, 0.2);
border-radius: 7px;
color: rgba(190, 230, 230, 0.5);
text-align: center;
font: 8px/1.45 var(--font-mono);
letter-spacing: 0.1em;
padding: 2px 2px 4px;
color: rgba(214, 236, 255, 0.72);
font: 10px/1.6 var(--font-mono);
letter-spacing: 0.02em;
}
.context-mode-standby strong {
color: #8fcfff;
font-size: 9px;
letter-spacing: 0.16em;
color: var(--text-primary);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
}
.context-mode-view {
min-height: 0;
@@ -1344,19 +1203,20 @@
}
.global-context-actions .panel-layer-toggle {
width: 100%;
min-height: 30px;
min-height: 32px;
padding: 6px 8px;
font-size: 8px;
font-size: 10px;
letter-spacing: 0.06em;
}
#global-context-panel #cockpit-entry {
position: static;
z-index: auto;
width: 100%;
min-height: 30px;
min-height: 32px;
padding: 5px 8px;
border-radius: 5px;
justify-content: center;
font-size: 8px;
font-size: 10px;
box-shadow: none;
}
#global-context-panel #cockpit-entry .material-symbols-outlined {
+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 -5
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,13 +101,13 @@ 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 ────────────────────────── */
body.mobile-shell #intel-hud .hud-edge,
body.mobile-shell #intel-hud .hud-corner,
body.mobile-shell #intel-hud .hud-top-bar,
body.mobile-shell #intel-hud .hud-bottom-bar {
display: none;
}
@@ -401,7 +401,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;
}
+12 -80
View File
@@ -124,7 +124,6 @@
line-height: 1.6;
}
.hud-top-bar,
.hud-bottom-bar {
display: none;
position: absolute;
@@ -141,24 +140,11 @@
pointer-events: none;
}
.hud-top-bar {
top: 70px;
}
.hud-bottom-bar {
bottom: 110px;
justify-content: center;
}
.hud-top-bar-center {
opacity: 0.84;
}
.hud-top-bar-left,
.hud-top-bar-right {
opacity: 0.6;
}
/* Corner blocks */
.hud-corner {
position: absolute;
@@ -298,54 +284,15 @@
gap: 20px;
}
#intel-hud[data-variant='tactical'] .hud-top-bar,
#intel-hud[data-variant='tactical'] .hud-bottom-bar {
display: none;
}
#intel-hud[data-variant='operator'] .hud-top-bar,
#intel-hud[data-variant='operator'] .hud-bottom-bar {
display: flex;
}
#intel-hud[data-variant='operator'] .hud-top-bar {
top: max(10vh, 90px);
left: 50%;
right: auto;
width: min(52vw, 720px);
transform: translateX(-50%);
border-color: rgba(255, 255, 255, 0.24);
background: linear-gradient(90deg, rgba(0, 0, 0, 0.56), rgba(8, 26, 34, 0.5));
box-shadow: 0 0 24px rgba(0, 255, 255, 0.08);
}
#intel-hud[data-variant='tactical'] .hud-top-left,
#intel-hud[data-variant='tactical'] .hud-top-right,
#intel-hud[data-variant='operator'] .hud-top-left,
#intel-hud[data-variant='operator'] .hud-top-right {
#intel-hud[data-variant='tactical'] .hud-top-right {
top: max(14vh, 136px);
}
#intel-hud[data-variant='operator'] .hud-bottom-bar {
border-color: rgba(255, 255, 255, 0.2);
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.52),
rgba(8, 22, 18, 0.52)
);
}
#intel-hud[data-variant='operator'] .hud-corner {
padding: 8px 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
background: rgba(0, 0, 0, 0.36);
}
#intel-hud[data-variant='operator'] .hud-bracket {
display: none;
}
#intel-hud[data-variant='minimal'] .hud-top-right,
#intel-hud[data-variant='minimal'] .hud-bottom-right,
#intel-hud[data-variant='minimal'] .hud-left-edge,
@@ -378,11 +325,7 @@
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
#intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
.hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) .scene-text-holdout {
background: rgba(0, 0, 0, 0.7);
@@ -399,11 +342,7 @@
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
#intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
.hud-readout {
display: block;
width: max-content;
@@ -413,11 +352,7 @@
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
#intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
:is(.hud-top-right, .hud-bottom-right)
.hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
@@ -427,38 +362,35 @@
}
/* Preserve glyph opacity without dimming the vertical backplates. */
:root[data-scope-mask='off']
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
.hud-edge {
:root[data-scope-mask='off'] #intel-hud[data-variant='tactical'] .hud-edge {
opacity: 1;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
#intel-hud[data-variant='tactical']
.hud-edge
.hud-readout
> * {
opacity: 0.5;
}
:root[data-scope-mask='off'] #intel-hud[data-variant='operator'] .hud-corner,
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
:is(.hud-top-bar, .hud-bottom-bar) {
#intel-hud[data-variant='minimal']
.hud-bottom-bar {
background: transparent;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
:is(.hud-top-bar, .hud-bottom-bar) {
#intel-hud[data-variant='minimal']
.hud-bottom-bar {
width: fit-content;
max-width: calc(100vw - 120px);
gap: 24px;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
:is(.hud-top-bar, .hud-bottom-bar)
#intel-hud[data-variant='minimal']
.hud-bottom-bar
.hud-readout {
overflow: hidden;
white-space: nowrap;
+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;
+22 -13
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);
@@ -461,14 +460,16 @@ html:not([lang='en']) .radio-transport button {
border-top: 1px solid rgba(98, 181, 255, 0.16);
}
.military-awareness-standby strong {
color: #79c2ff;
color: var(--text-primary);
font-size: 10px;
letter-spacing: 0.1em;
font-weight: 600;
letter-spacing: 0.08em;
}
.military-awareness-standby span {
color: #8192a6;
font-size: 9px;
letter-spacing: 0.05em;
color: rgba(214, 236, 255, 0.72);
font-size: 10px;
line-height: 1.5;
letter-spacing: 0.02em;
}
.military-awareness-standby button {
justify-self: start;
@@ -688,8 +689,14 @@ html:not([lang='en']) .data-count {
text-wrap: balance;
}
/* The status column keeps its width, so a wide chip (WIRD AKTIVIERT) never
overlaps the name; the name wraps in the room left beside it. */
html:not([lang='en']) .data-toggle-right {
flex: none;
}
/* 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;
}
@@ -1084,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;
@@ -1121,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;
}
+4 -21
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;
@@ -98,25 +98,12 @@
display: none;
}
.hud-top-bar,
.hud-bottom-bar {
width: calc(100vw - 24px);
left: 12px;
transform: none;
}
.hud-top-bar {
top: 54px;
font-size: 9px;
}
#intel-hud[data-variant='operator'] .hud-top-bar {
left: 12px;
right: auto;
width: calc(100vw - 24px);
transform: none;
}
.hud-bottom-bar {
bottom: 90px;
font-size: 9px;
@@ -205,7 +192,7 @@
under the expanded panel plus a vertical scrollbar that scrolls nothing
but glow. Pin the glow to the panel box here — its blur is ink overflow,
which never scrolls — instead of clipping the rail, which would also clip
legitimate content such as the Context radio popover. */
legitimate content that extends past a panel's edge. */
#left-panel-stack .panel-glow,
#right-context-rail .panel-glow {
inset: 0;
@@ -228,10 +215,6 @@
#right-context-rail #global-context-panel.collapsed {
width: 100%;
}
.context-radio-mini {
right: -38px;
width: min(244px, calc(100vw - 32px));
}
#left-panel-stack > #data-panel,
#left-panel-stack > #cctv-panel,
@@ -262,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;
}
+122 -41
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);
@@ -150,15 +152,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;
@@ -168,8 +170,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;
@@ -180,6 +181,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 {
@@ -190,39 +255,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);
@@ -243,12 +303,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;
@@ -260,11 +346,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 {
@@ -275,7 +361,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);
@@ -305,9 +391,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 {
@@ -357,6 +444,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,
@@ -378,14 +467,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,
#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;
}