Skip to content
shupp.dev / portfolio

--lx -0.220 · --ly -0.975 · loop asleep

VOID · v1.1 · CERAMIC MATERIAL

Void

Depth lives at the edge of a thing, never inside it.

A dark, single-mode system built on one perceptual trick. Every object is drawn with a 1.5px hairline of absolute black; a hard, short light gradient sits immediately inside it and a soft, long dark one immediately outside. The asymmetry drives lateral inhibition harder than a real edge would, so a face that is one flat colour edge to edge reads as glazed, lifted material. Two objects filled with the same hex can be two different substances. Press Strip the pair above to take the gradients away and watch the page fall flat — the fill never changes.

INVARIANTS I1–I3 · THE BOUNDARY

One hairline, two unequal gradients

The mechanism is the Cornsweet illusion. All three discs below are filled with exactly --s2 — sample the centre of each with a colour picker and you get the same value. Only the twelve pixels either side of their edge differ.

hairline alone

.lit — pair + decay

.lit--ceramic — decay 0

.lit--sunk — pair inverted

I1

The hairline is #000000, 1.5px, alwaysOn every object, at every size. It never scales, never takes a colour, never becomes a border-width token. It is a boundary marker, not a stroke weight.

I2

The page is never blackAbsolute black has to be darker than its surround, or the hairline stops reading as a void and starts reading as fill. The page floor is --s0, #101118.

I3

The pair is asymmetricInside: hard and short — 1.5px offset, zero blur. Outside: soft and long — 7 to 9px offset, 12 to 16px blur. Symmetric decay cancels the illusion and leaves you with something merely blurry.

I6

Fills are flatA component has one background value. All variation lives in box-shadow. There is no gradient anywhere in this system that is not a shadow.

§3 · SURFACE LADDER

Four values, lightest object on darkest page

Each step is roughly +6 per channel. Four surfaces is the whole ladder — a design that appears to need a fifth needs a different depth preset instead. Status hues sit at low chroma deliberately: a 1.5px band at 42% alpha against --s2 is already near the perceptual limit, and saturated hue blooms on near-black panels. To make a state clearer, raise --lift, never the saturation.

--s0 page
--s1 panel · well · nav
--s2 component
--s3 raised part

§4 · THE DEPTH PRIMITIVE

Three presets over one shadow structure

Material light is directional and always white (I4) — it describes what an object is made of, never what it is doing. --decay and --bleed are relative and scale with the object; the hairline is absolute and does not. Below 80px the decay band has no room to resolve, so switch to ceramic rather than continuing to compress it.

.lit

The default. Four layers: black bleed, hairline, hard contact light, soft inward decay. Everything rectangular that is not an input.

--lift: 0.19;
--bleed: 12px;
--decay: 15px;
background: var(--s2);

.lit--ceramic

Decay zero. No interior information at all — the purest form of the thesis, the cheapest to draw, and the only preset that cannot band.

--lift: 0.26;
--bleed: 16px;
/* no decay layer */

.lit--sunk

The pair inverted. One flip supplies the entire input vocabulary: field wells, toggle tracks, progress channels, log panels, pressed buttons.

background: var(--s1);
inset (dark) · inside
inset (light) · outside

Scale ruledecay ≈ 0.07 × min(w, h) · bleed ≈ 0.06 × min(w, h) · lift = clamp(0.19 + 0.14 × (1 − size/200), 0.19, 0.34). Small objects get a shorter, brighter band.

Dither is not optional polishA gradient from #000 to #14151a crosses about twenty 8-bit levels over hundreds of pixels; untreated, .lit shows concentric rings on OLED. This page carries one fixed feTurbulence overlay at 2.8%, and the WebGL ring dithers in-shader at ±0.5/255.

§5 · THE STATUS PRIMITIVE

State light is omnidirectional and tinted

inset 0 0 0 1.5px — no offset, no blur. A complete, even ring is something no light position could ever produce, which is precisely why hue can never be misread as lighting (I5). Status is a token, not a component, so the same signal works at a 6px dot and a 116px disc with no second implementation. Note that --lift is 0.42 here against 0.19 for neutral: that is matched luminance, not matched alpha.

