Files
gods-eye-view/src/ui/styles/foundation.css
T
Sameh Khamis bc5d038afd Merge Core main into german/hal-644-ar: Arabic on the Vue UI
Main moved the UI to Vue views and Pinia stores, so the Arabic changes to
the old DOM controllers now live in their views: the rail legend range
comes from formatScaleRange (keeping units a source supplies as given),
the voice timeout options are worded by Intl unit formatting in
VoiceControl, and the scene editor's JSON fields stay left to right in
SceneDialog. Main's CCTV status line, launch panel and calibration values
keep the Arabic isolates and Western-digit formatting.

isolate() and keepCodesWithValues() read the locale through getLocale(),
which follows the page's vue-i18n instance, and the Intl formatters take
intlLocale(getLocale()). A language switch now applies in place, and
applyDocumentLanguage sets <html dir> on every switch, so the page mirrors
without a reload.

The icon rails and their tips are placed with logical properties, so they
stay on the outer side of the mirrored stacks; Cyber's new stack offsets,
padding and edge lines are logical too, and rtl.css moves Cyber's right
stack to its new rail clearance. Main removed the Context mini radio, so its
right-to-left rules and test go.

The Arabic catalog gains main's new strings and drops the ones main no
longer uses; the other catalogs keep main's new keys and the branch's
landing labels.
2026-11-01 20:15:02 -08:00

667 lines
17 KiB
CSS

