Files
gods-eye-view/src/ui/styles/first-run.css
T
Sameh Khamis 484243fe4b feat(ui): give every panel one scrollbar, select and slider skin
Every scrolling surface carries the kit's .ui-scroll, which
draws the thin themed scrollbar from --scroll-thumb and --scroll-track; the
Context panel and others drew the platform's light scrollbar because the
page declares no dark color-scheme. The per-panel scrollbar rules go; Cyber
sets the two tokens and Cockpit's HUD windows keep their accent tint.

Selects draw .gev-field's own chevron instead of the platform arrow, the
voice selects join .gev-field, number fields keep dark spinners, and the
Street Level range uses the shared slider in its green.
2026-10-31 17:28:46 -07:00

416 lines
11 KiB
CSS

/* ── First-run mission launcher ───────────────────────────────────
Compact and non-modal: it gives a new visitor a meaningful starting
choice without blocking manual map interaction or spending API quota. */
#first-run-launcher {
position: fixed;
z-index: 175;
top: 50%;
left: 50%;
width: min(34rem, calc(100vw - 2rem));
/* Five tiles do not fit a short landscape phone (667 x 390), where the card
would otherwise render past BOTH ends of the viewport. Cap it to the
viewport and let the mission list scroll inside — the heading, the checkbox
and the status line must stay reachable at every height. */
max-height: calc(100vh - 1.5rem);
max-height: calc(100dvh - 1.5rem);
display: flex;
flex-direction: column;
padding: 1.15rem;
overflow: hidden;
color: var(--text-primary);
background:
linear-gradient(145deg, rgba(10, 27, 37, 0.97), rgba(7, 13, 22, 0.95)),
rgba(7, 17, 25, 0.96);
border: 1px solid rgba(54, 220, 255, 0.42);
border-radius: 0.65rem;
box-shadow:
0 1.4rem 4.5rem rgba(0, 0, 0, 0.72),
0 0 2.5rem rgba(0, 212, 255, 0.09),
inset 0 0 0 1px rgba(255, 255, 255, 0.025);
backdrop-filter: blur(28px) saturate(1.25);
-webkit-backdrop-filter: blur(28px) saturate(1.25);
opacity: 0;
transform: translate(-50%, calc(-50% + 0.8rem)) scale(0.985);
transition:
opacity 220ms ease,
transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
/* Inert until revealed AND again while fading out, so a click during the
dismiss animation cannot launch a second mission behind the fade. */
pointer-events: none;
}
/* An AUTHOR `display` on this id beats the UA's `[hidden] { display: none }` in
the cascade, so the flex above would otherwise leave the card in the
accessibility tree from page parse until it is revealed — or, on a share
link, until it is removed. Restore the attribute's meaning explicitly. */
#first-run-launcher[hidden] {
display: none;
}
#first-run-launcher.visible {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
pointer-events: auto;
}
.first-run-scanline {
position: absolute;
inset: 0 0 auto;
height: 2px;
background: linear-gradient(
90deg,
transparent,
rgba(54, 220, 255, 0.8),
transparent
);
box-shadow: 0 0 1rem rgba(54, 220, 255, 0.45);
pointer-events: none;
}
.first-run-header,
.first-run-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
color: rgba(171, 210, 220, 0.6);
font-family: var(--font-mono);
font-size: 0.58rem;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.first-run-kicker {
color: rgba(54, 220, 255, 0.86);
}
#first-run-launcher h2 {
margin-top: 0.6rem;
font-family: var(--font-mono);
font-size: clamp(1.25rem, 2.4vw, 1.75rem);
font-weight: 500;
letter-spacing: -0.045rem;
}
#first-run-description {
max-width: 29rem;
margin-top: 0.3rem;
color: rgba(215, 231, 235, 0.66);
font-size: 0.78rem;
line-height: 1.5;
}
.first-run-choices {
display: grid;
gap: 0.42rem;
margin-top: 1rem;
/* The one part of the card allowed to scroll when the viewport is too short.
min-height:0 is what actually lets this flex child shrink below its content. */
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
/* A visible, non-overlaying affordance that the list continues; the
scrollbar itself is the kit's .ui-scroll. */
scrollbar-gutter: stable;
}
.first-run-choices button {
display: grid;
grid-template-columns: 1.7rem 1fr 1.3rem;
align-items: center;
gap: 0.7rem;
width: 100%;
min-height: 3.55rem;
padding: 0.6rem 0.7rem;
color: rgba(225, 244, 248, 0.92);
text-align: left;
background: rgba(29, 69, 81, 0.18);
border: 1px solid rgba(100, 200, 220, 0.18);
border-radius: 0.42rem;
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast),
transform var(--transition-fast);
}
.first-run-choices button:hover,
.first-run-choices button:focus-visible {
background: rgba(25, 151, 178, 0.16);
border-color: rgba(54, 220, 255, 0.62);
outline: none;
transform: translateX(2px);
}
/* aria-disabled, not :disabled — the JS keeps the buttons focusable while a
mission runs so the keyboard is never dumped out of the launcher mid-flight. */
.first-run-choices button[aria-disabled='true'] {
cursor: wait;
opacity: 0.48;
transform: none;
pointer-events: none;
}
.first-run-choices button > .material-symbols-outlined:first-child {
color: #55dff5;
font-size: 1.35rem;
}
.first-run-choices button > span:nth-child(2) {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.first-run-choices strong {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08rem;
}
.first-run-choices small {
color: rgba(198, 220, 226, 0.55);
font-size: 0.66rem;
}
.first-run-arrow {
color: rgba(54, 220, 255, 0.4);
font-size: 1rem;
transition:
transform var(--transition-fast),
color var(--transition-fast);
}
.first-run-choices button:hover .first-run-arrow,
.first-run-choices button:focus-visible .first-run-arrow {
color: rgba(54, 220, 255, 0.9);
transform: translateX(2px);
}
/* Ordered after the shared button rule rather than shouting !important at it. */
.first-run-choices button.first-run-explore {
background: rgba(255, 255, 255, 0.025);
}
.first-run-footer {
margin-top: 0.85rem;
font-size: 0.48rem;
letter-spacing: 0.055rem;
}
.first-run-dismiss {
position: relative;
padding: 0 0 0 0.75rem;
border: 0;
background: none;
color: inherit;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
cursor: pointer;
}
/* A 44px touch target drawn outside the layout, so the footer row keeps the
height of its one line of text; a 44px button box would nearly treble it. */
.first-run-dismiss::before {
content: '';
position: absolute;
top: 50%;
right: 0;
left: 0;
height: 44px;
transform: translateY(-50%);
}
.first-run-dismiss:hover,
.first-run-dismiss:focus-visible {
color: rgba(54, 220, 255, 0.86);
}
.first-run-dismiss-touch {
display: none;
}
/* No hover and a coarse pointer: a phone or tablet, where ESC means nothing. */
@media (hover: none) and (pointer: coarse) {
.first-run-dismiss-key {
display: none;
}
.first-run-dismiss-touch {
display: inline;
}
}
.first-run-suppress {
display: flex;
align-items: center;
gap: 0.4rem;
color: rgba(199, 225, 231, 0.66);
text-transform: none;
font-size: 0.62rem;
letter-spacing: 0.02rem;
cursor: pointer;
}
.first-run-suppress input {
width: 0.82rem;
height: 0.82rem;
margin: 0;
accent-color: #36dcff;
cursor: pointer;
}
.first-run-suppress:hover {
color: rgba(225, 244, 248, 0.92);
}
/* The live region carries mission progress and failures, so it owns a full row
and reserves its height — otherwise the card resizes as it speaks. */
.first-run-note {
min-height: 1.1rem;
margin-top: 0.5rem;
color: rgba(199, 225, 231, 0.62);
font-family: var(--font-mono);
font-size: 0.62rem;
line-height: 1.35;
}
#first-run-launcher[data-state='loading'] .first-run-note {
color: rgba(54, 220, 255, 0.85);
}
/* Every surface that claims the screen hides the launcher. This list is the CSS
half of the ESC arbitration: src/firstRunExperience.js watches these same
classes and makes the launcher step aside, so a hidden card is never left
holding a keyboard handler. Keep the two lists in step —
EXCLUSIVE_SURFACE_CLASSES pins them together. */
body.ui-clean-view #first-run-launcher,
body.recording-mode #first-run-launcher,
body.cockpit-mode #first-run-launcher,
body.scene-playback-mode #first-run-launcher {
display: none;
}
@media (max-width: 620px) {
#first-run-launcher {
top: auto;
bottom: 5.8rem;
padding: 0.9rem;
transform: translate(-50%, 0.8rem) scale(0.985);
}
#first-run-launcher.visible {
transform: translate(-50%, 0) scale(1);
}
/* Five tiles have to clear the dock on a 375 x 812 phone, so the rows
tighten. The dismiss button stays: it is the only way to close the card
without choosing a mission on a phone, which has no ESC key. */
#first-run-launcher h2 {
margin-top: 0.45rem;
}
#first-run-description {
margin-top: 0.25rem;
font-size: 0.72rem;
}
.first-run-choices {
margin-top: 0.75rem;
}
.first-run-choices button {
min-height: 3rem;
padding: 0.45rem 0.6rem;
}
.first-run-choices small {
font-size: 0.62rem;
}
}
/* Short viewports — landscape phones, small laptop windows. The chrome tightens
so the mission list keeps a usable share of the height. The intro line STAYS:
it is the only copy that says what this thing is, and with four tiles it
fits. (It was dropped here while there were five; the tile that went is
exactly why it can come back.) */
@media (max-height: 620px) {
#first-run-launcher {
padding: 0.75rem 0.85rem;
}
#first-run-launcher h2 {
margin-top: 0.3rem;
font-size: 1.1rem;
}
#first-run-description {
margin-top: 0.2rem;
font-size: 0.7rem;
line-height: 1.4;
}
.first-run-choices {
margin-top: 0.55rem;
}
.first-run-choices button {
min-height: 2.7rem;
padding: 0.36rem 0.6rem;
}
.first-run-choices strong {
font-size: 0.68rem;
}
.first-run-choices small {
font-size: 0.6rem;
}
.first-run-footer {
margin-top: 0.5rem;
}
.first-run-note {
margin-top: 0.3rem;
min-height: 0;
}
}
/* A soft edge on the list, so a tile cut by the fold reads as "more below"
rather than as a rendering seam. Gated on `data-scrollable`, which the JS sets
only when the list ACTUALLY overflows — a fade on a list that fits would
promise content that is not there, which is the opposite of the affordance
it is meant to be. Decorative only: the list scrolls under it. */
.first-run-choices[data-scrollable='true'] {
-webkit-mask-image: linear-gradient(
to bottom,
#000 calc(100% - 1.4rem),
transparent 100%
);
mask-image: linear-gradient(
to bottom,
#000 calc(100% - 1.4rem),
transparent 100%
);
}
/* Scrolled to the end, there is nothing left to promise, so the fade lifts.
Browsers without scroll-driven animations keep the static fade, which is
still honest — there IS more list, it is just above rather than below. */
@supports (animation-timeline: scroll(self block)) {
.first-run-choices[data-scrollable='true'] {
animation: first-run-scroll-fade linear both;
animation-timeline: scroll(self block);
animation-range: contain 80% contain 100%;
}
}
@keyframes first-run-scroll-fade {
to {
-webkit-mask-image: linear-gradient(to bottom, #000 100%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 100%, transparent 100%);
}
}
@media (prefers-reduced-motion: reduce) {
#first-run-launcher,
.first-run-choices button,
.first-run-arrow {
transition: none;
}
/* Scrolling a tile into view must not animate either. */
.first-run-choices {
scroll-behavior: auto;
}
}