4

healthy

122 214 160

healthy

.is-success · lift 0.42

1

degraded

236 194 106

degraded

.is-warning · lift 0.42

1

failed

240 138 128

failed

.is-danger · lift 0.42

2

running

138 180 240

running

.is-info · lift 0.42

paused

255 255 255

paused

.is-idle · lift 0.10

I8 · greyscale audit

The deuteranopic separation between the amber and the red at this luminance is thinner than it looks in a swatch. Every ring is therefore paired with a dot, a word, or both — desaturate the row above and nothing is lost.

§6 · LIGHT MODEL

Fixed rest, impulse on action, asleep between

One global vector, unit length, written by one rAF spring into --lx / --ly. It does not track the pointer: move your cursor across this page without clicking and nothing changes at all. Press any control and the light takes an impulse toward that element, then springs back to rest — motion is evidence that something happened (I7). When the spring settles the loop cancels itself, so the page costs zero scripting between interactions.

Reference monitor · live

--lx -0.220 · --ly -0.975

rest 347° · stiff 0.14 · damp 0.76

frames 0 · loop asleep

Every shadow on the page is a calc() of these two numbers. No element is ever touched by JavaScript.

Impulse sources

pointerdown on a control injects velocity toward that element's screen position at gain 0.055; focusin does the same at 0.03, so keyboard navigation lights the page exactly like a click. Settling takes about 400ms with one small overshoot.

const FIXED = { x: -0.22, y: 0.97 };
const STIFF = 0.14, DAMP = 0.76;

light.vx += (FIXED.x - light.x) * STIFF;
light.vx *= DAMP;
light.x  += light.vx;

§7 · THE CONFORMAL RING

One Möbius map, no easing function anywhere

The single WebGL surface on the page. Its falloff field is warped by a disc automorphism, w = (z − a) / (1 − āz), which maps the unit disc to itself and fixes the boundary. The silhouette therefore stays a mathematically exact circle while the interior level sets are pushed off-centre, and a constant-width band in w-space becomes a variable-width band on screen: calligraphic line weight, free. Taking the arc angle in w-space rather than z-space makes the progress head accelerate and decelerate around the ring with no hand-authored easing at all. a is a single complex number — it is the light vector.

Release

api-gateway · main

3d10bb4
Batch ingest writes, drop per-row commit
ready

Drive modes

01

Stilla = 0, arc off. Terminal states stop dead and switch to a still tinted ring, so motion unambiguously means “in progress”.

02

Indeterminatea = 0.7·(cos ωt, sin ωt). The band thickens and thins as the map compresses and expands it.

03

Determinatea follows the arc head; progress is the fraction. Watch the head slow at the top of the circle and quicken at the bottom.

At most one WebGL context per page — context limits are real and mobile browsers drop contexts on backgrounding. This canvas handles webglcontextlost by remounting, caps its backing store at 2× DPR, and degrades to a static CSS ring carrying the same hairline if a context cannot be obtained. The loader is the only component in the system permitted to animate without a user action, because ongoing work is itself the event.

§8 · COMPONENTS

Geometry is normative

Every component is one of the four presets plus layout. There are no bespoke shadows below this line. A button press inverts the pair rather than moving the button, so a press can never cause layout shift; focus takes a tinted omnidirectional ring, because white contact light at 0.16 over --s2 cannot clear WCAG 2.4.11's 3:1 and the tinted ring can.

Controls

pill 37 × r19 · field 37 × r10 · toggle 46 × 27, travel 3.5 → 22.5

Knobs · thresholds

Error budget · 68%

CPU target · 45%

Scale ceiling · 82%

78px · indicator 2 × 15px in --edge, so it reads as a cut

Rows · discs · avatars

api-gatewayus-east-1 · 12 tasks
42ms
healthy
ingestus-west-2 · 24 tasks
840ms
degraded
99.94%
UPTIME
4.2k
RPS
ES

Well · machine text

