a6f6a91813
"sample data · fabricated, not real companies" sat in the corner of every load, because no deployment has a markers source wired by default and the sample set is therefore the overwhelmingly common case. A caption that is always on screen stops being read after the first second, which is the opposite of what a disclosure is for. So the corner keeps only the rare half — "live data", which is genuinely worth interrupting someone to say — and the fact that the markers are invented moves to the two places someone actually looks: the boot card everyone passes through, and the `?` card, which is one keypress away from any state the app can be in. It did not go into a README nobody opens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
637 lines
24 KiB
HTML
637 lines
24 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="color-scheme" content="dark" />
|
||
<!--
|
||
An empty data URI, not an icon file. `src/` carries no binary assets by
|
||
policy (ARCHITECTURE.md §3.1) and a favicon is not worth being the
|
||
exception; without this line every load spends a request on `/favicon.ico`
|
||
and logs the 404 to the console, which buries the errors that matter.
|
||
-->
|
||
<link rel="icon" href="data:," />
|
||
<title>Lumbridge Simulate — San Francisco</title>
|
||
<style>
|
||
/*
|
||
* The whole interface, inline, on purpose.
|
||
*
|
||
* There is no framework and no stylesheet because there is no build step
|
||
* that would earn one: this file is served as written, so what you read
|
||
* here is what the browser gets. It also means the first paint needs
|
||
* nothing but this document — which is the entire reason the boot card at
|
||
* the bottom of the body can be on screen before a single module has been
|
||
* fetched, let alone before the Bay Area heightfield has been built.
|
||
*
|
||
* Two constraints shape every rule below.
|
||
*
|
||
* The first is that this chrome floats over a *photographic* background
|
||
* that changes from a bright noon sky to a nearly black one and back. A
|
||
* panel tuned only for the dark case — which is what this was — reads as
|
||
* a grey smudge at noon, because dark-on-dark needs no edge and
|
||
* dark-on-bright very much does. Every card therefore carries three
|
||
* separations at once: a fill dark enough to hold white text against a
|
||
* white sky, a hairline that is light on the inside so the card has a
|
||
* rim, and one soft drop shadow so it sits above rather than in.
|
||
*
|
||
* The second is that there is exactly one accent. `#f2b134` is the
|
||
* identity and it is spent only on things that are *active* — the current
|
||
* chapter, the current city, the office door, a focus ring. The moment it
|
||
* also means "heading" and "border" and "hover" it stops meaning
|
||
* anything, which is what a second pass at this looked like before it was
|
||
* pulled back.
|
||
*/
|
||
:root {
|
||
--amber: #f2b134;
|
||
--amber-lit: #ffc555;
|
||
--amber-ink: #ffd68a;
|
||
|
||
/* One glass recipe, two weights. The strong one is for things that must
|
||
be read over the brightest part of the sky: the boot card and the
|
||
shortcuts sheet. */
|
||
--glass: rgba(9, 13, 18, 0.62);
|
||
--glass-strong: rgba(9, 13, 18, 0.86);
|
||
--glass-inset: rgba(255, 255, 255, 0.05);
|
||
--hairline: rgba(255, 255, 255, 0.11);
|
||
--blur: blur(14px) saturate(1.2);
|
||
--shadow: 0 6px 22px rgba(3, 6, 10, 0.45);
|
||
|
||
/* Four steps of ink and no more. Anything that wanted a fifth was
|
||
saying something the type scale should have said instead. */
|
||
--ink: rgba(255, 255, 255, 0.78);
|
||
--ink-2: rgba(255, 255, 255, 0.56);
|
||
--ink-3: rgba(255, 255, 255, 0.4);
|
||
--ink-4: rgba(255, 255, 255, 0.26);
|
||
|
||
/* 4px rhythm. Every gap, pad and offset below is a multiple of it, so
|
||
the columns line up without anyone having to nudge a value. */
|
||
--s1: 4px;
|
||
--s2: 8px;
|
||
--s3: 12px;
|
||
--s4: 16px;
|
||
--s5: 24px;
|
||
|
||
--r: 8px;
|
||
--r-sm: 5px;
|
||
--t: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
[hidden] { display: none !important; }
|
||
|
||
html, body {
|
||
margin: 0;
|
||
height: 100%;
|
||
/* Dark, not the old `#d9e6ee`. The canvas covers this within a frame,
|
||
but the frame before it used to be a full-screen flash of pale blue
|
||
that then dropped to a night scene. A dark page is wrong for half a
|
||
second at noon and right for half a second at midnight; the flash is
|
||
what people noticed. */
|
||
background: #0d1218;
|
||
overflow: hidden;
|
||
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; }
|
||
|
||
/* ---- Type scale ------------------------------------------------------
|
||
Five sizes. 9px is reserved for uppercase micro-labels, where the caps
|
||
and the tracking carry the legibility that the size does not. */
|
||
.t-title { font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; }
|
||
.t-body { font-size: 11px; letter-spacing: 0.01em; line-height: 1.55; }
|
||
.t-ctrl { font-size: 12px; letter-spacing: 0.01em; }
|
||
.t-micro { font-size: 10px; letter-spacing: 0.06em; }
|
||
.t-caps { font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; }
|
||
|
||
/* ---- Glass ----------------------------------------------------------- */
|
||
.card {
|
||
background: var(--glass);
|
||
backdrop-filter: var(--blur);
|
||
-webkit-backdrop-filter: var(--blur);
|
||
border: 1px solid var(--hairline);
|
||
box-shadow: var(--shadow), inset 0 1px 0 var(--glass-inset);
|
||
border-radius: var(--r);
|
||
padding: var(--s3);
|
||
color: var(--ink);
|
||
font-size: 11px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
/* One focus treatment for everything that can take focus. The offset is
|
||
what makes it legible on a control that is already amber. */
|
||
:is(button, a, input, [tabindex]):focus-visible {
|
||
outline: 2px solid var(--amber);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* ---- Left column ------------------------------------------------------ */
|
||
#panel {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
padding: var(--s4);
|
||
width: min(19.5rem, calc(100vw - var(--s4) * 2));
|
||
max-height: 100dvh;
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--s1);
|
||
pointer-events: none;
|
||
transition: transform var(--t), opacity var(--t);
|
||
}
|
||
#panel > * { pointer-events: auto; }
|
||
|
||
h1 {
|
||
margin: 0;
|
||
font-size: 11px;
|
||
letter-spacing: 0.2em;
|
||
text-transform: uppercase;
|
||
color: var(--amber);
|
||
}
|
||
#subtitle { margin: var(--s1) 0 0; font-size: 10px; letter-spacing: 0.06em; color: var(--ink-3); }
|
||
.clock {
|
||
margin: var(--s2) 0 0;
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: var(--ink-2);
|
||
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); }
|
||
|
||
.cities { display: flex; gap: var(--s1); }
|
||
.city {
|
||
flex: 1;
|
||
font: inherit;
|
||
font-size: 11px;
|
||
padding: var(--s2) var(--s1);
|
||
cursor: pointer;
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--r-sm);
|
||
background: var(--glass);
|
||
backdrop-filter: var(--blur);
|
||
-webkit-backdrop-filter: var(--blur);
|
||
box-shadow: var(--shadow);
|
||
color: var(--ink-2);
|
||
transition: background var(--t), color var(--t);
|
||
}
|
||
.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 {
|
||
font: inherit;
|
||
font-size: 12px;
|
||
padding: var(--s2) var(--s3);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
border: 1px solid transparent;
|
||
border-radius: var(--r-sm);
|
||
color: #10161d;
|
||
background: var(--amber);
|
||
font-weight: 600;
|
||
box-shadow: var(--shadow);
|
||
transition: background var(--t);
|
||
}
|
||
.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
|
||
should stay the door. */
|
||
.badge {
|
||
margin: 0;
|
||
border-left: 2px solid rgba(242, 177, 52, 0.55);
|
||
border-radius: var(--r-sm);
|
||
padding: var(--s2) var(--s3);
|
||
font-size: 10px;
|
||
line-height: 1.6;
|
||
color: var(--ink-2);
|
||
}
|
||
.badge a { color: var(--amber-ink); }
|
||
|
||
#chapters {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
background: var(--glass);
|
||
backdrop-filter: var(--blur);
|
||
-webkit-backdrop-filter: var(--blur);
|
||
border: 1px solid var(--hairline);
|
||
box-shadow: var(--shadow), inset 0 1px 0 var(--glass-inset);
|
||
border-radius: var(--r);
|
||
padding: var(--s1);
|
||
}
|
||
.chapter {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--s2);
|
||
padding: var(--s2) var(--s2);
|
||
background: none;
|
||
border: 0;
|
||
border-radius: var(--r-sm);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
color: var(--ink-2);
|
||
transition: background var(--t), color var(--t);
|
||
}
|
||
.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; }
|
||
|
||
#blurb { margin: 0; color: var(--ink-2); }
|
||
|
||
/* The panel toggle only exists on a narrow screen, where it doubles as the
|
||
header — hence the city name in it. */
|
||
.panel-toggle {
|
||
display: none;
|
||
position: fixed;
|
||
top: var(--s3);
|
||
left: var(--s3);
|
||
z-index: 4;
|
||
align-items: center;
|
||
gap: var(--s2);
|
||
font: inherit;
|
||
font-size: 11px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
padding: var(--s2) var(--s3);
|
||
cursor: pointer;
|
||
color: var(--amber);
|
||
background: var(--glass);
|
||
backdrop-filter: var(--blur);
|
||
-webkit-backdrop-filter: var(--blur);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--r-sm);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.panel-toggle .glyph { font-size: 13px; line-height: 1; }
|
||
|
||
/* ---- Top right: the plan --------------------------------------------- */
|
||
.corner {
|
||
position: fixed;
|
||
top: var(--s4);
|
||
right: var(--s4);
|
||
width: 15rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: var(--s1);
|
||
}
|
||
#minimap { padding: var(--s1); display: flex; flex-direction: column; gap: var(--s1); }
|
||
/* A real height, always. The canvas is pinned to 100%/100% inline by
|
||
`minimap.ts`, and an `auto` height here puts back the feedback loop that
|
||
rule exists to break: the backing store feeds layout, layout feeds the
|
||
backing store, and the map doubles in size every frame. */
|
||
.minimap-frame {
|
||
width: 100%;
|
||
height: 14rem;
|
||
border-radius: var(--r-sm);
|
||
overflow: hidden;
|
||
}
|
||
.minimap-canvas { outline: none; cursor: crosshair; }
|
||
.minimap-canvas:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }
|
||
.minimap-readout {
|
||
margin: 0;
|
||
padding: 0 var(--s1) 2px;
|
||
font-size: 10px;
|
||
letter-spacing: 0.05em;
|
||
color: var(--ink-3);
|
||
min-height: 1.3em;
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.tier {
|
||
margin: 0;
|
||
padding: var(--s2) var(--s3);
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: var(--s2);
|
||
font-size: 9px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--ink-3);
|
||
}
|
||
.tier .who { color: var(--ink-2); text-transform: none; letter-spacing: 0.04em; }
|
||
.tier.god .who { color: var(--amber-ink); }
|
||
.tier a { color: var(--amber-ink); text-transform: none; letter-spacing: 0.04em; }
|
||
|
||
/* ---- Bottom right: what you picked, and how to drive ------------------ */
|
||
.rail {
|
||
position: fixed;
|
||
right: var(--s4);
|
||
bottom: var(--s4);
|
||
z-index: 3;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: var(--s1);
|
||
/* Wide enough that the key hints stay on one line at desktop widths —
|
||
a shortcut list that wraps reads as five separate notes rather than
|
||
one row — and narrow enough that a long detail card never becomes a
|
||
second panel. */
|
||
max-width: min(30rem, calc(100vw - var(--s4) * 2));
|
||
}
|
||
#detail { max-width: 100%; }
|
||
.hint {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: var(--s1) var(--s3);
|
||
padding: var(--s2) var(--s3);
|
||
font-size: 10px;
|
||
letter-spacing: 0.04em;
|
||
color: var(--ink-3);
|
||
}
|
||
kbd {
|
||
font: inherit;
|
||
font-size: 9px;
|
||
padding: 1px 4px;
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 3px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--ink-2);
|
||
}
|
||
.help {
|
||
font: inherit;
|
||
font-size: 10px;
|
||
letter-spacing: 0.04em;
|
||
padding: 2px 6px;
|
||
cursor: pointer;
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--r-sm);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
color: var(--ink-2);
|
||
transition: background var(--t), color var(--t);
|
||
}
|
||
.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
|
||
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. */
|
||
.source {
|
||
position: fixed;
|
||
left: var(--s4);
|
||
bottom: var(--s4);
|
||
margin: 0;
|
||
z-index: 3;
|
||
max-width: calc(100vw - var(--s4) * 2);
|
||
font-size: 10px;
|
||
letter-spacing: 0.04em;
|
||
color: var(--ink-3);
|
||
background: var(--glass);
|
||
backdrop-filter: var(--blur);
|
||
-webkit-backdrop-filter: var(--blur);
|
||
border: 1px solid var(--hairline);
|
||
box-shadow: var(--shadow);
|
||
padding: var(--s1) var(--s2);
|
||
border-radius: var(--r-sm);
|
||
}
|
||
.source.live { color: #8fe89a; }
|
||
|
||
/* ---- Overlays --------------------------------------------------------- */
|
||
.overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 10;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: var(--s4);
|
||
background: rgba(4, 7, 11, 0.55);
|
||
backdrop-filter: blur(3px);
|
||
}
|
||
.sheet {
|
||
background: var(--glass-strong);
|
||
width: min(26rem, 100%);
|
||
max-height: calc(100dvh - var(--s5) * 2);
|
||
overflow-y: auto;
|
||
padding: var(--s4);
|
||
}
|
||
.sheet h2 { margin: 0 0 var(--s3); font-size: 11px; letter-spacing: 0.2em;
|
||
text-transform: uppercase; color: var(--amber); }
|
||
.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; }
|
||
.keys dd { margin: 0; color: var(--ink-2); }
|
||
.sheet-close {
|
||
margin-top: var(--s4);
|
||
font: inherit;
|
||
font-size: 11px;
|
||
padding: var(--s2) var(--s3);
|
||
cursor: pointer;
|
||
width: 100%;
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--r-sm);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--ink);
|
||
}
|
||
.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
|
||
heightfield, which reads as a broken site rather than a busy one. This
|
||
card is in the document, so it is on screen at first paint — before the
|
||
module graph has even been fetched — and `main.ts` only ever writes the
|
||
step line and then fades the whole thing out.
|
||
|
||
It is deliberately not a spinner. A spinner says "wait"; naming the
|
||
thing being built says "wait, for this", and the sentence about running
|
||
with no server is the one fact about this project worth reading while
|
||
you have nothing else to do. */
|
||
.boot {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 20;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: var(--s4);
|
||
background: radial-gradient(120% 90% at 50% 30%, #172029 0%, #0b0f14 70%);
|
||
transition: opacity 260ms ease;
|
||
}
|
||
.boot.done { opacity: 0; pointer-events: none; }
|
||
.boot-inner { width: min(22rem, 100%); text-align: left; }
|
||
.boot-mark { margin: 0; font-size: 12px; letter-spacing: 0.42em; text-transform: uppercase;
|
||
color: var(--amber); }
|
||
.boot-sub { margin: var(--s2) 0 0; font-size: 10px; letter-spacing: 0.08em; color: var(--ink-3); }
|
||
.boot-bar {
|
||
margin: var(--s4) 0 var(--s2);
|
||
height: 2px;
|
||
border-radius: 2px;
|
||
background: rgba(255, 255, 255, 0.09);
|
||
overflow: hidden;
|
||
}
|
||
.boot-bar i {
|
||
display: block;
|
||
height: 100%;
|
||
width: 34%;
|
||
border-radius: 2px;
|
||
background: linear-gradient(90deg, transparent, var(--amber), transparent);
|
||
animation: sweep 1400ms cubic-bezier(0.45, 0, 0.55, 1) infinite;
|
||
}
|
||
@keyframes sweep {
|
||
0% { transform: translateX(-100%); }
|
||
100% { transform: translateX(340%); }
|
||
}
|
||
.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); }
|
||
|
||
/* ---- 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. */
|
||
@media (max-width: 900px) {
|
||
.panel-toggle { display: flex; }
|
||
#panel {
|
||
padding-top: 56px;
|
||
width: min(18rem, calc(100vw - var(--s3) * 2));
|
||
z-index: 3;
|
||
}
|
||
body.panel-closed #panel {
|
||
transform: translateX(calc(-100% - var(--s3)));
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.corner { width: 11.5rem; top: var(--s3); right: var(--s3); }
|
||
.minimap-frame { height: 10rem; }
|
||
.rail { right: var(--s3); bottom: var(--s3); }
|
||
.source { left: var(--s3); bottom: var(--s3); }
|
||
}
|
||
|
||
@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. */
|
||
.hint span { display: none; }
|
||
.source { max-width: 60vw; }
|
||
}
|
||
|
||
/* 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. */
|
||
body.minimap-off .corner { display: none; }
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*, *::before, *::after {
|
||
transition-duration: 1ms !important;
|
||
animation-duration: 1ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
}
|
||
.boot-bar i { width: 100%; transform: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="scene" aria-label="Map of San Francisco, seen from above. Drag to orbit, scroll to zoom."></canvas>
|
||
|
||
<button id="panel-toggle" class="panel-toggle" aria-expanded="true" aria-controls="panel">
|
||
<span class="glyph" aria-hidden="true">☰</span><span id="panel-toggle-label">Bay Area</span>
|
||
</button>
|
||
|
||
<div id="panel">
|
||
<section class="card">
|
||
<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>
|
||
<p class="card badge" id="office-badge" hidden></p>
|
||
<nav id="chapters" aria-label="Chapters"></nav>
|
||
<p class="card" id="blurb"></p>
|
||
</div>
|
||
|
||
<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 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>
|
||
<button id="help" class="help" aria-haspopup="dialog">? shortcuts</button>
|
||
</div>
|
||
</div>
|
||
|
||
<p id="source" class="source"></p>
|
||
|
||
<div class="overlay" id="shortcuts" role="dialog" aria-modal="true"
|
||
aria-labelledby="shortcuts-title" hidden>
|
||
<div class="card sheet">
|
||
<h2 id="shortcuts-title">Keyboard</h2>
|
||
<dl class="keys">
|
||
<dt><kbd>1</kbd>–<kbd>9</kbd></dt><dd>Fly to a chapter, or an office viewpoint</dd>
|
||
<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><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>
|
||
<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>
|
||
<button class="sheet-close" id="shortcuts-close">Close</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="boot" id="boot">
|
||
<div class="boot-inner">
|
||
<p class="boot-mark">Tera</p>
|
||
<p class="boot-sub">Lumbridge Simulate · cities from above</p>
|
||
<div class="boot-bar"><i></i></div>
|
||
<p class="boot-step" id="boot-step">Starting up</p>
|
||
<p class="boot-note">Runs with no server, no key and no account. The markers are
|
||
fabricated — no real company data ships in this build.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module" src="/src/main.ts"></script>
|
||
</body>
|
||
</html>
|