Alpenglow Expeditions ← Back to the mountain
Field Notes · Designed & built autonomously by Claude Fable 5

One scalar runs the whole sky.

Alpenglow Expeditions is a fictional mountaineering outfitter built as a design showcase. Its centrepiece is a single full-viewport fragment shader — sun, scattering, clouds, spindrift, stars, aurora, four ridgelines — interpolated by one number: scroll progress. This page explains how, with the real code. It also runs the system on itself: drag the scrubber in section 03 and this page changes weather.

01 — Concept & direction

The site ascends

The brief: a six-day traverse told as one continuous climb. Scroll is altitude — an instrument HUD ticks from 1,200 m at basecamp to 4,808 m on the summit while the sky runs dawn → alpenglow → storm → dusk → summit night. The mountains and sky are 100% procedural: no photographs, no video, just mathematics lit by a palette of four claimed colours (pre-dawn #16233A, snow #F2F5F9, alpenglow #FF7E6B, pine #2C4A43).

Two voices carry the copy: Sentient, a serif with real warmth, for the romance — and Instrument Sans for the logistics: gear tags, altitudes, departure tables. Mountaineering is exactly this pairing; poetry administered through checklists.

02 — Palette & type

Four claimed colours, and a live sky

The first four chips are the locked brand claim. The second row is live — it shows the CSS custom properties the atmosphere is writing right now, on this page.

#16233APre-dawn
#F2F5F9Snow
#FF7E6BAlpenglow
#2C4A43Pine
--sky-zenith
--sky-horizon
--sky-glow
--glow-text

Sentient carries the romance,

and Instrument Sans carries the logistics: 2 × 60 m half ropes, −18° comfort rating, 4 L of melted water per climber per day, one guide to three climbers.

Instrument Sans · 11px · 0.18em tracking — the voice of every label on the mountain

03 — The signature technique

Scroll → sun elevation → palette → CSS variables

A ScrollTrigger spanning the whole journey emits one value, p ∈ [0,1]. A pure function atmos(p) interpolates eleven hand-set keyframes into a full atmospheric state: sun elevation, warmth, storm, night, aurora, plus the three palette anchors. That state goes two directions at once — into the shader uniforms, and into CSS custom properties. The GPU and the DOM read the same palette, so panels, rules, buttons and text are literally lit by the sky.

Progress0.88
Altitude
Sun elev.
Storm
Night
Aurora

atmosphere.js — keyframes to state (excerpt)

const KEYS = [
  { p: 0.00, sunEl: -0.07, warmth: 0.45, storm: 0.10, night: 0.32,
    zenith: [16,27,48],  horizon: [58,83,120],   glow: [232,146,107] }, // dawn
  { p: 0.20, sunEl:  0.035, warmth: 1.00, storm: 0.10, night: 0.00,
    zenith: [34,67,107], horizon: [255,142,112], glow: [255,178,126] }, // alpenglow
  { p: 0.56, sunEl:  0.42, warmth: 0.02, storm: 1.00, night: 0.00,
    zenith: [30,40,51],  horizon: [70,82,93],    glow: [108,120,131] }, // storm peak
  { p: 0.87, sunEl: -0.45, warmth: 0.00, storm: 0.04, night: 1.00,
    zenith: [6,10,22],   horizon: [22,35,58],    glow: [143,227,198] }, // summit night
  // ...11 keys total
]
const smooth = t => t * t * (3 - 2 * t)   // smoothstep between neighbours

The bridge — same state, two consumers

function setP(p) {
  state = atmos(p)     // one pure function
  sky.set(state)       // → GPU: uZenith, uHorizon, uGlow, uStorm, uNight…
  applyAtmos(state)    // → DOM: --sky-zenith, --panel, --rule, --glow-text
}

// applyAtmos, the DOM half. Panels are darkened zenith; accents are the
// glow colour lifted until it reads as text on them:
st.setProperty('--panel', rgb(lerp3(s.zenith, [8,13,24], 0.55), 0.58))
st.setProperty('--glow-text', rgb(liftForText(s.glow)))

sky.frag.glsl — the scattering ramp (excerpt)

// horizon hue climbs higher into the sky when the sun is low
float sy   = clamp((uv.y - HOR) / (1.0 - HOR), 0.0, 1.0);
float ramp = mix(0.52, 0.85, clamp(uSunEl * 1.6 + 0.5, 0.0, 1.0));
vec3  col  = mix(uHorizon, uZenith, pow(sy, ramp));

// warm band hugging the horizon + mie-ish forward scatter around the sun
col += uGlow * exp(-abs(uv.y - HOR) * 6.5) * uWarmth * 0.38;
col += uGlow * sunVis * (disc * 1.1 + mie * 0.55 + bloom * 0.30);

Why sample the model on the CPU instead of gl.readPixels? Reading the framebuffer back every frame stalls the pipeline; recomputing the palette from the same keyframes costs nanoseconds and guarantees the DOM matches the GPU even before the first frame renders. The mountains sink as you climb — four ridged-noise silhouettes whose baselines drop with an uAscent uniform derived from the altitude track — and under prefers-reduced-motion the scrub is replaced by six static per-chapter states set by an IntersectionObserver.

04 — Asset pipeline

Two portraits. Everything else is math.

The sky, mountains, storm, stars and aurora ship as ~200 lines of GLSL — zero image weight. The only generated assets are the two guide portraits (Higgsfield soul_2, 1 credit each) and the OG image, which is a real render of the site's own dawn, captured headlessly. Masters live outside the build; the site ships optimised WebP at 800/1600px.

Portrait of guide Mara Vollen
guide-mara · soul_2

“Editorial portrait of a female alpine mountain guide, early 40s, weathered wind-burned face… faded coral-red hardshell… golden hour on a glacier ridge, warm alpenglow rim light, cool pre-dawn blue shadows, 85mm, documentary expedition photography.” The wardrobe was art-directed to the site palette: coral shell, pre-dawn shadows.

Portrait of guide Elias Rott
guide-elias · soul_2

Same lighting recipe, pine-green shell, “calm weather-reading gaze toward the horizon.” Shooting both guides in one consistent golden-hour setup is what makes them read as one expedition rather than two stock photos.

05 — Iteration diary

Three passes over the mountain