The shared layer
Zane
design system
Everything /, /easyagent/ and /llmbench/
have in common, in four files. Ink on paper, hairline rules, deep navy for
emphasis — plus a WebGL dither field and a character-grid renderer for texture.
This page loads the system exactly the way a real consumer does, so it breaks
when the system breaks.
01 / Adopt
Four files
shared/editorial.css- Tokens (light and dark), reset, nav, buttons, footer, reveal keyframes, the shader-canvas CSP rule, view transitions, reduced motion.
shared/motion.js- Scroll reveals. Page-agnostic — nothing in it knows about a particular page.
shared/backdrop.js- One function: a dithering field mounted into a host element.
shared/ascii.js- One function: an image resolved to a character grid.
backdrop.js resolves its vendored shaders relative to itself, so the
whole shared/ directory can sit at a site root or in a subdirectory.
The head, verbatim
<script>
document.documentElement.classList.add("js");
document.documentElement.dataset.theme =
localStorage.getItem("bz-theme") || "dark";
</script>
<link rel="stylesheet" href="/shared/editorial.css">
<link rel="stylesheet" href="your-page.css">
The inline script must stay inline: html.js gates every
hidden-until-revealed style, so a no-JS visitor sees everything, and the theme
has to resolve before first paint or the page flashes white. Dark is the
default — the OS preference is deliberately not consulted. The
bz-theme key is shared across every page on the domain.
Load editorial.css before your page stylesheet.
Page CSS may override anything in the layer; the layer never reaches into a page.
02 / Tokens
The ramps
Component rules use tokens only, never literal colors — that is what lets
EasyAgent's body[data-profile] theming (Frampton's dark inversion
especially) cascade over everything. Flip the theme in the nav: every swatch below
is live, and the paper and ink ramps invert from the shared file.
03 / Type
Three stacks, three jobs
Source Serif 4 — body copy and headlines. The voice of the page.
Inter Tight — navigation, labels, UI. Present but not speaking.
JetBrains Mono — tags, specs, figures. 0123456789 · ↗ □ ●
Playfair Display italic — exactly one word per page.
04 / Components
Chrome
Buttons
.ed-cta-secondary and .ed-cta-ghost both set
appearance: none and a transparent background — dropping the
native look alone leaves the UA's buttonface behind, which paints a
near-white pill on a dark page.
Section label
Nav
.ed-nav is sticky, blurred paper — the subpage treatment, and what
you are looking at above. .ed-nav--overlay is fixed, transparent and
mix-blend-mode: difference; its white is literal, because difference
inverts it against whatever scrolls under. It exists for the main page's photo hero.
Focus & skip link
Tab into the page from the address bar: the skip link slides down from the top-left.
Every focusable element gets a 2px --blue-500 ring at 2px offset,
:focus-visible only — never on mouse click.
05 / Motion
Reveals
motion.js adds .is-inview on intersection and unobserves.
Reveals are keyframe animations, not transitions, so they never clobber a
component's own transition — a work row can hover-animate while its
reveal is still running.
data-reveal="rise"
Fade up 14px. The default for a block of content.
data-reveal="rule"
Slide in 10px from the left. For section labels and hairlines.
data-reveal="cascade"
Children stagger 45ms via --i, which motion.js sets. Dotted .dots leaders draw in alongside.
- Cascade child01
- Cascade child02
- Cascade child03
- Cascade child04
- Cascade child05
Reduced motion is handled in three places that must stay in sync:
motion.js short-circuits to final states, the shared CSS block kills
every reveal and transition, and each page's own block covers its local effects.
Any new effect must be covered in all three.
06 / Shaders
Dither backdrops
A vendored subset of paper-shaders
(Apache-2.0). One shader is wired up — dithering — across seven field
shapes and four dither matrices. mountBackdrop() parks at
setSpeed(0) when its host scrolls off screen, when the tab is hidden,
and under reduced motion, which cancels the rAF outright.
import { mountBackdrop } from "/shared/backdrop.js";
mountBackdrop(hostOrSelector, {
back, front, // two colors; the field dithers between them
shape, // 1 simplex · 2 warp · 3 dots · 4 wave · 5 ripple · 6 swirl · 7 sphere
type, // 1 noise · 2 Bayer 2x2 · 3 Bayer 4x4 · 4 Bayer 8x8
pxSize, scale, speed, frame,
observe, // false for fixed hosts that are always on screen
}); // → { setColors(back, front) }, or null when the host is absent
Host contract: an empty aria-hidden div that is a
sibling of the content, never a wrapper. The content wrapper carries
position: relative; z-index: 1; the host's parent carries
position: relative; overflow: hidden. The module adds
.is-live only once a canvas exists, so a CSS fallback shows through
when WebGL is unavailable. Canvas positioning comes from the
[data-paper-shader] > canvas rule in editorial.css,
because the production CSP blocks the inline <style> the library
injects — without it a backdrop works locally and is invisible in production.
Field shapes · Bayer 4×4
Dither matrices · simplex field
07 / ASCII
Character grids
There is no ASCII shader in paper-shaders — dithering emits 1-bit
pixel cells, not glyphs. So ascii.js is the character-grid counterpart
to backdrop.js: the same quiet-texture job, done with type. No WebGL
context, nothing to dispose, nothing for the reduced-motion blocks to suppress —
the output is a string.
import { asciiFromImage } from "/shared/ascii.js";
host.textContent = await asciiFromImage("/assets/work/shuttrr-row.webp", {
cols, // grid width in characters
charset, // dense → sparse glyph ramp
});
Bright pixels get the heaviest glyph, because these marks are drawn as light ink on a dark ground. Each image is stretched to its own 2nd/98th-percentile black and white points first — without that a dark frame renders as a blank grid. The Shuttrr row below has 78% of its pixels below 0.1 luma.
Ramp · default charset
Specimen · the Shuttrr work row
Hand-authored ASCII is the other half of the story: EasyAgent's per-agent wordmarks
and mascots are figlet string arrays set with textContent, never
innerHTML. Meaningful marks carry an aria-label;
decorative ones carry aria-hidden, like the specimen above.