# The open-source landscape for terminal, cyberpunk and minimal web UI A survey written while building [`examples/`](examples/) — fifty-nine runnable specimens, each driving a real library rather than a reimplementation of one. Every verdict below comes from putting the thing on screen, not from reading its README. **Method.** Candidates were found by search, then filtered on three things: licence, last activity, and whether the project is *usable as it ships* rather than as a starting point to fork. Each survivor was vendored at a pinned version (`scripts/vendor.sh`), driven from a specimen, and judged on what it actually did. Sizes below are the **vendored file, minified, not gzipped** — measured, not quoted from marketing copy. Dated **2026-08-31**, extended **2026-09-01** twice: first with the React strand, the era-GUI strand, sound, instrumentation, and two measured passes over the whole lab — [`audit.html`](audit.html) for accessibility and [`profile.html`](profile.html) for performance under load; then with the four specimens that close the two thinnest parts of the survey. Terminal gained the things that render an *actual* terminal (§1) rather than themes that look like one, and cyberpunk gained the register the rest of §2 was missing — machine vision and codec corruption, both carried by content that means something rather than by frame geometry. Extended again **2026-09-02**, and this pass mostly corrected the survey rather than adding to it. A new **interaction strand** (§9) gives the lab three specimens you operate instead of one, and two more measured passes went over everything — [`mobile.html`](mobile.html) at 390×844 under touch emulation, and [`announce.html`](announce.html) for what a screen reader would actually say. Between them they overturned a claim this file had been making since the first pass: the contrast failures that survived the high-contrast switch were **not** greys inside vendored sheets. Seventy-six of eighty-three were the lab's own specimens hard-coding a hex, and the reason nobody knew is that the audit page could count failures but never said whose colour they were. [`contrast.html`](contrast.html) is what asks. Extended once more **2026-09-02**, and this is the pass that went after the lab's blind spots rather than its gaps. Every specimen up to this point was dark, English, left-to-right, monospaced, rectilinear, and working — six assumptions so uniform nobody had noticed they were assumptions. Ten specimens break them, and two more strands hold the ones that did not fit anywhere: **data surfaces** (§10) for a table, a map and a waveform, and **typography** (§11) for the two specimens about the lab's own voice. The single most useful thing to come out of it is §12: a light control that shows how much of the house grammar was technique and how much was just the field being black. That control was removed on 2026-09-04 and §12 is now the record of what it found. Extended **2026-09-03**, and this pass added no libraries at all. It **split the era strand** (§6), which had been one specimen carrying five findings, into one per sheet plus the comparison — and the interesting part is that every one of the three claims §6 used to make was written from reading the sheets rather than counting them, and two of the three were wrong. Findings 85–92 are what the counting produced. Nothing was vendored, nothing was upgraded, and the section is longer than it was. Extended **2026-09-04**, and this pass **took the light out of the lab**. Eight surfaces were light — 28-bench and 38-ops-floor by design, and the six era-GUI specimens by quotation — and all eight are now dark. This is the one kind of pass that cannot add a finding by looking harder, because nothing was measured that had not been measured before; what it did instead was CHANGE ONE VARIABLE UNDER EVERY EXISTING MEASUREMENT AND SEE WHICH ONES MOVED. Most of §12 reversed, which is what §12 predicted. Two numbers in §13 that were expected to move did not move at all, which is finding 93 and the better result. And the era strand turned out to have been resting on something nobody had written down: **not one of the five sheets declares a single custom property**, across 683 kB, so there was no token to set and the palette had to be substituted by walking the parsed stylesheet. Findings 93–101, and `lab/relight.js` is the machinery. Stars and activity are from the GitHub API on those dates; they decay, the verdicts mostly do not. --- ## 1 · Terminal / TUI The monospace-grid register. The finding that governs the whole strand is specimen 04: **most of the "hacker UI" read is carried by the character grid itself**, before any library. A 5-row block-character bar chart with no dependency at all holds its own against everything else here. | project | ver | licence | ★ | last push | size | verdict | |---|---|---|---|---|---|---| | [WebTUI](https://github.com/webtui/webtui) | 0.1.10 | MIT | 2.4k | 2026-08 | 39 kB | **The strongest option.** Pure CSS, `@layer`-based so it composes instead of fighting you, and the box borders are real box-drawing characters rather than 1px rules faked to look like them. Attribute API (`box-="double"`, `is-="badge"`). Costs a hard commitment to a monospace grid. | | [TuiCss](https://github.com/vinibiavatti1/TuiCss) | 2.1.2 | MIT | 2.0k | 2026-06 | 34 kB | **The most complete period reconstruction.** Real menus, tabs, modals, tables, charts, statusbars, the full 16-colour DOS palette as utility classes. Also the most opinionated: you adopt the whole 1990 look. Re-themes cleanly to dark using its own palette classes. | | [terminal.css](https://github.com/Gioni06/terminal.css) | 0.7.5 | MIT | 1.4k | 2026-02 | 15 kB | **Not actually a TUI library** — a small, tasteful *document* theme that happens to be green-on-black, with a blinking prompt cursor built in. Right for a blog or a docs page; wrong for an interface. | | [hack.css](https://github.com/egoist/hack) | — | MIT | — | — | — | Not vendored. Overlaps terminal.css and is less maintained; nothing it does is unavailable above. | **And the other half of the strand: things that render an actual terminal.** Everything above is a *theme*. These four are not, and specimens 21 and 23 run them against one real recorded session and one real prompt. | project | ver | licence | size | verdict | |---|---|---|---|---| | [xterm.js](https://github.com/xtermjs/xterm.js) | 5.5.0 | MIT | 289 kB | **A real terminal emulator** — the one to reach for when it must accept input: a shell, a serial console, a container exec. It knows nothing about playback, so any replay scheduler is yours to write. | | [asciinema-player](https://github.com/asciinema/asciinema-player) | 3.8.1 | Apache-2.0 | 187 kB | **For showing a recorded session, and nothing else.** You get the clock, the scrubber and the controls; you give up control of the rendering. | | [ansi_up](https://github.com/drudru/ansi_up) | 6.0.2 | MIT | 23 kB | **A converter, not an emulator**, and the distinction is the whole finding. Beautiful ANSI-colour-to-HTML in 23 kB, no cursor concept at all — so it renders coloured logs perfectly and cannot render a full-screen TUI in any form. Ships ESM, not UMD. | | [jQuery Terminal](https://github.com/jcubic/jquery.terminal) | 2.44.1 | MIT | 176 kB | **A command interpreter with a REPL attached** — history, tab completion, argument parsing, its own colour markup. Not an emulator either, so no cursor addressing. The real cost is jQuery: 88 kB before you start. | **The line that matters is cursor addressing.** A program that only *appends* can be rendered by anything. A program that *moves the cursor* — which is every full-screen TUI there is — needs an emulator. That single question sorts this whole table, and specimen 21 makes it visible by feeding one recorded vim session to all three renderers at once. **Also relevant, not used:** [xterm.js](https://github.com/xtermjs/xterm.js) is the real answer when you need an *actual* terminal (a live shell, an SSH session) rather than the look of one. It is a different tool for a different job and would have made a dishonest specimen here. --- **Added in the seventh pass: 53-mixed-script**, which takes §11's non-Latin findings onto the character grid, where they get much worse. A terminal is a fixed matrix of cells and every rule it has about how many cells a character occupies is from 1991: wcwidth 1, wcwidth 2 (which only works if the font makes those glyphs exactly twice the Latin advance, and it does not), wcwidth 0 for combining marks — which means Thai, stacking up to three marks on one base, has no correct representation in the model at all. Not a bug in any terminal; a gap in the model. Finding 82, and the wider version of it: **the moment an interface draws a box out of characters it has made a promise it can only keep for one script**, which specimens 01, 02, 03, 21 and 23 all make. --- ## 2 · HUD / FUI Renamed in the sixth pass. This section used to be called "Cyberpunk / HUD / FUI" and the first word was doing no work: every specimen in it is an instrument that performs competence. The actual cyberpunk register — street level, multi-script, worn, oversold — is §14, and 24-image-decay moved there. | project | ver | licence | ★ | last push | size | verdict | |---|---|---|---|---|---|---| | [augmented-ui](https://github.com/propjockey/augmented-ui) | 2.0.0 | BSD-2 | 1.4k | 2024-05 | 163 kB | **Does one thing and is genuinely the best tool for it**: clipped, notched, scissored panel geometry with a border drawn along the polygon. All geometry, zero opinions about type or colour, so it layers over anything. Its corner sizes are custom properties, so with `@property` the *geometry itself* is animatable. 163 kB is the price of every corner permutation — subset it in production. Quiet for two years, but it is finished, not abandoned. | | [Arwes](https://github.com/arwes/arwes) | 1.0.0-next | MIT | 7.6k | 2026-07 | 56 kB (95 kB with deps) | **The most serious attempt at a real sci-fi design system**, and the only project here that treats *sequencing* as a first-class primitive. Usually presented as React, but `@arwes/frames` + `@arwes/animator` are framework-agnostic and drive fine from vanilla JS — specimen 06 uses eight frame presets with no React at all. Still alpha, the API moves, and it hands you primitives to build a design system rather than components to use. | | [LCARS](https://github.com/joernweissenborn/lcars) | main | MIT | 371 | 2021-01 | 103 kB | **Worth studying as the one FUI language that became a real design system.** Strict colour roles, a fixed radius grammar, rigid alignment on a 0.25 rem gutter. Ships the structural vocabulary (`lcars-row`/`column`/`bar`/`elbow`) plus the full Okuda palette. Unmaintained since 2021 and it does not matter — the spec it implements stopped changing in 1994. | **Reference, not code.** The genre's actual source material is film and game work: [HUDS+GUIS](https://www.hudsandguis.com/) is the standing archive, and Territory Studio (*Blade Runner 2049*, *Guardians of the Galaxy*) is the reference studio. Two things transfer directly and are visible in specimens 05–08: **numbers that nobody is meant to read** signal a machine thinking rather than data, and **sequencing beats styling** — the four-beat sweep → contact → lock → readout carries more than any amount of panel chrome. --- **Two libraries that are not FUI libraries at all, and carry the strand's two strongest specimens.** | project | ver | licence | size | verdict | |---|---|---|---|---| | [mos6502](https://www.npmjs.com/package/mos6502) | 1.1.1 | MIT | 26 kB | A real MOS 6502 CPU emulator with a real instruction decoder. Specimen 22 runs actual machine code through it so the T-800's scrolling assembly is genuinely executing — which is historically right, since the film used real 6502 listings. ⚠ Its `debug` stream desynchronises from execution (finding 27); use `decode()` against `getState().pc` instead. | | [glitch-canvas](https://github.com/snorpey/glitch-canvas) | 1.1.12 | MIT | 14 kB | **The only honest glitch library here.** It corrupts bytes inside the compressed JPEG and lets a real decoder produce the artefacts, rather than drawing a picture of damage. Specimen 24 puts it beside the usual CSS slice-and-offset and the two diverge in *kind*, not degree. | **Why the strand needed them.** The rest of §2 is clean sci-fi console — LCARS, Arwes' polished frames, notched panels. That is one register of this aesthetic and it is the politest one. Machine vision and codec corruption are the other: dirty, red, broken, and carried by content that means something rather than by frame geometry. --- ## 3 · Minimal / classless Specimen 09 puts byte-identical semantic HTML under all three. Demo pages are always cherry-picked to flatter their own framework; the same markup is the only fair test. | project | ver | licence | size | verdict | |---|---|---|---|---| | [Pico](https://github.com/picocss/pico) | 2.1.1 | MIT | 81 kB | **The most capable.** Real components from plain semantic HTML — dialogs, accordions, switches, dropdowns — plus a proper dark theme. The one to pick if the page has any interface in it. | | [Simple.css](https://github.com/kevquirk/simple.css) | 0.1.3 | MIT | 19 kB | **The most opinionated typographically**, and the most "traditional web" of the three. Auto dark theme from `prefers-color-scheme`. | | [Water.css](https://github.com/kognise/water.css) | 2.1.1 | MIT | 22 kB | **The smallest and quietest.** Auto light/dark, CSS-variable customisation, no components. Right for a README page or a prototype. | **The strand's real finding is specimen 10, and it is not about any of them.** Most of what teams still pull an animation library in for is now native, composited CSS: `animation-timeline: view()` for scroll-driven reveals with no observer and no rAF; `linear()` for genuine sampled spring easing; and `@starting-style` + `transition-behavior: allow-discrete` for animating in from `display: none`. Specimen 10 runs zero frames of JavaScript animation and reports live whether the browser has each feature. **Reach for a library when you need sequencing and coordination — not to fade something in on scroll.** --- ## 4 · Motion engines, shaders and effects ### Engines Specimen 11 authors the *identical* staggered grid animation three times so the only variable is the engine. | project | ver | licence | size | verdict | |---|---|---|---|---| | [GSAP](https://github.com/greensock/GSAP) | 3.15.0 | MIT | 71 kB | **Still wins for choreography, and is now fully free including every plugin.** The argument is not that it animates — it is that a twelve-step sequence becomes a single labelled object you can scrub, seek and reverse. Specimen 08 exposes exactly that: a live scrubber over a labelled timeline, with every readout derived from `tl.time()`. Nothing else here makes the sequence a value you can hold. | | [Motion](https://github.com/motiondivision/motion) | 13.1.1 | MIT | 136 kB | The React-ecosystem default, with a vanilla API. Its springs are real physics, so `duration` is a hint and settle time is emergent — the one visible difference between the three lanes, and a philosophical choice rather than a bug. Heaviest of the three. | | [anime.js](https://github.com/juliangarnier/anime) | 4.5.0 | MIT | 115 kB | v4 is a full rewrite with named ESM exports; the UMD build hangs them off an `anime` global rather than exposing one callable function as v3 did. Notably it now ships **built-in text utilities** (`splitText`, `scrambleText`), which overlap the text-effect libraries below. | Read the code panels in specimen 11, not the motion: the three look nearly identical on screen. **Choose on authoring ergonomics and bundle cost.** ### Text effects | project | ver | licence | size | verdict | |---|---|---|---|---| | [Splitting.js](https://github.com/shshaw/Splitting) | 1.1.0 | MIT | 4 kB | **Punches far above 4 kB.** Shards text into per-character spans and stamps `--char-index` on each. That index is the entire API; every effect is then one CSS rule using it as a negative delay. | | [Glitched Writer](https://github.com/thetarnav/glitched-writer) | 2.0.29 | MIT | 22 kB | A per-character state machine for scramble/decode. Unmaintained since 2021 but self-contained and it works. Note anime.js v4's `scrambleText` now covers the same ground if you already have anime. | Scramble-decode is **the most overused effect in this aesthetic and the easiest to do badly**. Specimen 12 puts four techniques side by side; the one that survives being used seriously is block redaction, not scramble. ### Shaders | project | ver | licence | size | verdict | |---|---|---|---|---| | [OGL](https://github.com/oframe/ogl) | 1.0.11 | MIT | 130 kB | **The right size for post-processing work.** three.js does everything; OGL does WebGL with the boilerplate removed and nothing else, and you keep writing actual GLSL. Its `Post` helper handles render-target bookkeeping in three lines. 130 kB against three.js' 635 kB. | | [@paper-design/shaders](https://github.com/paper-design/shaders) | 0.0.80 | Apache-2.0 | 199 kB | **The best-looking shader backgrounds available off the shelf**, and the defaults are tasteful. ~30 shaders exported as plain GLSL strings — that GLSL is the real asset and it is reusable on its own. See the caveat in Findings below before using the vanilla path. | | [three.js](https://github.com/mrdoob/three.js) | 0.185.1 | MIT | 635 kB | Vendored for comparison only. Correct when you need a scene graph, loaders and materials; overkill when you need one fullscreen quad. | **Nothing beat raw WebGL2 for specimen 13.** A CRT pass is one fullscreen triangle and one fragment shader; a library adds abstraction over roughly forty lines of setup and hides the mechanism that is the whole point. --- ## 5 · React component libraries The four libraries most of this aesthetic actually ships in, and the strand the first pass deliberately skipped. None of them is an npm package you import: all four are **copy-paste** libraries, so what you adopt is their source, and their source is therefore what specimen 19 runs — fetched from their real distribution points, compiled in the browser, unmodified. The governing finding is that **there is no engine comparison to make here.** All four import from `motion/react`. Where the vanilla strand has GSAP, Motion and anime.js genuinely competing (specimen 11), this strand settled that question already. What is left to choose on is licence, packaging and taste. | project | distribution | licence | verdict | |---|---|---|---| | [Magic UI](https://magicui.design/) | shadcn `registry-item.json` over HTTP | MIT | **The most professional packaging.** A real machine-readable registry, so `shadcn add` works and so does a script. Largest catalogue. The components are the most "finished" — closer to set pieces than to primitives. | | [Aceternity UI](https://ui.aceternity.com/) | shadcn registry, undocumented | **unstated** | **The most cinematic defaults, and the licence problem.** Its registry documents carry no SPDX field and the site publishes no licence. Treat as source-available for study; do not assume MIT before asking. | | [React Bits](https://reactbits.dev/) | raw files in the repo | **MIT + Commons Clause** | **The only one that works without Tailwind** — it ships a plain `.css` beside each component. The Commons Clause forbids *selling the software itself*, which is fine inside a product and fatal if you are building a component library on top of it. Read it before adopting. | | [motion-primitives](https://motion-primitives.com/) | raw files in the repo | MIT | **The smallest and most restrained.** Primitives to build with rather than finished set pieces. If you want a vocabulary rather than a catalogue, this is the one. | **The toolchain is the cost.** Running these honestly needs React, ReactDOM, Babel, Tailwind and Motion — ~3.5 MB before a single component renders. A real project pays that once at build time. It is still the reason the rest of this lab is vanilla: every other strand's specimen is the library and nothing else. --- ## 6 · Era GUI Faithful reconstructions of shipped operating systems. Nostalgia, but with real interaction rules underneath, and one properly useful finding about motion. | project | ver | licence | size | verdict | |---|---|---|---|---| | [98.css](https://github.com/jdan/98.css) | 0.1.20 | MIT | 24 kB | **The reference implementation of the genre.** Pixel-exact, tiny, and its class vocabulary became the de-facto standard the other two follow. | | [XP.css](https://github.com/botoxparty/XP.css) | 0.2.6 | MIT | 256 kB | Same vocabulary, later era. **91% of the file is 53 embedded SVGs** — pictures of its own chrome, measured in 56. | | [7.css](https://github.com/khang-nd/7.css) | 0.16.0 | MIT | 93 kB | Same vocabulary again, plus the only real motion here — 19 transitions and a pulsing focus ring. Its four opt-in classes are **Aero capabilities, not motion classes**; see finding 88. | | [system.css](https://github.com/sakofchit/system.css) | 0.1.11 | MIT | 20 kB | Mac System 6, and the smallest sheet here. Same *idea*, **different contract** — `.standard-dialog`, `.window-pane`, `.title`. Not drop-in with the Windows three, and it declares three of their names anyway. | | [NES.css](https://github.com/nostalgic-css/NES.css) | 2.3.0 | MIT | 289 kB | No window primitive at all, because a console had none. **Bigger than XP.css and only 3% imagery** — it draws 14,888 box-shadow pixels instead. Beautiful, and the least reusable thing here. | **Split into six specimens on 2026-09-03, and down to five on 2026-09-04.** This strand was one file for a month — 18 rendered a dialog under all five sheets and carried five findings at once. Each sheet now has its own specimen. 18 itself was **withdrawn** a day later, and the reason is layout rather than subject. Five panes side by side is the entire argument, because the swap test is a *comparison* and a comparison you have to scroll between is not one — and five panes do not fit. The stage is `repeat(auto-fit, minmax(250px, 1fr))` beside a rail, so below about 1650 px the fifth wraps onto a row of its own and the sheet you are meant to be comparing against is the one you cannot see. Two of the five also overflow their cells at that width: system.css's dialog measures 320×256 in a 301×461 frame and is clipped on the right, which turns a real finding about that sheet into something that reads as a rendering bug. Splitting the strand is what made it expendable. The five findings it used to carry are carried by 55–59 with room to measure properly, and the one question it kept — which sheets are actually interchangeable — is a five-row table rather than five screens. **That table is the second one below, and it reads better here.** The specimen, its page and its findings are untouched in the repository; `hidden: true` in the catalog is the only thing keeping it off the contact sheet, and deleting that line puts it back. | specimen | sheet | the finding | |---|---|---| | [55](examples/55-bevel/) | 98.css | the whole OS is four colours in four slots | | [56](examples/56-luna/) | XP.css | 91% of the file is pictures of its own chrome | | [57](examples/57-aero/) | 7.css | the motion, and the one kind no switch can stop | | [58](examples/58-onebit/) | system.css | three values a one-bit screen did not have | | [59](examples/59-cartridge/) | NES.css | 15,000 pixels drawn in the cascade | ### Which of them are actually interchangeable Specimen 18's finding, kept here now that the specimen is not on the sheet. One dialog, nine classes — `field-row`, `field-row-stacked`, `status-bar`, `status-bar-field`, `title-bar`, `title-bar-controls`, `title-bar-text`, `window`, `window-body` — and the count is each sheet's own class vocabulary taken off its parsed selectors, not how many elements it happens to style. That distinction is the whole measurement: counting matched elements scores all five at or near 100%, because every one of them styles `body` and most reset with `*`, and the page would cheerfully report that NES.css renders a Windows dialog perfectly. | sheet | of nine | verdict | |---|---|---| | 98.css | **9** | drop-in. The reference vocabulary; the other two follow its names. | | XP.css | **9** | drop-in, following 98.css. | | 7.css | **9** | drop-in, following 98.css. | | system.css | **3** | **not** drop-in, and the most misleading number here. | | NES.css | **0** | no window primitive at all. Its pane is close to bare HTML. | **The interesting number is system.css's 3, not the trio's 9.** It declares `.window`, `.title-bar` and `.field-row` and means something else by all three, so it *answers* a third of the markup rather than ignoring it — a title bar of its own design wrapped around a body it has no rule for, since `.window-body`, `.title-bar-text`, `.title-bar-controls` and both status-bar classes are absent. NES.css scores zero and looks obviously wrong; system.css looks half-built, and **half-built is what ships.** Partial name collision is worse than none, because none of it announces itself. **All six went dark on 2026-09-04**, with the rest of the lab — 18 included, on the same day it was withdrawn for the layout reasons above. That is the pass that produced findings 94–101, and the reason it produced so many is that turning a faithful reconstruction dark is the one operation its authors never had to consider — Windows 98, XP, 7 and System 6 did not ship a dark mode, so nothing in any of these sheets was built to survive one. `lab/relight.js` is the shared machinery: it walks each parsed stylesheet and emits an override, because **none of the five declares a custom property** (finding 94) and there is therefore nothing to set. Nothing in `vendor/` is edited and every count on every one of these pages is still read off the sheet as vendored, so the measurements are unaffected by the repaint. The split also **corrected three things this section used to say**, which is the argument for splitting rather than the cost of it. Each of the three was written from reading rather than from counting. - **"98.css, XP.css and 7.css accept byte-identical markup" was true and unquantified.** 18 now measures it as a class-vocabulary overlap and the answer is 9, 9, 9, 3, 0 against the dialog's nine classes. The interesting number is system.css's 3, not the trio's 9: it declares `.window`, `.title-bar` and `.field-row` and means something else by all three, so it *answers* a third of the markup rather than ignoring it. **Partial name collision is worse than none** — NES.css scores zero and looks obviously wrong, while system.css looks half-built, and half-built is what ships. - **"7.css is the only sheet shipping motion classes: `.animate`, `.glass`, `.has-shadow`, `.can-hover`" was wrong.** Read in the sheet, only `.animate` is motion; the others are translucency, depth and hover behaviour. What they share is that **every one is an Aero capability**, and Windows 7 made each conditional at runtime — a machine without a WDDM driver dropped to Aero Basic and lost the glass, the shadows, the animations and the hover previews together. The sheet reproduces a *capability negotiation*, which is a better thing than the note credited it with. - **"The motion vocabulary dates the compositor: none, then decoration, then structure" was a ramp, and the counts are not a ramp.** 57 measures 0/0/0 for both 98.css and system.css, **two animations for NES.css and both are `steps(1)`**, exactly one `@keyframes` for XP.css — the indeterminate progress bar, the single animation an OS cannot do without — and 19 transitions for 7.css. Four sheets, four different reasons, and only the last is about taste. --- ## 7 · Sound Almost absent from this space, and the one channel a screenshot hides completely. Three libraries, three different jobs, and they are not competitors. | project | ver | licence | size | verdict | |---|---|---|---|---| | [ZzFX](https://github.com/KilledByAPixel/ZzFX) | 1.3.2 | MIT | 10 kB | **The right default for interface sound.** A sound is a number array; there are no assets to host, cache or version. `ZzFXMicro` is 1 kB. | | [@arwes/bleeps](https://github.com/arwes/arwes) | 1.0.0-next | MIT | 5 kB | **The management model is the product, not the sounds.** Named bleeps, categories, per-category gain, one master, mute-on-blur. It is asset-based, so it has no sounds of its own. | | [Tone.js](https://github.com/Tonejs/Tone.js) | 15.1.22 | MIT | 338 kB | **Only when sound must be continuous and parametric** — a scan, a load, a threat level responding to state. Enormous for discrete bleeps, and the only option for anything else. | The composition is the finding: **synthesise with ZzFX, manage with Arwes.** Specimen 17 encodes ZzFX's samples as WAV data URLs and hands them to Arwes' manager, which gets both halves with zero binary assets in the repository. **The seventh pass put that verdict on trial** rather than repeating it. 49-nightside runs the same rain through both libraries and measures what it costs to *keep a sound going*, which is a different question from what it costs to download one and gets a different answer: for Tone intensity is a parameter and the node count does not move, for ZzFX intensity is an event rate and the main thread pays per drop. The verdict survives, with one correction that is not about either library — see finding 77, where the naive driver lost the argument on its own page. Two smaller confirmations: **Tone constructs an AudioContext on import too**, so the trap this section found in ZzFX is not specific to it, and **Tone needs no audio assets either** as long as you are willing to synthesise a street rather than record one. Three rules the shared layer enforces ([`lab/sound.js`](lab/sound.js)): default off, one control, and lazy — no `AudioContext` until a real click. The laziness is forced: importing `vendor/zzfx.js` constructs an `AudioContext` at module scope, so the *import itself* has to be deferred. --- ## 8 · Instrumentation | project | ver | licence | size | verdict | |---|---|---|---|---| | [uPlot](https://github.com/leeoniya/uPlot) | 1.6.32 | MIT | 50 kB | **The right chart library for this genre precisely because it has no animation layer to switch off.** It clears and repaints the whole canvas per update and is fast enough that this is a non-issue. | **The lab's one anti-motion finding.** Easing is interpolation, and interpolating measured data draws values nothing ever reported. On a marketing dashboard that is a nice touch; on an instrument someone makes a decision from it is a fabrication. Specimen 20 runs the same signal raw and through a spring side by side and names, in the units of the instrument, how far the eased lane is currently lying. --- ## 9 · Interaction / input The strand the lab did not have. Everything before it is something you watch; 23-shell was the only specimen that took a keystroke, and it was filed under Terminal because there was nowhere else to put it. It now lives here. | project | ver | licence | size | verdict | |---|---|---|---|---| | [Split.js](https://github.com/nathancahill/split) | 1.6.5 | MIT | 2 kB | **The smallest useful thing in `vendor/`.** Keeps siblings summing to their container while you drag the seam between them, and stops. No stylesheet — the gutter is invisible until you give it one, which is a feature at 2 kB. No keyboard *at all*: the gutter is a bare `
` with pointer handlers, so an entire layout is unreachable without a mouse. | | [WinBox](https://github.com/nextapps-de/winbox) | 0.2.82 | Apache-2.0 | 16 kB | A complete floating window manager with its chrome inlined in the bundle and no dependencies. Themed through constructor options and CSS custom properties. Note the licence — the only Apache-2.0 sheet in a mostly-MIT `vendor/`. It writes the frame colour as an *inline* style, so a CSS rule on its class loses; use the option. | | [interact.js](https://github.com/taye/interact.js) | 1.10.28 | MIT | 98 kB | **Gestures, and nothing about meaning.** dx and dy in pixels plus a composable modifier stack: restrict to a rectangle, snap to a grid, carry inertia after release. The right tool wherever *position is the data* — node graph, timeline, patch board. Pointer-first, and pointer-only. | | [NexusUI](https://nexus-js.github.io/ui/) | 2.1.5 | MIT | 166 kB | **Values, with the gesture hidden.** Finished instrument widgets that draw themselves and report a number in a range. Two warnings, both verified: 2.1.6 — the *current* release — publishes a minified build containing an unresolved git merge conflict and does not parse at all; and it never adopted `PointerEvent`, binding mouse and touch separately, so a dispatched `PointerEvent` leaves a dial completely inert. | | [tabbable](https://github.com/focus-trap/tabbable) | 6.5.0 | MIT | 30 kB | **The reference implementation of "what is focusable"**, and worth reading rather than reimplementing. `disabled`, `inert`, `visibility`, zero-size, a closed `
`, an `` with no `href` are all decided differently and all decided correctly. `tabbable()` returns tab order; `focusable()` is the larger set including everything at `tabindex="-1"`. | | [focus-trap](https://github.com/focus-trap/focus-trap) | 8.2.2 | MIT | 60 kB | Where focus is *allowed* to go. Its real value is the half people skip — `returnFocusOnDeactivate`. Its UMD build does **not** bundle tabbable; it expects the global, so the script tags cannot be swapped. | | [hotkeys-js](https://github.com/jaywcjlove/hotkeys-js) | 4.0.7 | MIT | 8 kB | Chords and scopes, so the same key can mean two things in two contexts and neither leaks. Earns its place on the default filter, which refuses to fire inside `input`/`select`/`textarea` — the bug in every hand-rolled key handler ever shipped. | **The findings this strand exists to produce.** *Conservation against freedom.* Specimen 25 runs tiling and floating over the same area and turns the trade into arithmetic. The tiled side reports the sum of its panes, which is 100 % no matter what you do to it because it cannot be anything else — tiling cannot lose a pane, because it cannot grow one without taking from another. The floating side reports how much of that same area is now *occluded*, sampled on a 48×30 grid rather than estimated, and that number only goes up. A window costs nothing to open and nothing to bury. *The cost of a gesture is a ratio, and it is measurable.* Specimen 26 records, for every control, how far the pointer travelled and how much the value moved. A dial is a small circle: it charges a lot of travel for a little value, and it charges more the smaller you draw it. A slider is a straight rail the length of its track and charges almost nothing. "Fiddly" is that ratio, and nobody designing a panel thinks of it as a number. *A roving tabindex is the difference between one keypress and twenty-seven.* Specimen 27's grid has 27 cells and exactly one tab stop; switch the roving off and the same grid costs 27 presses to get past. The overlay draws the live tab order over the page so the number can be watched collapsing. Every figure comes from asking tabbable, not from counting by hand. *A trap is a runtime behaviour, not a property of the DOM.* tabbable will happily report the whole document as reachable while focus-trap is holding you inside one box. Believing that number is how a modal you can tab out of ships. The readout re-roots itself on the trap's container when the trap engages, and says which root it is using. --- **Added in the seventh pass: 52-braindance**, and it is the gesture study this section has been missing since it opened. Drag, snap, focus and chords were all here; the *thresholds* were not — the two numbers every touch interface uses to decide, from one press, whether you meant a tap, a drag or a hold, which nobody writes down and no two products agree on. A braindance deck is the right host because all three gestures are live on the same pixels at once. Every press is recorded in (duration, distance) space and re-decided under Android's constants, iOS's, a slow hand's and a twitchy one's, so the claim is made out of the visitor's own input rather than asserted. Findings 81 and, in passing, the two rules nobody states: a hold has to fire on a timer rather than on release, and a drag has to stay a drag once it is one. --- ## 10 · Data surfaces Three subjects a study of information-dense UI had somehow never included, and one specimen moved in (20-telemetry) that had been filed under HUD because there was nowhere else for it. | project | ver | licence | size | verdict | |---|---|---|---|---| | [Tabulator](https://tabulator.info/) | 6.5.2 | MIT | 446 kB | **The interface people actually work in all day.** Arrives with virtual rendering, sorting, filtering, frozen columns, selection and a dozen themes. The kilobytes are the cheap part — its row is a `
`, so you have inherited a parallel implementation of something the browser already had, and every accessibility affordance a real `` gives you free has to be reasserted. | | [MapLibre GL](https://maplibre.org/) | 5.6.1 | BSD-3 | 939 kB | A complete vector map engine with a real camera. **Pinned behind latest on purpose:** 6.x is ESM-only and splits into a main module, a shared chunk and a separate worker wired by bare relative imports, which does not vendor as one file. Offline it cannot draw text at all — see finding 44. | | [wavesurfer.js](https://wavesurfer.xyz/) | 7.12.11 | BSD-3 | 43 kB | Small, current, and the plugin system is honest about being separate files. Renders into a shadow root, which is where its scroll container hides — a real accessibility consequence, and invisible from the light DOM. Will take precomputed `peaks` and never decode at all. | | [Natural Earth 110m](https://www.naturalearthdata.com/) | via world-atlas 2.0.2 | public domain | 108 kB | 177 country polygons as TopoJSON. The whole basemap of specimen 32, with no tile server anywhere. | | [topojson-client](https://github.com/topojson/topojson-client) | 3.1.0 | ISC | 7 kB | Unpacks the above. Does one thing. | **The finding this strand keeps producing.** Three specimens, three subjects, and the same idea underneath all of them, arrived at independently each time: | specimen | the drawing | the data | |---|---|---| | 20-telemetry | an eased line between samples | readings nothing ever reported | | 32-tactical-map | a range ring drawn on the glass | 851 km north, 553 km east, for the same nominal 800 | | 33-waveform | one min/max bar per column | 99.27 % of the samples, discarded before paint | | 31-table | about a hundred row elements | fifty thousand rows | Every one of them is a claim about data that is not the data, and in three of the four the claim changes with the scale. **This is the strongest cross-cutting result in the lab**, and none of the four specimens was built to produce it. --- ## 11 · Typography The strand that exposes what the lab had been assuming. Grep every specimen for `font-family` and the answer is `ui-monospace` and nothing else: the only other faces anywhere here — Antonio, Press Start 2P, MS Sans Serif, ET Book — arrived inside a vendored sheet and belong to it. Thirty-four specimens and not one typographic decision the lab had made for itself. | project | ver | licence | size | verdict | |---|---|---|---|---| | [Recursive](https://www.recursive.design/) | 1.085 | OFL-1.1 | 305 kB | **One file that is about twenty.** Five axes, and the two that matter are not weight: MONO runs proportional → monospaced as a real continuum, CASL changes the skeleton of a, g and l. An axis is an animatable property, so type can move between states with every frame a drawn instance rather than a synthetic smear. | | [IBM Plex](https://www.ibm.com/plex/) | 5.3.0 | OFL-1.1 | 61 kB | Drawn for exactly the register specimen 28 needed: an instrument in a lit room. Sans and Mono share metrics, which is the whole reason to reach for it over a prettier grotesk. | | [Noto Sans Arabic](https://fonts.google.com/noto) | 5.3.0 | OFL-1.1 | 49 kB | The RTL half of specimen 37. Cheap enough to vendor, which Japanese is not — and that asymmetry is itself the finding. | | [Paged.js](https://pagedjs.org/) | 0.4.3 | MIT | 922 kB | Listed in §3 with the minimal strand, because what it argues about is the page rather than the type. | **Two specimens, one finding, from opposite directions.** Drag MONO below 1 in specimen 35 and the box drawing, the column alignment and the numeric readouts come apart together. Put CJK in the same table in specimen 37 and they come apart in exactly the same way, for exactly the same reason: the advance widths stopped agreeing. **Monospace was never an aesthetic choice in this lab — it was a layout contract**, and the terminal and HUD strands depend on it silently. Measured on the machine this was written on: 1 `ch` is 7.83 px, a CJK glyph is 13.00 px, a 24 `ch` box holds about 23 Latin glyphs or about 14 CJK ones, and the box-drawing table is out by up to 12.6 px. Specimen 37 ships an Arabic face and deliberately no Japanese one. A real CJK webfont is megabytes, so the platform's face is inherited — **and that is the finding rather than a shortcut.** For most of the world's text you do not ship the font. Its metrics are not yours, they differ per machine, and any layout tuned to a vendored face is guessing. --- ## 12 · The light control, and what it left behind Not a strand. One specimen, 28-bench, and the most useful thing in the pass that added it. It was the lab's only light field for a month; on 2026-09-04 it was taken away, and this section is now the record rather than the room. Thirty-seven specimens at the time, and until 28-bench every single one of them sat in a dark frame. The era-GUI chrome was the only other light surface anywhere and it was a quotation, not a design. So the whole house grammar had been developed against one background and never once tested against another, and nobody knew which parts of it were **technique** and which were just the field being black. Most of it did not survive. | what | on black | on paper | |---|---|---| | grain | adds light to a dark field; reads as a surface | adds nothing, subtracts contrast; reads as a dirty screen | | glow | a colour plus a halo of itself; the element advances | the halo is darker than the page; the element **recedes** | | "dim means secondary" | demote by going darker, six usable steps | demote by going lighter, and `#767676` is the last grey clearing 4.5∶1 — about **two** steps | | a saturated accent | ordinary; teal means "this is a value" | reads as **alarm** whether you meant it or not | | structure | a slightly lighter panel on a darker ground | there is no "slightly lighter" — every division is a 1 px rule, and the layout has to be genuinely aligned because nothing hides a mistake | What did survive: weight, rule, alignment, the monospace numeric column, the motion switch, and specimen 20's refusal to interpolate measured data. **That list is the actual house style.** Everything else was the dark. ### Putting it back, which is a different experiment The field went dark again on 2026-09-04, and the interesting part is that the table above is not simply read backwards. Four of the five rows reversed and one did not, and the one that did not is the finding: - **Grain, glow, saturated colour and the six steps of secondary all became available again the instant the ground flipped.** 28-bench still refuses every one of them. The switch positions did not move; the REASONS underneath them did, from "this cannot work here" to "this register does not want it". Only the first of those is a fact about CSS. - **Hairlines did not reverse.** A dark field genuinely does offer "slightly lighter", and the specimen still declines it: every division is still a 1 px rule and the alignment still has to be real. Structure by hairline was the one thing paper forced that turned out to be worth keeping afterwards. Which makes 28-bench, now, the lab's evidence that the dark strands' bloom is a **house habit rather than a requirement of the field** — a claim the specimen could not have made while it was the only page not on black. **And one control reversed that nobody had listed as part of the grammar.** For a month 28-bench was the single page in the lab where the high-contrast switch moved *down* instead of up, because on paper you gain contrast by darkening. That is an accessibility control whose SIGN is a function of the ground, and it is not a thing the switch itself knows: `html[data-contrast="high"]` is a selector, and every specimen has to decide for itself which direction "high" points. Two specimens had to have that inverted by hand when the field moved. ## 13 · Full composites The strand added in the fifth pass, and the only one whose subject is not a library. Everything else in this survey studies one project in isolation, which is the right way to learn what a project is and a poor way to learn what it is like to *use* one, because nothing ships alone. These four specimens each put several libraries on one screen, with a real layout and more than one data surface, and each borrows its style from a different strand. | specimen | the room | what is in it | the question | |---|---|---|---| | 16-composite | dark, shader + HUD | shader field, augmented-ui, monospace instruments | how do TECHNIQUES layer? | | 38-ops-floor | dark; was the §12 control | Split.js, uPlot, Tabulator, canvas-gauges | what does composing a PRODUCT cost? | | 39-watch-floor | dark, HUD + CRT pass | MapLibre, Tabulator, uPlot, augmented-ui | what does the FINISH cost the surfaces under it? | | 40-workstation | 1998 desktop, relit | 98.css, interact.js, LiteGraph, Tabulator, uPlot | what does an ERA reach, and what does it miss? | | 41-house-style | dark, console + HUD | six React components, uPlot, Tabulator | what does a component library keep, and where? | Specimen 16 is the original and it answers a layering question: shader behind, interface authored at full contrast, finish applied last. The four added here answer four different ones, and they turned out to be the same question seen from four sides. ### Reachability is the property that decides everything Specimen 38 counts, at run time, the CSS rules it took to bring each library into one room. It was a light room until 2026-09-04 and the four numbers are unchanged, which is finding 93: | library | cost | why | |---|---|---| | Split.js | **4 rules** | ships no stylesheet at all. It inserts a bare div and expects you to style it. The cheapest library to compose is the one with no opinion. | | uPlot | **4 rules** | its sheet is structural; the palette is passed as options. The only one of the four that composes cleanly. | | Tabulator | **24 rules** | vendored as the `midnight` theme. On paper that was a full repaint rather than a tweak; in the dark it is an *alignment* rather than an inversion, because midnight paints its wrapper #222, its header #333, its rows #666 and its even rows #444 on #fff type, and this room is #131416 with #1a1b1e cards. Same count either way. Expensive, and entirely *possible*, because all of it is CSS. | | canvas-gauges | **0 rules, 43 constructor arguments** | was already at home on a light ground, and is completely unreachable from the cascade. | So the cost of composing a library is **not** proportional to how different it looks from your design. It is proportional to **how much of its look lives in CSS you can reach.** canvas-gauges is the one that already looked right and the one that hurt, because a canvas widget is invisible to the cascade — which means it is invisible to the lab's contrast switch, its motion switch, and any future theming, all of which are stylesheets. **On a composite surface every lab-wide behaviour has to be built twice**, once for the DOM and once for the canvases, or it silently applies to whichever half you happened to be looking at. Specimen 40 finds the same split from the far side. 98.css styles `button`, `input`, `select` and `table` by element name, so a library nested inside a 1998 window is not merely placed in the era, it is **converted** to it: Tabulator's header filter is a real `` and becomes a 1998 input without being asked. uPlot and LiteGraph draw into canvases, and 98.css matches **zero** elements inside either — each window's status bar prints the count, read from the era sheet's own rules at run time. Put the two together: > **Nothing inherits the right amount.** The DOM widget takes more than you > wanted and the canvas widgets take nothing at all, and no amount of care with > your own stylesheet changes either, because the split is not about your CSS. > It is about what the widget is made of. ### The third kind of unreachable Specimen 41 finishes the taxonomy. A library can keep its appearance in **reachable CSS** (Tabulator, Split.js — write a rule, win), or in **constructor arguments** a stylesheet can never touch (canvas-gauges). A copy-paste React component library is neither. Magic UI, Aceternity, motion-primitives and React Bits ship **markup**, with the design written as Tailwind utility classes inside a file you now own a copy of. There is no rule to override, because the rule is `.bg-zinc-500` and it belongs to the whole page. There is no argument to pass, because the class is a string literal. Counted from the vendored source at load: | | | |---|---| | distinct utility classes the six hard-code | **26** | | of those, named anywhere in the specimen's own sheet | **2** | | classes the specimen can pass in | 8 | | overlap between those two sets | **none** | And there are three sub-modes, each quoted with the line it is on: | mode | where | what you can do | |---|---|---| | **merged** | `cn("…", className)` — spotlight:12, text-shimmer:31, text-animate:418, border-trail:29 | your class genuinely *replaces* theirs, because `cn` is twMerge and resolves conflicts. Exists only because the author remembered. | | **hard-coded** | border-beam:69, border-trail:27 | a literal className with no prop threaded to it. A higher-specificity rule from your own sheet, or a fork. | | **assigned raw** | DecryptedText:368 — `className={parentClassName}` | both strings land, neither is removed, and stylesheet order decides. The worst of the three, because it looks like it worked. | The HOUSE STYLE switch in that specimen turns its overrides on, and every one of them has to aim at a **utility** rather than at a component, because a utility is all there is to aim at. `.border-transparent` alone is hard-coded independently by BorderBeam and BorderTrail, two components from two different libraries that share nothing else, so one rule moves both. **You cannot restyle a component; you can only restyle a class, everywhere it appears, including in components you have not read.** ### A copy-paste component can be incomplete, and fail silently The sharpest result in specimen 41 is not about styling. Aceternity's Spotlight is mounted over that specimen's chart and **cannot be seen**. Its own source opens `animate-spotlight … opacity-0`: it starts transparent and waits for a keyframe named `spotlight` to bring it up, and that keyframe is not in the file. It lives in a `tailwind.config` extension published separately in their documentation. Checked live on the page rather than asserted: **zero** stylesheets define a rule for `animate-spotlight`, computed `animation-name` is `none`, computed opacity is `0`. It compiles, it mounts, it produces DOM, and it shows nothing. The lab deliberately does not supply the missing keyframes. Writing them from memory would be inventing upstream's design and calling it theirs, which is the one thing this project does not do — and it would erase the finding. So the Spotlight stays invisible and the rail says why. That is the real cost of the copy-paste distribution model, and it is not the 3.5 MB of toolchain. It is that **the component is not the unit.** The unit is the component *plus* a config, *plus* a `cn` helper, *plus* a path alias, *plus* a Tailwind build — and only the first of those arrives when you copy the file. ### Two rendering models in one layout uPlot and Tabulator mutate the DOM; React owns its subtree and will throw that work away on its next render. Both are mounted in specimen 41 into a div React renders **empty** and never gives children to, constructed in an effect that runs once and destroyed in its teardown. Neither library knows the other model exists, so the seam is entirely the composite's to maintain — the same shape as specimen 40's resize bus, and the same answer: **the thing no library ships is the thing between libraries.** ### Where two sheets meet, the era loses, and it loses on specificity 98.css bids `input` — one level, the weakest selector it is possible to write, and the natural way to write an era reconstruction because in 1998 the element *was* the document. Tabulator bids `.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input`. So the field is split property by property: Tabulator wins background, colour and border, and 98.css wins `font-family` only because Tabulator's rule never mentions it. The result belongs to neither design. Reordering the `` tags does nothing. That was the first theory in this specimen and the page disproved it in one reload — order decides ties, and this is not a tie. The checkbox in the Device manager window is the only real fix, and it names its own price: restate the era's rule at the library's specificity, in your own sheet, and that element is **yours** from then on. `vendor/` is still not forked, which is this project's rule, but upstream has stopped reaching that field. **That is the actual cost of composing an era reconstruction with anything modern, and it is charged per element.** ### The finish is not uniform, so contrast is a function of position The strongest result in the strand, and the one that generalises furthest beyond this lab. `lab/grit.css` in its `crt` variant is three stacked layers, and two of them vary across the screen: the vignette is a radial gradient reaching `rgba(0,0,0,.74)` at the corners, and the scanlines darken every other pixel row by `.42` at `.55` layer opacity. Specimen 39 computes the consequence from those exact values rather than asserting it, for one colour pair — the log's own ink on the log's own panel: | where | ratio | |---|---| | the pair, as it renders | **10.12∶1** | | at the middle of the screen | 10.12∶1 | | on a scan line | **6.21∶1** | | in the bottom corner, under the vignette | **3.71∶1** — a fail | | the grain's black lift, separately | 8.60∶1 | No colour changed anywhere. Darkening both ink and panel by the same alpha does not preserve the ratio, because WCAG's `(L1+.05)/(L2+.05)` carries a flare term standing in for ambient light that does not scale with the content: slide both ends toward black and the constant dominates. The grain works the other way and costs less — screen-blending lifts the panel further than the ink, closing the pair from underneath. **And the first version of this panel was wrong, in the way it accuses others of being wrong.** It named its two colours as constants — `#a7c0cc` on `#0a1620` — and reported 9.63∶1. The colour actually behind those rows was `#666666`, which Tabulator's `midnight` theme paints onto `.tabulator-table` and which showed through rows this specimen had deliberately made transparent: a real ratio near **3.0∶1 before the finish did anything at all**. The specimen was quoting its own stylesheet in the section about tools that quote stylesheets. Both colours are now read off a live cell, with the background composited up the ancestor chain until something opaque stops it, and the table above is what that returns. **A measurement that names its own colours is a claim, not a measurement.** **Every automated audit in this lab is blind to all of it**, `contrast.html` included, because they sample a colour *pair* and the pair is exactly what does not change. This is the same shape as the finding in "Contrast is time-dependent": the tool measures the stylesheet, and the reader looks at the screen. ### A clip is a cut augmented-ui draws its corners with `clip-path`, and `clip-path` clips **descendants**. A rounded corner lets content sit near the edge; a 14 px diagonal removes a 14 px triangle of whatever is under it — map canvas, table header, the last digit of a right-aligned column. Every panel in specimen 39 reserves interior padding equal to its own corner, and reports both numbers. Corner geometry is a layout variable and belongs in the box model, which is a different claim from finding 23's rule about what corners may *mean*. ### The same library costs different amounts in different rooms Tabulator took 24 rules to bring onto paper in specimen 38 and **15** to bring into the dark in specimen 39. Same version, same theme, same vendored file. The library did not change; the room did. Worth remembering before treating "we already use X" as though it were a fixed cost. ### Two libraries cannot both own the window WinBox is vendored, was the obvious choice for specimen 40, and is deliberately unused. It ships drag, resize, focus, minimise, stacking **and its own chrome**, and 98.css *is* chrome — taking the behaviour while suppressing the appearance means fighting a bundle that writes its frame colour inline (specimen 25). So the split is behaviour from one library, appearance from another: interact.js drags and resizes, 98.css draws. **Keep the library whose job is invisible and drop the one whose job is visible** is the general form of that answer. And nobody ships the bus between them. interact.js resizes a box; it has no idea the box contains a chart that must be told its new size, a table that must re-measure, or a canvas whose backing store is in device pixels and not in CSS ones. Every window in specimen 40 declares an `onSize` and the drag handler calls it by hand. **No library in any of these stacks ships that wiring, and every composite needs it** — including the ordering problem underneath it, since Tabulator throws if `redraw()` is called before `tableBuilt` and the widgets on a composite surface do not finish mounting together. --- ## 14 · Cyberpunk The strand added in the sixth pass, and the reason is a correction rather than an expansion. §2 was called "Cyberpunk / HUD / FUI" and held nine specimens, and not one of them was cyberpunk. Clipped frames, an animation state machine, an LCARS grammar, a reticle, a machine-vision overlay, a bench analyser, degraded instrument states, round gauges. Every one is an instrument that **performs competence**. That is a real and coherent register and it is not this one. The genre's own phrase is the brief: *high tech, low life*. Street level, multi-script, worn, cheap, and owned by someone who is not you. Six specimens. | specimen | what it drives | the question | |---|---|---| | 24-image-decay | glitch-canvas | moved here from §2: real codec damage is the gritty register, not the competent one | | 42-signage | Fontworks OFL faces, Noto KR/Thai | what is vertical Asian signage actually made of? | | 43-cybercore | cybercore-css | what does the only framework named for this genre ship, and does its accessibility claim hold? | | 44-breach | a real Apache-2.0 solver | where does a hacking minigame's difficulty actually live? | | 45-street | no library | the ways the hardware and the owner lie | | 46-sold | no library | who the screen is for | | 47-analog | glsl-ntsc-video, crt-fx | is damage in the signal domain a different thing from damage drawn on top? | | 48-slant | glitched-writer, Rampart One | what does the sheared-slab grammar cost? | | 49-nightside | Tone.js, ZzFX | what does it cost to keep a sound going, as against to download one? | | 50-quickhack | no library | what happens when a control has a price? | | 51-scan | no library | once you have found six things, where do the six labels go? | | 54-translated | no library | what does an ellipsis remove, in a language that is not the one you laid out for? | Two more from the seventh pass live in other strands, because this lab files by subject rather than by costume: **52-braindance** is in §9, since the deck is the skin and the tap/drag/hold thresholds are the study, and **53-mixed-script** is in §1, since its subject is the character grid. **The seventh pass closed both gaps this section had listed as open.** 47 is the shader specimen — and it is not a better CRT, it is the other domain entirely: RGB encoded to a real composite waveform at the broadcast constants, the *waveform* damaged, and upstream's demodulator left to produce the artefacts (finding 73). 49 is the sound, and it does not repeat §7's verdict, it tries to break it — §7 weighs ZzFX against Tone by download, this weighs them by what it costs to keep a sound going, which goes the other way until the driver itself turns out to be the cost (finding 77). > The other four are one argument in four costumes, and it is the argument this > strand exists for. 48 asks what the genre's own grammar costs and finds a > focus ring that is absent rather than faint (75). 50 gives a control a price > and finds two greys that mean opposite things, with the cost measured in > keyboard reach (78). 51 asks where the labels go once there is a crowd, which > is the question reference art never has to answer (79). 54 asks what an > ellipsis takes, and finds the one truncation that reverses an instruction > instead of damaging it (83). **In every case the genre's version of the > interface is the one-frame version**, and everything found here is what > happens on the second frame. ### Vertical is a script property, not a rotation Every reference image in this genre is a wall of vertical signage and almost every western implementation is Latin text rotated ninety degrees with kana sprinkled on. The browser has had `writing-mode: vertical-rl` since 2016, and specimen 42 measures what using it properly costs: | | | |---|---| | Latin run, default (`mixed`) | 147px — lies down, and that is **correct** Japanese typesetting | | the same run, `text-orientation: upright` | 221px | | with `text-combine-upright: all` on the digits | **143px** — shorter than the default | That last row is tate-chu-yoko, what every real sign does with its floor numbers, and it is *smaller* than either alternative because two digits become one square. Punctuation is a position rather than a glyph: 「」turn, 、and 。 change corner, the long vowel rotates, none of it a font swap. And `ch` does not turn with the writing mode — it is still the advance of the digit zero, so every `ch`-sized rail in this lab would size a column's **thickness** while appearing to size its length. The best result there was found by looking at the render rather than by planning it. Vertical writing is a **CJK affordance**: `text-orientation: mixed` stands up characters whose Unicode vertical orientation is upright, which covers Han and Hangul and does not cover Thai. So the Thai signs lie on their side, correctly, and **a genre that treats "Asian signage" as one texture gets it wrong every time.** Japanese and Korean stack; Thai does not. On cost: §11 argues you do not ship CJK, and for body text it is right. Display type is the opposite case, because no platform fallback owns a 16×16 bitmap gothic. `scripts/vendor_jp.py` reads upstream's own `index.css` and takes only the unicode-range subsets covering the exact text rendered — **107 of 619 subsets, 891 kB against ~5.3 MB for the lot.** ### The only framework named for the genre, audited cybercore-css is not a technique like augmented-ui, Arwes or LCARS. It is a design **system**: 594 rules, 399 classes, 38 keyframes, 74kB of pure CSS, zero JavaScript, and a palette, type scale, spacing scale, layout utilities and thirty components. Specimen 43 adopts it whole, because that is the only honest way to study a system, which makes it §13's finding arriving from the CSS side. Its reduced-motion audit went wrong twice before it went right, and both wrong turns are worth keeping: 1. The cynical prediction was that a framework selling glitch and neon treats `prefers-reduced-motion` as a README line. 2. The correction was worse. A naive search found **one** reduce block containing `html{scroll-behavior:auto}`, and the conclusion nearly published was that the claim was cosmetic. The other fifteen were further down a 74kB minified file. Measured properly, walking the sheet through its `@layer` nesting: **16 reduce blocks, 28 of 44 animated selectors cancelled, 64%.** That is not a grade, it is an outlier — LCARS ships 3 keyframes and cancels none, the era sheets cancel none, and nothing else in `vendor/` cancels anything at all. And the 16 survivors are not a random remainder. Every set piece stops: spinners, skeletons, the noise layer, scanlines, the neon border, the datastream, the `animate-*` utilities, the base glitch. What keeps running is the small perpetual furniture — a pulsing status dot, a blinking terminal cursor, indeterminate progress, badge pulses, the typing effect, and the `--intense` and `--hover` glitch variants the base rule does not reach. **A spinner stops when the work finishes; a status dot pulses for as long as the page is open.** Its palette, incidentally, passes clean: all seven text colours clear 4.5:1 on its own background, worst case 4.71:1. Neon on near-black passes by construction, which is the one accessibility advantage this entire genre has. ### The difficulty is the drawing Specimen 44 is the code-matrix breach puzzle with the real Apache-2.0 solver compiled in the browser and run against the same matrix on screen, which turns an opinion into a comparison. The solver finds the optimal seven-step path, landing all three sequences, in single-digit milliseconds. The clock gives a person twenty seconds and most people lose. Every bit of that gap is manufactured by presentation, and the specimen names the three devices doing it: - **The constraint is invisible until you violate it.** The alternating row/column rule is the entire puzzle and nothing on screen states it; it is drawn as an affordance. Legal moves at any step: three to six out of thirty-six. The search space is tiny and does not feel tiny. - **The clock is the mechanic**, and it starts on the first pick rather than on load — a genuinely good decision, because it makes the pressure feel chosen. Turn it off and the same matrix stops being a puzzle. - **The buffer is a commitment device.** Every pick is irreversible and visible, so the cost of a mistake is displayed continuously. A progress bar pointed the other way. ### A composite of small honest things Specimens 45 and 46 are adjacent because they are the same finding twice. 45 is a ticket machine that has been outdoors for eight years. Burn-in from a layout retired in 2018, drawn as a layer that never redraws while the live UI does. A dead pixel column that crosses every element and belongs to none. A dead patch of digitiser, deliberately **not drawn**, because a fault you can see is not the fault this is about. A clock wrong by seven years and 3h47m, stated with total confidence. A certificate that expired in 2019 while the machine still takes cards. And an advertisement that cannot be dismissed, because its close button sits inside the dead zone. Meanwhile the status bar says SYSTEM NOMINAL, and the two numbers have no relationship, because **nothing in that device's model represents its own screen. Self-report is not telemetry.** §8 drew six ways an instrument lies about its *data*; an instrument can be taught to say STALE, and nothing here can be taught to say SCRATCHED. 46 is a listings app where the task is one tap and takes considerably more. Measured off real bounding boxes: **46.9% of the frame is sold against 35.9% editorial**, four of ten rows are paid placement, and the disclosure — present, accurate and honest on every one of them — is set at **8px against a 14px body**, because disclosing something and making it readable are different claims and only the first is regulated. The sort control sorts by price *underneath* the pinned rows. The filter works and *expires*. The countdown restarts at zero. The interstitial's close button is present, labelled, focusable and inert for three seconds. > In 45, two independent hardware faults combine into something > indistinguishable from malice and nobody intended it. In 46, six independently > defensible product decisions do the same, and nobody had to intend that > either. **A composite of small honest things is how hostile interfaces are > actually built** — which is why auditing them one control at a time finds > nothing, and why they are shipped one control at a time. --- ## Accessibility, measured [`audit.html`](audit.html) runs axe-core 4.10.2 over every specimen against WCAG 2.1 A/AA, mounted at 1280×800 and left running so the animations are genuinely in flight. **Before fixes:** 20/20 specimens failing, three rules. **After:** 16 of 54 reported clean, and **two rules left across the entire lab** — colour contrast, which is the aesthetic and is switchable, and one ARIA violation, which is irreducible and is explained below. That violation now appears in **five** specimens rather than one: 31-table owns it, and 38, 39, 40 and 41 each inherit it by composing the same table. A library's irreducible defect is inherited by every screen that includes it, which is obvious once written down and was not obvious while building. | what | count | verdict | |---|---|---| | `html-has-lang` | 20 → 0 | Every specimen omitted a language. Free to fix, and universal — worth checking first in any project of loose HTML files. | | `label` | 3 → 0 | Three range inputs without accessible names. Ordinary bugs. | | ARIA structure | 41 → **1** | Every one a real defect: `role="grid"` with no rows, `aria-valuetext` on a `button`, a focusable `separator` with no value, three scrollable regions no keyboard could reach — one of them hiding inside a shadow root. Findings 32–34 and 46. The survivor is finding 42. | | `color-contrast` | **661** | Not a bug. This is the aesthetic. | | **unjudged** | **919** | Not a result at all. See below. | **Read that "7 clean" with suspicion — and here is why the audit page now prints an `unjudged` column next to it.** axe cannot resolve an element's background through a semi-transparent overlay or a background image, and `lab/grit.css` lays exactly such an overlay across every specimen *by design*. Those elements come back as `incomplete`: neither passed nor failed, never judged. There are **919 of them**, and they cluster precisely in the specimens reported clean — 41-house-style now leads with 155, 39-watch-floor follows with 139, then 22-machine-vision (60), 01 (50), 05 (49), 06 (47), 16 (37). The two leaders are both composites, and that is the point made twice: the specimens that put the most content under the most finish are simultaneously the least legible and the least auditable. So "clean" here means *nothing axe could judge failed*, which is a much weaker claim than it looks, and the honest total is 661 known failures on top of 919 unknowns. A tool that reported only violations would have understated this while looking rigorous. That is the failure mode worth naming: **the grain overlay that makes this aesthetic work is also what makes it unauditable.** Dim-on-dark is *how this genre says "secondary"* — a HUD readout, an inactive terminal label, LCARS numeric filler. Take the dimming away and the hierarchy goes with it. So [`lab/contrast.css`](lab/contrast.css) makes it a switch rather than a redesign, on the same pattern as sound: default off, one control, remembered, and honouring `prefers-contrast: more`. The switch is measured, not asserted: **661 → 2 → 0** across 54 specimens. (`audit.html` counted 582 rather than 580 on its own run; the lab is animated and the two tools sampled it at different moments, which is the section below in miniature.) Two survive the switch, both in 10-quiet-motion, and both disappear once the specimen has finished arriving. That last number needs the qualification below. ### The claim this section used to make, and why it was wrong For three passes this file said the survivors were greys inside vendored sheets, fixable only by forking `vendor/`. That was an assumption dressed as a finding. `audit.html` could *count* failures but never said whose colour they were, and nobody had asked. [`contrast.html`](contrast.html) asks. It throws the switch, runs the same axe rule, and then for every element that still fails it walks the frame's real stylesheets — recursing through `@layer`, `@media` and `@supports`, because WebTUI puts its entire sheet inside a layer — and reports which rule actually sets the colour. Of the 83 that survived at 24 specimens: | owner | nodes | | |---|---|---| | the lab's own specimens, inline `` makes `@tailwindcss/browser` try to resolve that import over the network before falling back to its bundled copy. Utilities generate identically with the tag omitted, and the console stays clean — which matters more in a lab than in an app, where a stray 404 reads as a broken specimen. 20. **An auditing tool must cache-bust its own iframes.** `audit.html` served frames from the HTTP cache and cheerfully reported failures that had been fixed minutes earlier. Any tool that mounts the thing it measures needs to be sure it is measuring the current version. 21. **Stopping motion means stopping three different things.** CSS handles the declarative half; WAAPI animations are objects with their own `pause()`, which is better than CSS can manage because they resume where they stopped; and rAF loops can be reached without touching a single specimen by wrapping the page's own `requestAnimationFrame` and *holding* callbacks rather than dropping them — a loop that recurses through rAF simply stops advancing, and nothing needs restarting. (`lab/motion.js`) 22. **Use a near-zero duration, never `animation: none`.** `0.001ms` still fires `animationend` and `transitionend`, so a specimen whose sequencing is chained on those events completes instead of hanging half-built. `none` deadlocks exactly the specimens that most need the reduced-motion path to work. 23. **augmented-ui offers every corner permutation, and that is a trap.** Specimen 05 originally gave each of its eight panels a different one — `tl-clip br-clip`, `tr-clip bl-clip`, `tl-2-clip-y br-clip`, `tl-clip tr-2-clip-x br-clip bl-clip` — and the result reads as wild rather than as designed, because nothing the shapes do correlates with anything the panels are. The fix is a rule you can state in one line: **corner treatment says WHERE a panel sits, never which panel it is.** Body panels clip one diagonal, always the same one; an edge bar clips the two corners on the edge it holds; the viewport, being the single hero, clips all four. One size token, and no `-2-clip-x/y` double notches anywhere — those read as damage. The library will happily let you skip this; nothing else will. 24. **axe reports `incomplete`, not a violation, for anything it cannot resolve a background for** — a semi-transparent overlay or a background image is enough. `lab/grit.css` overlays every specimen, so a large share of this lab's text is unjudged rather than passed, and the specimens that look cleanest are the ones with the most unjudged elements. Always read `incomplete` alongside `violations`; a tool that hides it understates the problem while looking thorough. 25. **Audit your own chrome.** The lab's sound, motion and contrast toggles are injected only on standalone pages, so they never appeared inside the audit's own iframes and were never checked. They were also, at ~3.0∶1, the least legible elements in the whole project. 26. **A debug stream can desynchronise from the execution it describes.** `mos6502` takes a `debug` flag and reports disassembly and registers from `emulate()`. Its execution is correct — the same program run with debug on and off gives identical results and the program counter never leaves `$8000–$801E` — but the debug stream periodically reports instructions at `$801E`/`$801F`, memory the CPU never visits. Verify a debug facility against the thing it claims to describe before you build a display on it; specimen 22 disassembles from `decode()` against `getState().pc` instead, which is still the library's own API, just the reliable half. 27. **Record a terminal under a PTY, or you are recording a different program.** A program that detects a pipe turns colour off and stops addressing the cursor — which is exactly what a terminal comparison needs to see. The window size has to be set on the master side too (`TIOCSWINSZ`), or a full-screen program draws to a size the player does not share. 28. **SIGKILL the recording, do not SIGTERM it.** Handed SIGTERM, a full-screen program tidies up after itself: leaves the alternate buffer, restores the screen, and the recording ends on a blank terminal. That is correct behaviour and a useless recording — the last frame is what a still screenshot of the specimen shows, so specimen 21's two *correct* renderers displayed nothing while the broken one displayed garbage. 29. **`ansi_up` publishes an ES module, not a UMD bundle.** Loading its dist file with a plain `