/* ═══════════════════════════════════════════════
GOD'S EYE VIEW — Dark Sci-Fi UI
Apple meets Blade Runner
═══════════════════════════════════════════════ */
:root {
--bg-dark: #0a0a0f;
--glass-bg: rgba(12, 12, 20, 0.72);
--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;
--text-secondary: rgba(232, 234, 237, 0.5);
--text-dim: rgba(232, 234, 237, 0.3);
/* Opaque surface for browser-painted menus, which cannot use --glass-bg. */
--menu-bg: #12121c;
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--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);
/* 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;
--display-panel-expanded-width: 272px;
--cockpit-radio-collapsed-width: 148px;
--cockpit-radio-expanded-width: 232px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Keyboard focus is an app-wide state and must survive selected/active button
skins. Individual controls can choose an inset/outset offset, but no local
`outline: none` rule may erase the ring. */
:where(
button,
a[href],
input,
select,
textarea,
summary,
[tabindex],
[role='button'],
[role='checkbox'],
[role='combobox'],
[role='link'],
[role='listbox'],
[role='menuitem'],
[role='menuitemcheckbox'],
[role='menuitemradio'],
[role='option'],
[role='radio'],
[role='searchbox'],
[role='slider'],
[role='spinbutton'],
[role='switch'],
[role='tab'],
[role='textbox'],
[role='treeitem']
):focus-visible {
outline: 2px solid var(--text-primary) !important;
outline-offset: 2px;
}
/* Native `<select>` popups are painted by the browser, not by this sheet: the
closed control inherits its panel skin, but the option list falls back to the
platform's light palette unless the control declares a scheme. Declare it on
the controls rather than on :root so the app keeps its own hand-tuned
scrollbar and input skins, and set the option colors explicitly because the
popup surface is not themed on every platform. */
select {
color-scheme: dark;
}
select option,
select optgroup {
background-color: var(--menu-bg);
color: var(--text-primary);
}
select option:disabled {
color: var(--text-dim);
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
background: var(--bg-dark);
font-family: var(--font-sans);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
}
/* ── Cesium Container ─────────────────────────── */
#cesiumContainer {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
/* Unified world-overlay host: detection uses one host-owned blend surface
beneath the shared text/card canvas; both share one frame path. The root
holds ONLY the card canvas — it is a stacking context (z-index below), and
a canvas that blends against the scene cannot live inside one. */
#world-overlay-root {
position: absolute;
inset: 0;
z-index: 6;
overflow: hidden;
pointer-events: none;
}
#world-overlay-detection-surface,
#world-overlay-canvas {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
pointer-events: none;
}
/* Scope mask — the explicit circular viewport treatment (see
src/scopeMask.js for the emergent-artifact history). z2 keeps it above
the WebGL scene but under the z5 detection surface, preserving the old
compositing order where detection's blend rode the masked scene. */
#scope-mask {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
/* z1 like the celestial ring; the ring is appended LATER so DOM order
keeps it ABOVE the mask — matching the old stacking, where the DOM
ring rode above the in-scene scope artifact. Detection (z5) and the
overlay root (z6) stay above both. */
z-index: 1;
pointer-events: none;
}
/* The detection surface is host-owned but parented into #cesiumContainer
(position:absolute, z-index:auto — NOT a stacking context) so its
`mix-blend-mode: screen` composites against the WebGL scene. z5 keeps it
under the z6 root that carries the shared card canvas; do not move this
surface inside #world-overlay-root, which would isolate the blend. */
#world-overlay-detection-surface {
z-index: 5;
}
/* Canvas cards retain their spatial renderer; this parallel focusable list
exposes only the currently painted actionable targets to assistive tech. */
.world-overlay-accessibility {
position: fixed;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ── Source-backed cockpit weather ─────────────
Low-resolution GPU cloud pass composited only inside the cockpit keyhole. */
#cockpit-cloud-effects {
position: fixed;
inset: 0;
z-index: 7;
display: none;
width: 100vw;
height: 100vh;
opacity: 0;
pointer-events: none;
clip-path: circle(min(40vw, 52vh) at 50% 50%);
filter: blur(0.7px) saturate(0.72) contrast(1.08);
transform: translateZ(0);
transition: opacity 900ms ease;
}
body.cockpit-mode #cockpit-cloud-effects.active {
display: block;
opacity: 0.86;
}
/* ── Full-globe celestial keyhole ring ─────── */
.celestial-ring-overlay {
position: absolute;
inset: 0;
/* Above the z1 scope mask regardless of DOM order (the ring is built by
StyleManager before the scope installs) — the ring always rode above
the scope treatment. Detection (z5) and the overlay root (z6) stay on
top of both. */
z-index: 2;
overflow: hidden;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition:
opacity 300ms ease,
visibility 300ms ease;
}
.celestial-ring-overlay.visible {
opacity: 1;
visibility: visible;
}
.celestial-ring-overlay.disabled {
display: none;
}
.celestial-ring-canvas {
position: absolute;
inset: 0;
display: block;
transform-origin: 50% 50%;
will-change: transform, opacity;
}
.celestial-ring-outline {
position: absolute;
box-sizing: border-box;
border: 0.8px solid rgba(151, 218, 232, 0.095);
border-radius: 50%;
will-change: width, height;
}
.celestial-marker {
position: absolute;
z-index: 2;
transform: translate(-50%, -50%);
font-family: 'Material Symbols Outlined';
font-size: 20px;
font-weight: 400;
font-style: normal;
line-height: 1;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 20;
transition: opacity 120ms linear;
user-select: none;
}
.celestial-sun {
color: rgba(231, 194, 72, 0.92);
text-shadow:
0 0 7px rgba(231, 194, 72, 0.72),
0 0 18px rgba(231, 194, 72, 0.3);
}
.celestial-moon {
color: rgba(55, 211, 239, 0.84);
text-shadow:
0 0 7px rgba(55, 211, 239, 0.65),
0 0 17px rgba(55, 211, 239, 0.25);
}
#celestial-toggle:disabled {
cursor: not-allowed;
opacity: 0.42;
}
/* Hide Cesium default UI elements */
.cesium-viewer-toolbar,
.cesium-viewer-animationContainer,
.cesium-viewer-timelineContainer,
.cesium-viewer-bottom,
.cesium-viewer-fullscreenContainer,
.cesium-viewer-vrContainer,
.cesium-viewer-geocoderContainer,
.cesium-viewer-selectionIndicatorContainer,
.cesium-viewer-infoBoxContainer {
display: none !important;
}
/* Data-source attribution (Google Maps / 3D Tiles, etc.).
Required by the Google Maps Platform Terms of Service — keep it visible.
Styled to be unobtrusive but legible against the dark UI. */
#cesium-credits {
position: fixed;
/* Bottom-LEFT corner, mirrored from the GEV mic pill (right:36/bottom:36) so the two bottom
corners are symmetric; sits below the coordinate HUD (.hud-bottom-left, bottom:180). Was
dead-center at the bottom, tucked under the LOCATIONS/STYLE panels. Must stay visible (Google
/Cesium attribution ToS). */
bottom: 36px;
left: 36px;
z-index: 90;
max-width: 90vw;
/* Keep the Google logo + "Data attribution" link on ONE line — they were wrapping to a second
row (looked like it was falling over). nowrap + a roomy max-width holds them inline. */
white-space: nowrap;
pointer-events: auto;
font-family: var(--font-mono);
font-size: 10px;
color: rgba(232, 234, 237, 0.55);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
}
#cesium-credits .cesium-credit-logoContainer,
#cesium-credits .cesium-credit-textContainer,
#cesium-credits .cesium-credit-expand-link {
display: inline-block;
vertical-align: middle;
}
#cesium-credits a,
#cesium-credits .cesium-credit-expand-link {
color: rgba(232, 234, 237, 0.7);
}
/* Cesium's stock attribution lightbox grows with every registered data source.
Keep the title and close affordance fixed while the complete, unabridged
credit list wraps and scrolls inside a compact panel. */
.cesium-credit-lightbox-overlay {
z-index: 200 !important;
}
.cesium-credit-lightbox {
display: flex;
flex-direction: column;
width: 470px;
max-width: calc(100vw - 2rem);
max-height: min(70dvh, 36rem);
overflow: hidden;
}
.cesium-credit-lightbox > ul {
flex: 1;
min-height: 0;
margin: 0;
padding: 8px 20px 14px 40px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
font-size: 12px !important;
line-height: 1.35;
white-space: normal;
}
.cesium-credit-lightbox > ul > li {
padding-bottom: 5px;
overflow-wrap: anywhere;
}
@media (max-width: 575px) {
.cesium-credit-lightbox {
width: 100% !important;
max-width: none !important;
height: 100% !important;
max-height: 100dvh !important;
margin: 0 !important;
}
}
/* Clean-view / recording: the Google + Cesium attribution MUST stay visible.
Google Maps Platform ToS and Cesium's terms require the credit whenever the
content is displayed — and these are exactly the modes used to record demos
and launch videos, so hiding #cesium-credits here (as it was before) shipped
non-compliant footage. The full credit line — Cesium/Ion logo, "Google Maps",
and the "Data attribution" link that opens the per-layer license popover —
stays on screen; it's already styled small and unobtrusive above. The GEV
chrome (panels/HUD) still fades via its own rules; only the required notice
is exempt. (Note: Cesium re-applies inline styles on the credit elements each
frame, so the on-screen credit content is owned by Cesium's update loop, not
CSS overrides — we deliberately don't fight it here.) */
#gev-voice-control {
position: fixed;
right: 36px;
bottom: 36px;
/* Z ladder: panels ~100, voice pill 150, toast 200, clean-view-exit 300 */
z-index: 150;
display: flex;
align-items: center;
gap: 10px;
min-width: 214px;
padding: 9px 14px;
color: var(--text-primary);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 999px;
box-shadow:
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,
0 0 20px rgba(0, 212, 255, 0.08);
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
font-family: var(--font-mono);
pointer-events: auto;
}
#gev-voice-button {
width: 72px;
height: 34px;
flex: 0 0 auto;
color: var(--accent);
background: rgba(0, 212, 255, 0.06);
border: 1px solid rgba(0, 212, 255, 0.28);
border-radius: var(--btn-radius);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0;
cursor: pointer;
}
#gev-voice-button:hover {
color: #ffffff;
border-color: var(--accent);
box-shadow: 0 0 12px rgba(0, 212, 255, 0.22);
}
.gev-voice-readout {
min-width: 0;
line-height: 1.2;
text-align: right;
}
#gev-voice-status {
font-size: 10px;
color: var(--accent);
}
#gev-voice-detail {
max-width: 132px;
margin-top: 2px;
overflow: hidden;
color: var(--text-secondary);
font-size: 9px;
text-overflow: ellipsis;
white-space: nowrap;
}
#gev-voice-control[data-status='listening'] {
border-color: rgba(92, 255, 198, 0.45);
}
#gev-voice-control[data-status='listening'] #gev-voice-status {
color: #5cffc6;
}
#gev-voice-control[data-status='executing'] {
border-color: rgba(255, 220, 120, 0.55);
}
#gev-voice-control[data-status='executing'] #gev-voice-status {
color: #ffdc78;
}
#gev-voice-control[data-status='error'] {
align-items: flex-start;
width: min(440px, calc(100vw - 72px));
border-color: rgba(255, 92, 110, 0.6);
/* Multi-line error block reads as a panel, not a capsule */
border-radius: var(--panel-radius);
}
#gev-voice-control[data-status='error'] #gev-voice-status {
color: #ff5c6e;
}
#gev-voice-control[data-status='error'] .gev-voice-readout {
flex: 1;
text-align: left;
}
#gev-voice-control[data-status='error'] #gev-voice-detail {
max-width: none;
overflow: visible;
color: #ffb4bd;
line-height: 1.45;
text-overflow: clip;
white-space: pre-wrap;
overflow-wrap: anywhere;
user-select: text;
}
/* ── Title Bar ────────────────────────────────── */
#title-bar {
position: fixed;
top: 32px;
inset-inline-start: 36px;
z-index: 100;
pointer-events: none;
user-select: none;
}
#title-bar .title-glow {
position: absolute;
top: -20px;
inset-inline-start: -20px;
width: 200px;
height: 80px;
background: radial-gradient(ellipse, var(--accent-dim) 0%, transparent 70%);
filter: blur(20px);
opacity: 0.6;
}
#title-bar h1 {
display: flex;
align-items: center;
gap: 12px;
font-family: var(--font-mono);
font-size: 26px;
font-weight: 600;
letter-spacing: 8px;
color: var(--text-primary);
text-shadow: 0 0 30px var(--accent-glow);
position: relative;
}
#title-bar .title-logo {
position: relative;
display: block;
width: 52px;
height: auto;
filter: drop-shadow(0 0 12px rgba(0, 246, 255, 0.5));
}
#title-bar .title-logo::before,
#title-bar .title-logo::after {
--radio-broadcast-opacity: 0.28;
content: '';
position: absolute;
z-index: 1;
left: 50%;
top: -9px;
width: 13px;
height: 7px;
border: 1px solid var(--accent);
border-bottom: 0;
border-radius: 999px 999px 0 0;
opacity: 0;
pointer-events: none;
transform: translateX(-50%);
transform-origin: 50% 100%;
}
#title-bar .title-logo::after {
--radio-broadcast-opacity: 0.17;
top: -15px;
width: 27px;
height: 13px;
}
#title-bar.radio-broadcasting .title-logo::before,
#title-bar.radio-broadcasting .title-logo::after {
opacity: var(--radio-broadcast-opacity);
animation: title-radio-broadcast 1.8s ease-in-out infinite;
}
#title-bar.radio-broadcasting .title-logo::after {
animation-delay: 0.28s;
}
@keyframes title-radio-broadcast {
0%,
100% {
opacity: calc(var(--radio-broadcast-opacity) * 0.55);
transform: translateX(-50%) scale(0.94);
}
50% {
opacity: var(--radio-broadcast-opacity);
transform: translateX(-50%) scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
#title-bar.radio-broadcasting .title-logo::before,
#title-bar.radio-broadcasting .title-logo::after {
animation: none;
}
}
.brand-logo {
aspect-ratio: 775 / 520;
overflow: visible;
pointer-events: none;
}
.brand-logo > img,
.brand-logo > svg {
display: block;
width: 100%;
height: 100%;
}
#title-bar .title-accent,
.loader-content .title-accent {
color: var(--accent);
font-weight: 300;
}
#title-bar .subtitle {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 5px;
color: var(--text-dim);
margin-top: 8px;
text-transform: uppercase;
}
#title-bar .frame-rate-readout {
margin-top: 6px;
font-family: var(--font-mono);
font-size: 11px;
font-variant-numeric: tabular-nums;
letter-spacing: 1px;
color: var(--accent);
}
/* ── Style Indicator ──────────────────────────── */
#style-indicator {
position: fixed;
top: 32px;
inset-inline-end: 36px;
z-index: 100;
pointer-events: none;
user-select: none;
text-align: end;
}
#style-indicator .indicator-label {
display: block;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 3px;
color: var(--text-dim);
text-transform: uppercase;
}
#style-indicator .indicator-value {
display: block;
font-family: var(--font-mono);
font-size: 16px;
font-weight: 500;
letter-spacing: 5px;
color: var(--accent);
margin-top: 4px;
text-shadow: 0 0 20px var(--accent-glow);
transition: var(--transition-smooth);
}