[ Parable build guide ]
How Cornice was built
A high-alpine resort, rendered without a single photograph. The signature is a pair of linked pieces: a live lift-status board and an interactive SVG piste map — plus a falling-snow canvas that knows when to stop.
The idea
A ski resort lives on two questions you ask before you've had coffee: which lifts are turning, and what can I ski down. So those two became the heart of the page — a board that reads like the one at the base station, and a map you can actually poke. Everything else (the village, the passes) hangs off that. Voice is crisp and a little proud, the way a good resort talks: "94 runs. 3,100 vertical metres. One very good reason to set an early alarm."
The stack
Deliberately plain. One index.html, one styles.css, one
main.js. No framework, no bundler, no CDN — the snow is a hand-rolled 2D canvas, the
mountain and every run are inline SVG, so the page has zero runtime dependencies and ships
straight to GitHub Pages. Type is Anton (a tall condensed display face with the
energy of resort signage), Inter for body, and Space Mono
for the numbers a mountain runs on — altitudes, wait times, temperatures.
Signature technique — the board wired to the map
The lift board isn't a static table: wait-time numbers count up from zero when the board scrolls
into view, and each status pill carries a pulsing dot (green open, amber hold, red closed). The
counter is the same tiny requestAnimationFrame loop that drives the conditions strip —
one function, cubic-eased, reused for every number on the page.
// one counter, reused for conditions + lift wait times
const tick = (t) => {
const p = clamp((t - t0) / dur, 0, 1);
const eased = 1 - Math.pow(1 - p, 3); // cubic-out — a settle
el.textContent = (to * eased).toFixed(dec);
if (p < 1) requestAnimationFrame(tick);
};
The piste map is the interactive half. Each run is an SVG <path> carrying its
own name, length and grade in data- attributes, mirrored by a row in the run list
beside it. Hovering or focusing either the line or the row lights up its twin and writes
the readout; the grade filter just sets one attribute on the map and lets CSS fade the rest.
// hover a line OR its list row — both light up, readout updates
const activate = (id) => {
const rec = byId.get(id);
rec.path.classList.add('is-active');
rec.path.parentNode.appendChild(rec.path); // bring the run to the front
rec.row?.classList.add('is-active');
readout.innerHTML = name + diff + length;
};
// filter is pure CSS: .pistemap[data-filter="adv"] .run:not(.run--adv){opacity:.08}
Details that matter
- Cost, not vanity. The snow canvas caps device-pixel-ratio at 1.5, throttles to ~30fps, and scales its flake count to the viewport area (40–150 flakes) — so it stays smooth on a phone and invisible on the GPU.
- It pauses when unseen. An
IntersectionObserverstops the snow loop when the canvas leaves the viewport, and avisibilitychangelistener freezes it the moment you switch tabs. - Reduced motion draws one frame. Under
prefers-reduced-motionthe snow paints a single settled field, the pulses stop, and every counter/reveal resolves instantly — no animation loop runs at all. - Grade is never colour-only. Every run is labelled in the readout and the run
list, has an SVG
<title>, and the double-black is drawn dashed as well as dark — so difficulty survives colour-blindness and greyscale. - Content is never hidden without JS. The hero rise and scroll reveals are
gated behind a
.jsclass set synchronously in<head>— kill JavaScript and the whole page, board and map included, is fully legible.
The lift board, snow depth, prices and the pass checkout are a demo — the numbers are invented and "Choose 6-day" charges nothing. Wire the board to a real lift feed and the passes to a payment provider before selling a single day on the mountain.
Ship it on GitHub Pages
Nothing to build. Push the folder and point Pages at the root.
gh repo create bswxyz/cornice --public --source . --push
gh api --method POST /repos/bswxyz/cornice/pages -f 'source[branch]=main' -f 'source[path]=/'
Relative paths and a .nojekyll file mean it serves from the project subpath without
a single config change. That's the whole point of the static build.