ingest scaled to 24 tasks
ingest p95 above 500ms threshold
search reindex complete — 4.1M docs
billing health check failed (3/3)
auth deployed — 8f2c91a
api-gateway deployed — 3d10bb4

Nav rail · ≤720px projection

§8 · SIZES × STYLES

The same object, all the way down

Five button sizes, four styles, three badge sizes, two badge styles — and one 1.5px hairline through every cell of it. That invariance is the test: if the boundary had scaled with the box, the small end of each row would read as a different family. Sizes change the box and the type; they never change the edge.

Button · 5 sizes / 4 styles

xs · 26sm · 31md · 37lg · 44xl · 52 default ghost danger disabled

Badge & chip · 3 sizes / 2 styles

healthy degraded failed running paused sm · 21
healthy degraded failed running paused md · 25
healthy degraded failed running paused lg · 30
healthydegradedfailedrunning ring only

Type scale

display Boundary 34 / 26 / 20 / 16
ui Deploy to production 15 / 13.5 / 12 / 11
text The fill is one value edge to edge. 15 / 13.5 / 12.5
mono 3d10bb4 · 14:22:07Z 12.5 / 11

Four roles and no weight above 600. The display face floors at 16px and the text face at 12px — below those the first loses its shape and the second loses its strokes.

Every token in the system

--s0 page
--s1 well
--s2 component
--s3 raised
--ink primary
--ink-2 secondary
--ink-3 tertiary
--ink-display display
--hue-success healthy
--hue-warning degraded
--hue-danger failed
--hue-info running
--hue-neutral paused

Sixteen values, and the four surfaces are the whole ladder. The readouts are live: fire the page into the other mode and every one of them changes, because a mode in this system is a token set and nothing else.

§7–9 · PRESS, DISCLOSURE, SCROLL

Everything that moves, moves as material

Three kinds of motion exist in this system and no others. A press flips which side of the hairline receives light, so the surface takes the press while the object stays exactly where it is. A disclosure grows an object out of nothing, hairline and black bleed drawn correctly at every intermediate height — never a fade, never a scale. And scroll produces readouts, not animations: a rail that reports position and a header that reports it has pinned. Behaviour ships in void.js, which owns the page's single rAF loop; the components below are legible without it.

Press · the detent

Hold either button. Down is instant, the release settles over 170ms — that asymmetry is what makes it read as a detent rather than a fade. The black bleed goes first, because a pressed object is no longer above the surface and casts nothing.

.is-pressed · no translate, no scale, no layout shift

Look at the two icons: the incision inverts with the surface it is cut into. On the raised button the light sits above the stroke; on the pressed one it sits below it.

Disclosure · menu

The panel is the element that carries the depth preset, and it is the element being sized — so the hairline is drawn around the real shape at every frame. Animate a wrapper instead and the shadow detaches from the object.

220ms open · Esc, ↑ ↓, Home, End · focus returns to the trigger

Disclosure · fold

Revealed content sits below the surface that revealed it: the panel is a well, not a raised card. One open at a time within a group.

Reverts the revision automatically when the error rate crosses the threshold for two consecutive intervals. The rollback itself is a deploy, and it shows up in history as one.
Routes 5% of traffic to the new revision, holds for the bake window, then either promotes or reverts. No manual step in between.
Production deploys need a second reviewer. The requester cannot approve their own change, including via an automation token.

Scroll · two readouts

Scroll the list. Each group header gains a hairline the moment it pins — a material change that says “you have crossed into this group”, not a transition that plays while you scroll. The rail beneath it reports the list's position, exactly as the rail under the control bar reports this page's.

us-east-1
api-gateway
ok
auth
ok
billing
ok
notifier
ok
us-west-2
ingest
ok
compactor
ok
archiver
ok
eu-west-1
search
ok
indexer
ok
suggest
ok

writes coalesce to one rAF and skip below Δ0.4% — a settled list writes nothing

The entrance animates the light, not the objectEvery card in this section arrived by taking --lift-scale from 0 to 1 — a registered custom property, so it interpolates. The object was already in place at its final size and position; the light simply had not found it yet. Nothing translates, so nothing reflows, and the layout is identical at every frame of the entrance.

