Quirebound
Free

Meridian — Day/Night Studio Index

A studio portfolio that crosses from daylight to night as you scroll. One scroll value drives every colour token through color-mix, so the ground, ink, rules and accent turn over continuously — in lockstep with a shader that takes the sun down behind them. Selected work travels sideways on a sticky rail.

Meridian — Day/Night Studio Index

The prompt — 7 sections

The turn

The conceit of the page is one number.
Define the palette twice — once for daylight, once for after dark — and mix
the two live against a custom property called `--turn`:
```css
--ground: color-mix(in oklab, #E6E8E4 calc(100% - var(--turn, 0) * 100%), #080B14);
```
Do that for every token: ground, ink, muted text, accent, hairline rules. Mix
in `oklab`, not sRGB, or the midpoints go grey and muddy.
Two rules make this survive contact with a real browser:
- **Put both sets in a stylesheet, not an inline `style` attribute.** Declare
the daylight values unguarded, then re-declare the mixed values inside
`@supports (color: color-mix(in oklab, red, blue))`. An inline declaration
outranks the `@supports` rule and the page silently never turns; and a
browser that cannot parse `color-mix` still gets a complete, legible
daylight page rather than an invalid one.
- **Nothing in the markup should know which side of the meridian it is on.**
Components read `var(--ink)` and `var(--rule)`. There is no dark-mode
branch, no `isDark` prop, no class toggle.
Suggested palette. Day: ground `#E6E8E4`, ink `#151A17`, muted `#5C635C`,
accent `#0E7C6B`, rules `rgba(21,26,23,0.17)`. Night: ground `#080B14`, ink
`#E8ECF2`, muted `#8C93A4`, accent `#3FE0C4`, rules
`rgba(232,236,242,0.19)`. The accent crosses too — deep teal by day,
luminous aqua after dark.

Driving it

Write `--turn` from a single `requestAnimationFrame` loop, onto the page root,
from how far the hero film has been scrolled through:
```
progress = clamp01(-rect.top / (rect.height - innerHeight))
turn = clamp01((progress - 0.18) / (0.78 - 0.18))
smooth += (turn - smooth) * 0.10
root.style.setProperty("--turn", smooth)
```
Measure the film, not the document — the sun going down and the page going
dark have to be one event, not two things that happen to overlap.
Under `prefers-reduced-motion: reduce`, skip the loop and pin `--turn` to 1.
The work and contact sections are written for the dark half; landing there is
the honest static state.

Tide shader

The hero runs the same scroll-scrubbed WebGL surface as the rest of the
library, with a fragment program that crosses over on its own copy of the
turn: `smoothstep(0.18, 0.78, uProgress)`.
Compose it in two halves about a horizon at `uv.y = 0.56`.
**Sky.** Gradient from a low colour to a zenith colour, both mixed on the
turn — warm `#FDB873`-ish to cool near-black. Add a disc for the sun, plus a
wide `exp(-d * k)` bloom around it. Drop the disc from just above the horizon
to sitting on it as the turn advances, shrink it, and shift it from gold to
pale blue; it becomes a moon without ever being modelled as one. Fade hashed
stars up only once it is genuinely dark, and lay a `fbm` cloud band along the
horizon.
**Water.** The whole trick is the perspective divide. Below the horizon,
distance is `1 / (horizon - uv.y)`, so build the sample coordinate as:
```glsl
float depth = HZ - uv.y;
float persp = 1.0 / max(depth, 0.006);
vec2 wp = vec2(x * persp * 0.42, persp * 0.30 + uProgress * 1.4);
```
Clamp the divide or it aliases to noise at the horizon line. Texture it with
caustics — ridge two octaves of value noise (`1.0 - abs(n * 1.33 - 0.66)`) and
raise to a high power, which leaves only the crests. Add the glitter path: a
gaussian lane in `x` centred on the sun, narrowing as the sun drops,
multiplied by the caustics. After dark, add a second caustic field in
turquoise for bioluminescence.
Finish with a hairline of scatter right on the horizon,
`exp(-abs(uv.y - HZ) * 320.0)`.

Scrims

The daylight shot needs lifting so dark type reads; the night shot needs
deepening so light type reads. These are different gradients, not one gradient
in two colours — so use two stacked scrims cross-faded on `--turn`
(`opacity: calc(1 - var(--turn))` and `opacity: var(--turn)`) rather than
trying to mix them.

The work rail

Vertical scroll, horizontal travel. A section three-and-a-half viewports tall
holds a sticky, viewport-height stage; inside it a `max-content` track slides
sideways.
The travel is one CSS expression:
```css
transform: translate3d(calc(var(--rail-p) * (100cqw - 100%)), 0, 0);
```
`100%` is the track's own width and `100cqw` is the stage's — put
`container-type: inline-size` on the stage. The track therefore always ends
flush with the right edge whatever it contains: no measuring, no resize
handler, nothing to go stale when an image loads late. Use `cqw` rather than
`vw` or a scrollbar overshoots it.
Write `--rail-p` from the same smoothed loop as everything else.
Put one shared film behind the stage rather than a WebGL context per panel,
and let the panels pass over it as hairline cards at
`color-mix(in oklab, var(--ground) 62%, transparent)` with a small backdrop
blur. Pad the track with a gutter-width spacer at each end so the first panel
starts aligned with the type above it.
Under reduced motion the rail becomes an ordinary `overflow-x: auto` strip
with `scroll-snap-type: x mandatory` — same content, same gestures, no
hijack.

Page

1. **Hero** — eyebrow, a light-weight display line with one italic word in the
accent, a paragraph, and a circled ↓ reading "Scroll until dark".
2. **What we do** — a claim at `clamp(2rem, 5vw, 4rem)` and a paragraph.
3. **The turn** — names what is happening to the page as it happens.
4. **Selected work** — the rail.
5. **Capabilities** — four rows, a sticky heading beside them.
6. **Contact** — a second ambient tide, a headline, two pills.
Fixed masthead with a live state readout: a dot in the accent and two labels,
"Daylight" and "After dark", cross-faded on `--turn`. It is the same value the
whole page runs on, so it reports rather than decorates.

Type

Fraunces for display, at light weights and negative tracking — the optical
size axis does the work at large sizes and the wonk keeps it from reading as
a default serif. Instrument Sans for body. DM Mono for every uppercase label
at `0.2em``0.28em`, always 10px.
Scope the faces to the template's own custom properties and reference them
explicitly. A template that inherits `font-display` from its host site is not
a template.

More like this