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
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.
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.
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.
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.
§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.
healthy
122 214 160
.is-success · lift 0.42
degraded
236 194 106
.is-warning · lift 0.42
failed
240 138 128
.is-danger · lift 0.42
running
138 180 240
.is-info · lift 0.42
paused
255 255 255
.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.
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
Drive modes
Stilla = 0, arc off. Terminal states stop dead and switch to a still tinted ring, so motion unambiguously means “in progress”.
Indeterminatea = 0.7·(cos ωt, sin ωt). The band thickens and thins as the map compresses and expands it.
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
Well · machine text
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
Badge & chip · 3 sizes / 2 styles
Type scale
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
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.
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.
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
24 × 24 grid, 2px safe marginLive area is 20 × 20. Nothing touches the frame.
Stroke only, no fillsA filled shape is a surface, and surfaces get the depth primitive. Icons are lines.
Angles at 0°, 45°, 90° onlyArcs otherwise. Arbitrary diagonals break the family faster than any other rule.
Icons never take hueStatus is the ring. An icon may sit inside a tinted chip; it never becomes the tint.
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.
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.
Boundary
Deploy to production · Require approval · 6 services across 3 regions
The fill is one value edge to edge; all the signal is at the boundary.
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.