Design system
Derived from two frames of the Corruption film: the binary title card, and HAC's desk after the attack. Cold desaturated teal-grey throughout, with the green corruption and small amber highlights as the only saturated colour. Not clean sci-fi — a room that was lived in, then attacked, then abandoned, and the machines are still on.
Every colour resolves through tailwind.config.ts to a custom property in app/globals.css. No component carries a raw hex.
Colour
Stored as RGB channels rather than hex so opacity modifiers work — bg-corrupt/12 is needed constantly for glows and washes.
--void #05070A rgb(5 7 10)
page ground. Not pure black: pure black kills bloom.
--panel #0C1316 rgb(12 19 22)
raised surfaces, cards
--env #223438 rgb(34 52 56)
the desaturated teal of the room
--phosphor #DFEAE6 rgb(223 234 230)
primary text; the white of the title card
--phosphor-dim #7C8F8B rgb(124 143 139)
secondary text; the AA floor, never faded
--corrupt #4FFF7B rgb(79 255 123)
the green flame. Attention only, never body text.
--corrupt-dim #1F3D2B rgb(31 61 43)
corrupted surfaces, dead glow
--corrupt-text #9DFFBC rgb(157 255 188)
the one green that IS legible as running text
--amber #FFB74A rgb(255 183 74)
the old CRT. Secondary terminals, warnings, Recovered.
--alert #FF2E3C rgb(255 46 60)
the glitched red subtitle. Errors, locked states, Maxi.
--rust #8A6A3C rgb(138 106 60)
decay, wood panelling, the physical world
--unknown #55635F rgb(85 99 95)
no confidence. Headings and chrome only — see above.
Type
JetBrains Mono is the default face. Inter is loaded for one job: long-form prose, where monospace hurts sustained reading. Two scales — a UI scale, and a term-* scale with a single 1.45 line-height so terminal output aligns row-for-row with ASCII beside it.
Display 4xl — the Corruption
Heading 2xl — the Kami Protocol
term-xl — hac/os:/home/hac$ ls
term-lg — hac/os:/home/hac$ ls
term — hac/os:/home/hac$ ls
term-sm — hac/os:/home/hac$ ls
term-xs — hac/os:/home/hac$ ls
Prose face, Inter, 1.7 leading, same 96ch site-shell as every other section. This is what /lore and /story bodies use. Nothing here glitches, glows or scans; the frame is themed and the paragraph is left alone, because these pages have to survive being read for ten minutes.
Tabular numerals: 0123456789 / 1000 1111 8888 — figures must not jitter as they change.
Canon status
The most important part of the system. Six statuses, six treatments, applied by lib/canon/status.ts and nowhere else — a writer changing canon_status in Postgres changes the presentation, and nodes are never art-directed individually.
canonListingTag() → [✓ confirmed]Established. Multiple sources agree, or it happened on chain.
Heading
The last entry in HAC's journal
Body
The base is gone. I got out. I did not get out clean, and I am writing this down because I have stopped trusting that I will remember it.
canonListingTag() → [◈ witnessed]One account, from someone who was there. Honest but subjective.
Heading
The last entry in HAC's journal
Body
The base is gone. I got out. I did not get out clean, and I am writing this down because I have stopped trusting that I will remember it.
canonListingTag() → [◊ recovered]Pulled back out of damaged storage. Reassembled, and it shows.
Heading
The last entry in HAC's journal
Body
The base is gone. I got out. I did not get out clean, and I am writing this down because I have stopped trusting that I will remember it.
canonListingTag() → [▓ corrupted]Touched by the virus. Unreliable, not absent — still readable, and still worth reading.
Heading
The last entry in HAC's journal
Body
The base is gone. I got out. I did not get out clean, and I am writing this down because I have stopped trusting that I will remember it.
canonListingTag() → [≠ disputed]Forked chains preserve incompatible versions of this. Both are on record.
Heading
The last entry in HAC's journal
Body
The base is gone. I got out. I did not get out clean, and I am writing this down because I have stopped trusting that I will remember it.
canonListingTag() → [? unknown]Nobody has established this. The gap is the information.
Heading
The last entry in HAC's journal
Body
The base is gone. I got out. I did not get out clean, and I am writing this down because I have stopped trusting that I will remember it.
Where the brief and the accessibility rules disagreed
Unknown is given its own dim token because low contrast is the point. Against the void that lands at 3.25:1 — enough for large text, not enough for body copy, and accessibility was declared non-negotiable. So the token is used for headings and chips only, and Unknown body copy falls back to --phosphor-dim at 5.9:1. Compare the two samples above: the feeling survives where it is seen, the contrast floor holds where it is read.
Corrupted also has a hard rule from the canon, encoded in CorruptedText: at most two characters at a time, never whitespace, never a whole line, and never a structural character in ASCII art. The virus makes HAC's records unreliable, not absent — a corrupted file is playable, a missing file is just a locked door.
ASCII
Art is data. Every piece is a .txt file under content/ascii/ rendered by AsciiArt, which takes a source, a canon status and a size. It degrades by status exactly like text, it is aria-hidden with a required text alternative, and its height is reserved before paint.
Density ramp — sparse to dense
Confirmed — steady
Corrupted
Silhouette intact, cells eaten inside it. Structural characters are never substituted, because losing them turns art into noise.
Recovered — amber, reassembles once
Game of Life
A real simulation, on the homepage hero and nowhere else: a 52×13 wrapped torus stepping at 170ms, seeded with two gliders and a block. Paused when the tab is hidden and when it scrolls out of view. This is canon rather than decoration — the Cyfurs emerged from cellular-automaton physics and the HAC Key bears the Glider.
Wordmark
CYFURS as a grid of bits — the film's title card. Each 5x5 glyph pixel is two bits wide and every row is doubled, so the letters read as blocks. Bits reflip about every 1.4s. The real word is an h1; the art is aria-hidden over it.
CYFURS
design system — all states
Component states
Buttons, panels, inputs and the CRT frame. Every interactive element has a visible focus ring; tab through this section.
Panel
The default container. One border, one wash, no radius.
Withheld
Used for locked records and status notices. Amber, never red: this is not an error.
Terminal input
CRT frame — /os only
Bezel, curvature, vignette, 3px scanline pitch and a slow flicker, all beneath the text. Scoped to one route: framing the whole site wrecks mobile and makes long-form reading hostile.
hac/os:/home/hac/journal$ ls last-entry.txt hac/os:/home/hac/journal$ cat last-entry.txt JOURNAL — entry unnumbered. Timestamp corrupt.
RGB channel split — headings and status lines only
the corruption has already happened
Bloom — layered text-shadow, never filter: blur()
CYFURS
CORRUPTED
Motion and accessibility
prefers-reduced-motion: reduce disables all of it — scanlines, flicker, glitch, type-on, bit-flip and the simulation. Off, not reduced. The site must be completely usable with every effect disabled.
- · Body text is never glitched, never below 16px, never below 4.5:1.
- · Every animation is decorative and aria-hidden; screen readers get clean text.
- · Type-on renders the full string in the DOM and reveals it with a CSS clip — never by building it character-by-character in state, which hides it from crawlers and assistive tech and reflows on every tick.
- · Nothing here shifts the layout: animated blocks reserve their exact height.
- · Verify with DevTools → Rendering → Emulate CSS prefers-reduced-motion, or by turning on Reduce Motion in the OS.