Everything above still sleepsPresses, menus, folds and pins are CSS transitions on discrete state changes — they need no loop at all. The rAF loop exists only for the light spring and the ring, and it cancels itself the moment both are done.

§8.1 · ICONS & BREADCRUMBS

An icon is an incision, not a drawing

The path is absolute black — the same ink as the hairline — and it carries the same asymmetric pair: dark line, light edge offset opposite the light vector. That is what makes it read as cut into the surface rather than printed on it. vector-effect: non-scaling-stroke holds the weight at 1.5px from 14px to 40px, exactly as the hairline never scales with its element.

incised — default

raised — sign flipped

flat — no pair

14 · inline
16 · dense
20 · default
24 · touch
40 · feature
01

24 × 24 grid, 2px safe marginLive area is 20 × 20. Nothing touches the frame.

02

Stroke only, no fillsA filled shape is a surface, and surfaces get the depth primitive. Icons are lines.

03

Angles at 0°, 45°, 90° onlyArcs otherwise. Arbitrary diagonals break the family faster than any other rule.

04

Icons never take hueStatus is the ring. An icon may sit inside a tinted chip; it never becomes the tint.

05

Never load-bearing aloneEvery icon in a nav or a control has a text label beside it. The set is a scanning aid, not a vocabulary.

discover
radar
capture
pipeline
profile
agency
calendar
clock
bell
filter
flag
track
target
trend
check
close
plus
chevron-right
chevron-left
chevron-down
external
download
refresh
sliders
lock
warning
info
more

Breadcrumb · desktop projection

Ancestors dim, current node bright and never a link. The separator is a .icon--mark: a dim stroke, not a cut, because a separator is not an object and does not get the light treatment.

Breadcrumb · mobile projection

IT Support Services BPA

The back control names the parent — never a bare chevron — so the hierarchy stays legible with only one node visible. Item names truncate; ancestors never do.

§9 · TYPE

Four roles, and a display face that blooms

Heavy display type blooms on near-black, so the display role gets its own, dimmer ink (--ink-display) and a hard floor at 16px, below which the face loses its shape. Supporting prose runs at 300 weight and never sits on --ink-3 — thin strokes vanish against low-contrast surfaces. Every value that updates in place takes tabular figures, because proportional digits jitter horizontally and that reads as flicker here.

display · Rammetto One 400 · ≥16px · --ink-display · tracking −0.015em

Boundary

ui · Cabin 400/500/600 · anything you click, label or scan

Deploy to production · Require approval · 6 services across 3 regions

text · Titillium Web 300 · ≥12px · supporting prose

The fill is one value edge to edge; all the signal is at the boundary.

mono · machine text only: SHAs, timestamps, log lines

3d10bb4 · 14:22:07Z · p95 840ms · --lift 0.42

§12 · ANTI-PATTERNS

The failures that collapse it into a generic dark theme

These are what implementing from a description rather than from the rules produces. Each one is individually reasonable and collectively fatal.

Give the hairline a colour to show statusBreaks I1 and I5. Status goes inside the hairline.

Scale the hairline with element sizeIt is a boundary marker, not a stroke weight. Scaling turns the language from “flat with an edge” into “outlined”.

Make the page background blackAbsolute black must be darker than its surround (I2).

Use symmetric blur on both sides of the edgeCancels the illusion; looks merely blurry.

Add a gradient to a fillAll variation lives in box-shadow (I6).

Track the cursor with the lightAmbient motion during reading (I7).

Translate or scale a button on pressLayout shift. The system changes material instead.

Add a fifth surface valueUse a different depth preset.

Raise status saturation for clarityBlooms on OLED. Raise --lift instead.

Ship the default preset without ditherVisible banding on OLED.

Mount a WebGL context per componentContext limits and mobile context loss. One per page.

Use emoji or icon fonts for statusThe ring and the dot are the status vocabulary.

Fade or scale a panel openA scaled hairline is not 1.5px and a faded one was never there. The panel is an object: grow it.

Animate a wrapper instead of the objectThe shadow detaches from the shape. Size the element that carries the depth preset.