Real weather, real aircraft, a heightfield off the main thread, and instruments
Three things that were built and never connected, connected.
**The weather was already there.** `observe()` has always taken a
`WeatherObservation` and `main.ts` has always passed null, so the cloud,
precipitation, visibility and marine-layer paths in atmosphere.ts had never run
outside a test. The server already shipped NWS, met.no and Open-Meteo, all
configured off. What was actually missing was that a single TERA_ORIGIN_LAT/LNG
served one metro and lied to the other — so weather and traffic are per-region
now, derived from the city's own bounds, and the Bay Area gets its fog while
Long Beach gets its own sky. The route takes ?city= or a validated ?lat=&lng=
and refuses to become an open geocoding proxy for the planet.
**The heightfield moved to a Worker.** 2.3 s of blocked main thread at boot, and
another ~950 ms of point-in-polygon on top of it: the park mask is filled in the
worker now, and block placement samples four corners and only runs the exact
test on a cell that straddles an edge — 8 buildings differ out of 185,036.
createScene is async and takes a Stage as a consequence, and there is a
main-thread fallback because "clone it and it works" has no exception clause.
**Spaces is a chunk you fetch when you reach for the door**, not one everybody
downloads. Same for the godmode tools. The entry chunk is 722 kB rather than
772; three.js is most of what is left and splitting it is a different job.
**Godmode is an instrument panel now** rather than one slider: the date and the
season, not just the hour, so the Meeus moon and the sun's seasonal arc become
visible instead of merely correct; a weather override that says on screen when
it is lying; a frame-time and draw-call readout; and a pose editor that emits a
paste-ready Chapter block, which is the thing that makes adding New York cheap.
Two blockers the review caught:
- Every city switch leaked 8 GPU textures — one of them a 2048x2048 shadow map
— and ~10.5 shader programs, and deleteTexture had never been called once in
the app's lifetime. The renderer was being built per scene; it belongs to the
canvas, for the life of the page.
- An upstream fetch that threw rather than returning null skipped the cache
stamp, so the TTL — the only rate limit on outbound calls — collapsed to one
upstream request per inbound request, and the caller got a 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+303
-54
@@ -2,7 +2,16 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!--
|
||||
`viewport-fit=cover` so the canvas reaches the edges of a notched screen;
|
||||
every fixed card below then pays for it with an `env(safe-area-inset-*)`.
|
||||
Deliberately **no** `user-scalable=no` and no `maximum-scale`: the map
|
||||
itself is already protected from browser zoom by `touch-action: none` and
|
||||
by `scenekit.ts` swallowing WebKit's `gesture*` events, and taking page
|
||||
zoom away from everyone to protect one element is the accessibility
|
||||
mistake that rule exists to avoid.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<!--
|
||||
An empty data URI, not an icon file. `src/` carries no binary assets by
|
||||
@@ -89,12 +98,29 @@
|
||||
what people noticed. */
|
||||
background: #0d1218;
|
||||
overflow: hidden;
|
||||
/* The canvas cannot scroll the page — `touch-action: none` sees to that
|
||||
— but a drag that starts on the panel, the rail or the source line
|
||||
still rubber-bands the whole document on iOS and can still trigger
|
||||
pull-to-refresh on Android, which on a full-screen map looks like the
|
||||
map itself has come loose. */
|
||||
overscroll-behavior: none;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
color: var(--ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#scene { display: block; width: 100vw; height: 100dvh; touch-action: none; }
|
||||
#scene {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
touch-action: none;
|
||||
/* A slow orbit on iOS is, to WebKit, a long press on a page: it raises
|
||||
the selection callout and the magnifier over the city. There is no
|
||||
text here to select. */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
/* ---- Type scale ------------------------------------------------------
|
||||
Five sizes. 9px is reserved for uppercase micro-labels, where the caps
|
||||
@@ -162,22 +188,10 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.scrub { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s2); }
|
||||
.scrub input { flex: 1; accent-color: var(--amber); height: 14px; min-width: 0; }
|
||||
.scrub button {
|
||||
font: inherit;
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 6px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--r-sm);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--ink-2);
|
||||
transition: background var(--t), color var(--t);
|
||||
}
|
||||
.scrub button:hover { background: rgba(255, 255, 255, 0.16); color: var(--ink); }
|
||||
/* The `#hour` scrubber and its `now` button used to live here. They were
|
||||
god-only, and the godmode panel now owns the same override with a date
|
||||
as well as an hour — two writers for one value, of which this was the
|
||||
one that silently threw the date away. `main.ts` says the rest. */
|
||||
|
||||
.cities { display: flex; gap: var(--s1); }
|
||||
.city {
|
||||
@@ -195,7 +209,9 @@
|
||||
color: var(--ink-2);
|
||||
transition: background var(--t), color var(--t);
|
||||
}
|
||||
.city:hover { background: rgba(40, 48, 58, 0.7); color: var(--ink); }
|
||||
@media (hover: hover) {
|
||||
.city:hover { background: rgba(40, 48, 58, 0.7); color: var(--ink); }
|
||||
}
|
||||
.city[aria-pressed="true"] { background: rgba(242, 177, 52, 0.22); color: var(--amber-ink); }
|
||||
|
||||
.enter {
|
||||
@@ -212,7 +228,7 @@
|
||||
box-shadow: var(--shadow);
|
||||
transition: background var(--t);
|
||||
}
|
||||
.enter:hover { background: var(--amber-lit); }
|
||||
@media (hover: hover) { .enter:hover { background: var(--amber-lit); } }
|
||||
|
||||
/* The public-office note. Amber-edged rather than amber-filled: it is an
|
||||
explanation, not an action, and the one filled amber thing on screen
|
||||
@@ -255,7 +271,9 @@
|
||||
color: var(--ink-2);
|
||||
transition: background var(--t), color var(--t);
|
||||
}
|
||||
.chapter:hover { background: rgba(255, 255, 255, 0.09); color: var(--ink); }
|
||||
@media (hover: hover) {
|
||||
.chapter:hover { background: rgba(255, 255, 255, 0.09); color: var(--ink); }
|
||||
}
|
||||
.chapter[aria-pressed="true"] { background: rgba(242, 177, 52, 0.2); color: var(--amber-ink); }
|
||||
.num { font-size: 9px; letter-spacing: 0.1em; opacity: 0.55; font-variant-numeric: tabular-nums; }
|
||||
|
||||
@@ -266,7 +284,7 @@
|
||||
.panel-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: var(--s3);
|
||||
top: calc(var(--s3) + env(safe-area-inset-top));
|
||||
left: var(--s3);
|
||||
z-index: 4;
|
||||
align-items: center;
|
||||
@@ -287,10 +305,18 @@
|
||||
}
|
||||
.panel-toggle .glyph { font-size: 13px; line-height: 1; }
|
||||
|
||||
/* ---- Top right: the plan --------------------------------------------- */
|
||||
/* ---- Top right: who you are, and the plan ------------------------------
|
||||
The tier badge is its own fixed card rather than the last child of
|
||||
`.corner`, which it used to be. Two reasons, and the second is a bug:
|
||||
a phone turns the plan into a bottom sheet and the badge must not go
|
||||
down there with it, and `M` — which hides the plan — was also hiding
|
||||
the one line that says whether you are signed in. */
|
||||
.corner {
|
||||
position: fixed;
|
||||
top: var(--s4);
|
||||
/* Clear of the tier badge above it, which is one line of 9px caps in a
|
||||
card: 8 + 14 + 8 of padding and leading, two hairlines, and the 4px
|
||||
rhythm's worth of gap. */
|
||||
top: calc(var(--s4) + env(safe-area-inset-top) + 2.4rem);
|
||||
right: var(--s4);
|
||||
width: 15rem;
|
||||
display: flex;
|
||||
@@ -325,7 +351,12 @@
|
||||
}
|
||||
|
||||
.tier {
|
||||
position: fixed;
|
||||
top: calc(var(--s4) + env(safe-area-inset-top));
|
||||
right: var(--s4);
|
||||
z-index: 3;
|
||||
margin: 0;
|
||||
max-width: min(15rem, 50vw);
|
||||
padding: var(--s2) var(--s3);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -336,7 +367,15 @@
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
.tier .who { color: var(--ink-2); text-transform: none; letter-spacing: 0.04em; }
|
||||
.tier .who {
|
||||
color: var(--ink-2);
|
||||
text-transform: none;
|
||||
letter-spacing: 0.04em;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tier.god .who { color: var(--amber-ink); }
|
||||
.tier a { color: var(--amber-ink); text-transform: none; letter-spacing: 0.04em; }
|
||||
|
||||
@@ -344,7 +383,10 @@
|
||||
.rail {
|
||||
position: fixed;
|
||||
right: var(--s4);
|
||||
bottom: var(--s4);
|
||||
/* The safe-area inset is zero everywhere except the phone it exists for,
|
||||
where `.rail { bottom: 16px }` puts the `?` button underneath the home
|
||||
indicator — a control you can see and cannot press. */
|
||||
bottom: calc(var(--s4) + env(safe-area-inset-bottom));
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -356,7 +398,32 @@
|
||||
second panel. */
|
||||
max-width: min(30rem, calc(100vw - var(--s4) * 2));
|
||||
}
|
||||
#detail { max-width: 100%; }
|
||||
#detail {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--s3);
|
||||
}
|
||||
#detail-text { flex: 1; min-width: 0; }
|
||||
/* A dismiss the thumb can find. On a mouse the detail card is cleared by
|
||||
moving the pointer off the marker, which is an affordance touch simply
|
||||
does not have: a tap sets it and nothing takes it away. Tapping empty
|
||||
map clears it too — this is the version you can see. */
|
||||
.detail-close {
|
||||
flex: none;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 0 var(--s1);
|
||||
margin: -2px 0;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: var(--r-sm);
|
||||
background: none;
|
||||
color: var(--ink-3);
|
||||
transition: color var(--t);
|
||||
}
|
||||
@media (hover: hover) { .detail-close:hover { color: var(--ink); } }
|
||||
.hint {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -377,6 +444,19 @@
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
/* The plan toggle is for pointers, and only for pointers.
|
||||
Where there is a keyboard the `M` hint two elements to the left is the
|
||||
affordance and a button saying the same thing is a second row of glass
|
||||
for nothing. Where there is not — a phone, a tablet, a touchscreen
|
||||
kiosk — the hints are hidden or meaningless and this is the only way in.
|
||||
Keyed off the pointer rather than off the width because the defect it
|
||||
fixes is about fingers: an 820px iPad has no `M` either, and it lands on
|
||||
the desktop layout by design (see `deviceProfile` in `stage.ts`). */
|
||||
#plan-toggle { display: none; }
|
||||
@media (pointer: coarse), (max-width: 600px) {
|
||||
#plan-toggle { display: inline-flex; align-items: center; }
|
||||
}
|
||||
|
||||
.help {
|
||||
font: inherit;
|
||||
font-size: 10px;
|
||||
@@ -389,18 +469,21 @@
|
||||
color: var(--ink-2);
|
||||
transition: background var(--t), color var(--t);
|
||||
}
|
||||
.help:hover { background: rgba(255, 255, 255, 0.16); color: var(--ink); }
|
||||
@media (hover: hover) {
|
||||
.help:hover { background: rgba(255, 255, 255, 0.16); color: var(--ink); }
|
||||
}
|
||||
|
||||
/* ---- Bottom left: where the numbers came from -------------------------
|
||||
Shown only when the data is live — see `renderLegend` in main.ts for why
|
||||
Shown only when the data is live — see `renderSource` in main.ts for why
|
||||
the sample-data half of this was retired. The disclosure it used to
|
||||
carry did not go into a README nobody opens: it is on the boot card
|
||||
everyone passes through and in the `?` card, which is one keypress away
|
||||
from any state the app can be in. */
|
||||
from any state the app can be in. The `?` card is also where the live
|
||||
sources' own credit lines land; see `#credits`. */
|
||||
.source {
|
||||
position: fixed;
|
||||
left: var(--s4);
|
||||
bottom: var(--s4);
|
||||
bottom: calc(var(--s4) + env(safe-area-inset-bottom));
|
||||
margin: 0;
|
||||
z-index: 3;
|
||||
max-width: calc(100vw - var(--s4) * 2);
|
||||
@@ -417,6 +500,18 @@
|
||||
}
|
||||
.source.live { color: #8fe89a; }
|
||||
|
||||
/* The scrim behind the panel sheet. Declared *before* the phone breakpoint
|
||||
and not after it: two rules of equal specificity, and the last one in
|
||||
the file wins whatever the media query says — which is how this spent
|
||||
an afternoon being permanently invisible. */
|
||||
#scrim {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
background: rgba(4, 7, 11, 0.45);
|
||||
}
|
||||
|
||||
/* ---- Overlays --------------------------------------------------------- */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
@@ -438,6 +533,8 @@
|
||||
}
|
||||
.sheet h2 { margin: 0 0 var(--s3); font-size: 11px; letter-spacing: 0.2em;
|
||||
text-transform: uppercase; color: var(--amber); }
|
||||
/* The Touch section, under the keyboard one. */
|
||||
.sheet h2:not(:first-child) { margin-top: var(--s4); }
|
||||
.keys { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: var(--s2) var(--s3);
|
||||
align-items: baseline; font-size: 11px; }
|
||||
.keys dt { text-align: right; }
|
||||
@@ -454,7 +551,7 @@
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--ink);
|
||||
}
|
||||
.sheet-close:hover { background: rgba(255, 255, 255, 0.16); }
|
||||
@media (hover: hover) { .sheet-close:hover { background: rgba(255, 255, 255, 0.16); } }
|
||||
|
||||
/* ---- Boot -------------------------------------------------------------
|
||||
The page used to be blank for the ~2.3 s it takes to build the Bay Area
|
||||
@@ -505,12 +602,22 @@
|
||||
.boot-step { margin: 0; font-size: 10px; letter-spacing: 0.06em; color: var(--ink-2);
|
||||
min-height: 1.4em; }
|
||||
.boot-note { margin: var(--s4) 0 0; font-size: 10px; line-height: 1.6; color: var(--ink-4); }
|
||||
/* One notch up from the note above it, and a hairline off it. A credit
|
||||
line is somebody's licence term rather than this project's small print,
|
||||
and two identical grey paragraphs read as one. */
|
||||
.credits {
|
||||
margin-top: var(--s3);
|
||||
padding-top: var(--s3);
|
||||
border-top: 1px solid var(--hairline);
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* ---- Responsive -------------------------------------------------------
|
||||
Two breakpoints and no more. At 900 the left column stops being furniture
|
||||
and becomes a sheet you open; at 600 the plan stops fitting beside the
|
||||
map at all and lives behind `M`. `main.ts` owns the *state* of both, so
|
||||
these rules only describe what each state looks like. */
|
||||
map at all and moves to the "plan view" button on the rail (and `M`,
|
||||
where there is a keyboard). `main.ts` owns the *state* of both, so these
|
||||
rules only describe what each state looks like. */
|
||||
@media (max-width: 900px) {
|
||||
.panel-toggle { display: flex; }
|
||||
#panel {
|
||||
@@ -523,23 +630,131 @@
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.corner { width: 11.5rem; top: var(--s3); right: var(--s3); }
|
||||
.corner {
|
||||
width: 11.5rem;
|
||||
top: calc(var(--s3) + env(safe-area-inset-top) + 2.4rem);
|
||||
right: var(--s3);
|
||||
}
|
||||
.tier { top: calc(var(--s3) + env(safe-area-inset-top)); right: var(--s3); }
|
||||
.minimap-frame { height: 10rem; }
|
||||
.rail { right: var(--s3); bottom: var(--s3); }
|
||||
.source { left: var(--s3); bottom: var(--s3); }
|
||||
.rail { right: var(--s3); bottom: calc(var(--s3) + env(safe-area-inset-bottom)); }
|
||||
.source { left: var(--s3); bottom: calc(var(--s3) + env(safe-area-inset-bottom)); }
|
||||
}
|
||||
|
||||
/* ---- The phone --------------------------------------------------------
|
||||
Below 600 this stops being a scaled-down desktop and becomes a different
|
||||
layout, because the constraints are different in kind and not in degree:
|
||||
there is no hover, so nothing may depend on it; there is no keyboard, so
|
||||
a card explaining the keyboard is furniture; and there is one thumb,
|
||||
which reaches the bottom third of the screen and not the top corners.
|
||||
|
||||
The renderer draws the same conclusion from the same numbers —
|
||||
`deviceProfile()` in `stage.ts` keys off this exact 600px edge — so the
|
||||
stylesheet and the pixel budget cannot drift apart. */
|
||||
@media (max-width: 600px) {
|
||||
.rail { max-width: calc(100vw - var(--s3) * 2); }
|
||||
/* The provenance line and the key hints would collide in the last 40px
|
||||
of a phone, and the honesty line is the one that has to survive. */
|
||||
/* Everything hit by a finger clears 44px, which is 11 steps of the 4px
|
||||
rhythm and the smallest target anyone has managed to defend. `#help`
|
||||
was 17px tall. */
|
||||
#panel-toggle,
|
||||
#enter,
|
||||
.city,
|
||||
.chapter,
|
||||
.sheet-close {
|
||||
min-height: 44px;
|
||||
}
|
||||
.chapter { align-items: center; }
|
||||
.help,
|
||||
.detail-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* The rail spans the width so the detail card — now reachable, because
|
||||
a marker picks on tap — is a readable line rather than a column. */
|
||||
.rail {
|
||||
left: var(--s3);
|
||||
right: var(--s3);
|
||||
max-width: none;
|
||||
align-items: stretch;
|
||||
}
|
||||
/* No keyboard, so no key hints — and with them gone the card around
|
||||
them is a full-width bar of glass holding one button. On a phone the
|
||||
hint *is* the `?` button. */
|
||||
.hint span { display: none; }
|
||||
.source { max-width: 60vw; }
|
||||
.hint {
|
||||
align-self: flex-end;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
/* The honesty line moves out of the thumb's way rather than out of the
|
||||
layout. It is the one caption that is never allowed to be dropped for
|
||||
space, so it goes to the quiet corner under the ☰ button and stays to
|
||||
one ellipsised line. */
|
||||
.source {
|
||||
top: calc(var(--s3) + env(safe-area-inset-top) + 3.1rem);
|
||||
bottom: auto;
|
||||
max-width: min(60vw, calc(100vw - var(--s3) * 2));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* The plan, as a bottom sheet. At 11.5rem in a corner it ate a fifth of
|
||||
a 375px map and its own affordances were about six pixels across —
|
||||
a map you cannot read to move a map you can. Full width above the
|
||||
rail, it is a plan view; and it is still off until asked for, which
|
||||
on this layout means the "plan view" button on the rail. It was `M`
|
||||
and only `M` for a while, which meant this block was written for a
|
||||
device that could never reach it. */
|
||||
.corner {
|
||||
top: auto;
|
||||
left: var(--s3);
|
||||
right: var(--s3);
|
||||
width: auto;
|
||||
bottom: calc(var(--s3) + 3.5rem + env(safe-area-inset-bottom));
|
||||
}
|
||||
.minimap-frame { height: min(38dvh, 18rem); }
|
||||
/* Hover-only text on a device with no hover. */
|
||||
.minimap-readout { display: none; }
|
||||
|
||||
/* The panel, as a bottom sheet. A left drawer on a phone covers the
|
||||
thing being looked at; a sheet rises from the edge the thumb starts
|
||||
at, and the scrim behind it means tapping the map closes it. */
|
||||
#panel {
|
||||
inset: auto 0 0 0;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
max-height: 72dvh;
|
||||
padding: var(--s4) var(--s3) calc(var(--s4) + env(safe-area-inset-bottom));
|
||||
gap: var(--s2);
|
||||
z-index: 6;
|
||||
pointer-events: auto;
|
||||
background: var(--glass-strong);
|
||||
backdrop-filter: var(--blur);
|
||||
-webkit-backdrop-filter: var(--blur);
|
||||
border-top: 1px solid var(--hairline);
|
||||
border-radius: var(--r) var(--r) 0 0;
|
||||
box-shadow: 0 -8px 30px rgba(3, 6, 10, 0.5);
|
||||
}
|
||||
body.panel-closed #panel { transform: translateY(100%); opacity: 0; }
|
||||
#scrim { display: block; }
|
||||
body.panel-closed #scrim { display: none; }
|
||||
}
|
||||
|
||||
/* The minimap's own visibility is a user decision (`M`), seeded from the
|
||||
viewport width by `main.ts` rather than by a media query, so that
|
||||
toggling it on at 375px actually shows it. */
|
||||
|
||||
/* The minimap's own visibility is a user decision (`M`, or the "plan view"
|
||||
button on the rail), seeded from the viewport width by `main.ts` rather
|
||||
than by a media query, so that toggling it on at 375px actually shows
|
||||
it. */
|
||||
body.minimap-off .corner { display: none; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -564,11 +779,6 @@
|
||||
<h1 id="title">San Francisco</h1>
|
||||
<p id="subtitle">Tera · Lumbridge Simulate</p>
|
||||
<p id="clock" class="clock" aria-live="polite"></p>
|
||||
<div id="scrub" class="scrub" hidden>
|
||||
<input id="hour" type="range" min="0" max="23.9" step="0.1" value="12"
|
||||
aria-label="Hour of day" />
|
||||
<button id="now" title="follow the wall clock">now</button>
|
||||
</div>
|
||||
</section>
|
||||
<nav id="cities" class="cities" aria-label="City"></nav>
|
||||
<button id="enter" class="enter">Enter the office →</button>
|
||||
@@ -577,27 +787,45 @@
|
||||
<p class="card" id="blurb"></p>
|
||||
</div>
|
||||
|
||||
<p class="card tier" id="tier" hidden></p>
|
||||
|
||||
<aside id="corner" class="corner" aria-label="Plan view">
|
||||
<div class="card" id="minimap">
|
||||
<div class="minimap-frame"></div>
|
||||
<p id="minimap-readout" class="minimap-readout"></p>
|
||||
</div>
|
||||
<p class="card tier" id="tier" hidden></p>
|
||||
</aside>
|
||||
|
||||
<div class="rail" id="rail">
|
||||
<div class="card" id="detail" aria-live="polite" hidden></div>
|
||||
<div class="card" id="detail" aria-live="polite" hidden>
|
||||
<span id="detail-text"></span>
|
||||
<button id="detail-close" class="detail-close" aria-label="Dismiss">×</button>
|
||||
</div>
|
||||
<div class="card hint" id="hint">
|
||||
<span><kbd>1</kbd>–<kbd>9</kbd> chapters</span>
|
||||
<span><kbd>[</kbd> <kbd>]</kbd> city</span>
|
||||
<span><kbd>O</kbd> office</span>
|
||||
<span><kbd>M</kbd> plan</span>
|
||||
<!-- The plan view's tap target, for the devices where `M` is not a
|
||||
thing that exists. Until this button did, the plan was seeded off
|
||||
below 600px and the key was the only way to turn it on, so the
|
||||
bottom-sheet layout designed for it further up this file was
|
||||
unreachable on the device it was drawn for. Hidden where the key
|
||||
hint beside it is true; see `#plan-toggle` in the stylesheet.
|
||||
`aria-pressed` and no second label, because the state it reports is
|
||||
the map appearing next to it. -->
|
||||
<button id="plan-toggle" class="help" aria-pressed="true">plan view</button>
|
||||
<button id="help" class="help" aria-haspopup="dialog">? shortcuts</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="source" class="source"></p>
|
||||
|
||||
<!-- Behind the panel sheet on a phone, and nowhere else. A sheet with no
|
||||
scrim leaves no way to dismiss it but the same button that opened it,
|
||||
which is the wrong half of the screen for a thumb. -->
|
||||
<div id="scrim"></div>
|
||||
|
||||
<div class="overlay" id="shortcuts" role="dialog" aria-modal="true"
|
||||
aria-labelledby="shortcuts-title" hidden>
|
||||
<div class="card sheet">
|
||||
@@ -607,15 +835,36 @@
|
||||
<dt><kbd>[</kbd> <kbd>]</kbd></dt><dd>Previous / next city</dd>
|
||||
<dt><kbd>O</kbd></dt><dd>Enter or leave the office</dd>
|
||||
<dt><kbd>M</kbd></dt><dd>Show or hide the plan view</dd>
|
||||
<dt><kbd>?</kbd></dt><dd>This card</dd>
|
||||
<dt id="key-overlays"><kbd>?</kbd></dt><dd>This card</dd>
|
||||
<dt><kbd>Esc</kbd></dt><dd>Close an overlay, or leave the office</dd>
|
||||
<dt><kbd>Tab</kbd></dt><dd>Every control, in reading order</dd>
|
||||
</dl>
|
||||
<p class="boot-note">On the plan: click or drag to move the view, scroll to zoom,
|
||||
arrow keys to aim and <kbd>Enter</kbd> to go.</p>
|
||||
<h2 id="touch-title">Touch</h2>
|
||||
<dl class="keys">
|
||||
<dt>One finger</dt><dd>Orbit</dd>
|
||||
<dt>Two fingers</dt><dd>Pinch to zoom, drag to move over the ground</dd>
|
||||
<dt>Tap a marker</dt><dd>Its card, at the bottom of the screen</dd>
|
||||
<dt>Tap the map</dt><dd>Dismiss the card; tap the ☰ sheet's scrim to close it</dd>
|
||||
<dt>Plan view</dt><dd>The button on the rail shows or hides it, as a sheet above the rail</dd>
|
||||
</dl>
|
||||
<!-- Stated as the rule rather than as a claim about this page, because
|
||||
this file is static and the answer is not: one build serves a
|
||||
deployment with a weather station and one with nothing at all. -->
|
||||
<p class="boot-note">The markers on this map are fabricated. No real company data
|
||||
ships in this build; a deployment wired to a live source says <em>live data</em>
|
||||
in the corner, and this one does not.</p>
|
||||
ships in this build. A deployment wired to a live source names it in the corner —
|
||||
<em>live weather</em>, <em>live traffic</em>, <em>live markers</em>, or
|
||||
<em>live data</em> when all three are. Nothing in the corner means everything you
|
||||
are looking at came out of this bundle.</p>
|
||||
<!-- Credit for whatever live source is answering right now, written by
|
||||
`renderCredits` in main.ts. Empty and hidden on a build with no
|
||||
server, which is most of them — and not decoration when it is not:
|
||||
MET Norway and Open-Meteo are CC BY 4.0 and the API forwards the
|
||||
line each of them asks for. This card is where it is shown because
|
||||
it is reachable from every state on both layouts, and because the
|
||||
corner line is clamped to one ellipsised phrase on a phone. -->
|
||||
<p class="boot-note credits" id="credits" hidden></p>
|
||||
<button class="sheet-close" id="shortcuts-close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user