← Back to Cornice

[ 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.

static · no build stepvanilla JSAnton / Inter / Space Monolight + darkreduced-motion aware

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

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.

← Back to Cornice