Library · Primitives
Controls & surfaces
The whole vocabulary, with the class string printed beside each piece. Everything here is server-rendered: no control on this page needs client JavaScript to look right. Copy the string, keep the tokens, and the primitive lands correctly in any repo running the same four variables.
Action
Buttons
one primary per view
primary aka-button secondary aka-button-secondary quiet text-13 font-light text-muted-foreground/75 transition-colors hover:text-foreground
Sizes
py-1.5 / py-2.5 / py-3
sm rounded-md px-3 py-1.5 text-12 md rounded-lg px-3.5 py-2 text-13 ← default, what aka-button is lg rounded-lg px-5 py-3 text-15
Links
quiet where the ink around it is already muted (bylines, captions, lists); ink inside running prose
quiet aka-quiet-link accent text-primary underline decoration-border underline-offset-[3px] nav text-muted-foreground transition-colors hover:text-foreground ink aka-ink-link ← full ink at rest, the select accent on hover
Surface
Surfaces
one material, lit from above or reversed. no drop shadow anywhere
Raised. The fill grades light-to-dark downward and the top edge is lifted, so the light reads as coming from above the page.
Well
The same material with the grade and the top edge inverted, which is what a cut into the page looks like. Callouts, closing notes, code, media.
Lift
Hover this one. It moves 2px and its edge sharpens; it does not get brighter, because law 04 applies to a card as much as to a canvas.
card aka-card ← raised: sits on the page well aka-card-well ← recessed: cut into the page media aka-card-well aka-card-media ← a well with the grain off head aka-card-head ← the label band at the top of a card rule aka-card-rule ← a hairline inside one lift aka-card aka-card-lift ← a card that is also a control
Overlay
a menu panel: the ground, translucent, over a blur; lit at the top like a card
panel aka-overlay rounded-lg p-1.5 group px-3 pb-1 pt-2 text-10 font-medium uppercase tracking-[0.14em] text-muted-foreground/75 item block rounded-md px-3 py-2 text-13 transition-colors hover:bg-muted/40
Hint
hover or focus the button; the second copy is the same span, pinned open
host aka-hint-host hint aka-hint aka-card px-2 py-1 text-11 font-light end aka-hint-end ← anchored to the trailing edge, for a control at the page edge
Scroll-linked deck
one custom property, no state, no re-render
--flow The deck position, 0…n-1. The only thing JavaScript writes per frame. --flow-cover The centred cover's width. 62vw on a phone, 400px from lg. Everything else is derived from it. --flow-near How far the first off-centre cover sits, as a fraction of the cover, so the deck always overlaps by the same amount. --flow-far The extra fan per card beyond the first, so each edge reads as a deck of many rather than a deck of one. --flow-depth How far back an off-centre cover is pushed. preserve-3d sorts them by it, so nothing computes a z-index. --flow-turn How far it rotates away. 36° on a phone, 48° from lg: the same deck, flattened. --flow-step How much page scroll advances the deck by one cover. The first and last third of each step holds a cover landed, so stopping between two of them is something you have to aim for.
Carousel
one slide at a time; the page's own scroll drives it, the buttons step it
A slide is a figure the width of the track.
Its caption sits under it in the caption ink.
The counter and the buttons say where you are.
<Carousel label="…">
<CarouselSlide index={1} total={3} caption="…">…</CarouselSlide>
</Carousel>
track aka-carousel-track scroll-snap-type: x mandatory, scrollbar hidden
slide aka-carousel-slide flex: 0 0 100%, scroll-snap-align: startSegmented control
icons that share no silhouette, a glass end each for the selected one
group aka-card inline-flex items-center gap-0.5 rounded-lg p-0.5 button grid h-7 w-7 place-items-center rounded-md transition-colors on 1st aka-glass text-background on 2nd aka-glass aka-glass-rose text-background off text-muted-foreground hover:text-foreground a11y role=group + aria-label, aria-pressed per button, tooltip per button
Chips & tags
TagRow / KickerTags · uppercase, tracked, never colored by category
chip rounded-md border border-border/60 px-1.5 py-0.5 text-10 font-medium uppercase tracking-[0.12em] text-muted-foreground/75 tech aka-card-well inline-flex items-center rounded-md px-2 py-0.5 text-11 font-medium uppercase tracking-[0.14em] text-muted-foreground
Status
the one place a hue other than the accent may appear
neutral border-border/60 text-muted-foreground/75 active text-primary warn text-status-warn danger text-status-danger
Card
Project plate
the mark at the size where it is the thing you see
Live on /demo and the landing grid. Server-rendered; the hover is CSS.
.aka-plate grid aspect-[4/3] w-full place-items-center overflow-hidden rounded-xl
background-color: color-mix(in srgb, var(--plate-tint) var(--plate-mix), var(--stamp-ground))
mark relative block aspect-square h-[62%] overflow-hidden rounded-[16%]
.aka-mark-ground a dark ground for art drawn light on nothing
hover .group:hover .aka-plate { transform: translateY(-3px) }
This replaced a card that surrounded a 26px mark with a border, a description,
four tags and an arrow, which made the cleanest element on it also the smallest.
The chrome went and the mark took the space.
The plate is landscape and the mark inside it is square, because every mark is.
The inset is sized off the plate's height for that reason: taking it off the
width would grow the mark by a third the moment the plate stopped being square.
Art that carries its own ground fills the inset frame; a bare glyph shows the
tint through it; a screenshot is the one exception and takes the whole plate,
because cropping a picture of the work into a small square in the middle of a
large plate reads as a thumbnail of a thumbnail.Reading ink
the two steps that carry long-form text
Body ink. What an essay is set in.
Quiet ink. A caption, a date, an aside.
.aka-ink-body color-mix(in srgb, var(--foreground) 82%, transparent) .aka-ink-quiet color-mix(in srgb, var(--foreground) 62%, transparent) Classes, not utilities: an essay names its ink once instead of carrying a number through every paragraph. See foundations.
Reveal
scroll-driven, no JavaScript
Live on the landing's writing and music lists.
.aka-rise animation-timeline: view(); animation-range: …; animation-duration: auto Longhands only. The `animation` shorthand resets animation-timeline to auto and the reveal silently becomes a one-shot on load. This replaced an IntersectionObserver that existed to fade in one paragraph and forced the whole landing section, every plate and every link, to be a client component in order to hold its one boolean.
Input
Text fields
label above, hint below, never placeholder-as-label
We reply within 48h.
Message
Search
label text-10 font-medium uppercase tracking-[0.14em] text-muted-foreground/75 input w-full rounded-lg border border-border bg-background px-3 py-2.5 text-13 text-foreground placeholder:text-muted-foreground/75 focus:border-foreground/30 focus:outline-none hint mt-1.5 text-11 font-light text-muted-foreground/75
Choice
square = many, round = one
box h-4 w-4 rounded border border-border bg-background checked h-4 w-4 rounded border border-foreground/40 bg-foreground text-background radio h-4 w-4 rounded-full border border-border toggle h-5 w-9 rounded-full border border-border bg-muted/40 → bg-foreground when on
Tabs
active carries the accent; the rest stay quiet
active text-primary inactive text-muted-foreground/75 hover:text-foreground wrapper flex flex-wrap items-center gap-x-1 gap-y-1 -ml-2.5
Content
Kicker
uppercase label, 11px, tracked 0.18em, muted ink at 75%, the floor
Section kicker
aka-kicker
Label
the label inside a card head or above a table, 10px, tracked 0.14em, muted ink at 75%, the floor
Card label
aka-label
Section title
20px light, tight; mt-2 is the step under a kicker
The section title
mt-2 aka-section-title
Standfirst
one sentence under the title, saying what the section argues
A standfirst that says what this section argues, in one sentence.
aka-standfirst
Lead
the h3 that opens a write-up block
Why keep this page
aka-lead
Prose
15px light on the muted ink, 1.6 leading, paragraphs 2.5rem apart
Body copy is sans and light, and the space between paragraphs is generous enough that each one reads as its own thought rather than the next line of the last.
The muted ink is the default. Full ink is for the terms a reader will look for again.
aka-prose
List
the disc list; markers in muted ink at 75%, spacing stays a utility
- Watch the work first.
- Prototype in code.
- Ship the smallest true thing.
aka-list space-y-2
Code
inline chips and blocks share the muted ground
Run pnpm dev and open the app.
npx shadcn@latest add https://www.hitlkit.dev/r/hitl-card.json → writes components/hitl/hitl-card.tsx
inline aka-code block aka-card-well overflow-x-auto rounded-lg p-4 font-mono text-11 leading-relaxed text-foreground/80
Lists
quiet dot markers for prose; the disc list is aka-list, for reference lists
- Watch the work first. research before design
- Prototype in code. working surfaces over mockups
quiet li flex gap-3 → <span className="mt-[7px] h-1 w-1 rounded-full bg-muted-foreground/40" /> disc aka-list space-y-2 lead <span className="text-foreground/85">Term.</span> then body
Table
rules only between rows; no zebra, no vertical rules
| token | value |
|---|---|
| --radius | 0.625rem |
| --dur-step | 280ms |
head border-b border-border + label class row border-b border-border/40 cell py-2 pr-4 align-top text-12 font-light
Media frame
every screenshot gets the same frame + caption
A caption states what the frame shows, not that it is a screenshot.
frame aka-card-well aka-card-media overflow-hidden rounded-lg caption mt-1.5 text-11 font-light text-muted-foreground/75
Breakout
a block that takes the site's width from inside the reading column
Prose keeps the column. A gallery, a card row or a six-up swatch ramp steps out to the width the project plates already use, and back in on a phone.
block aka-breakout width min(100vw - 3rem, 1180px), the same 1180px as max-w-site margin calc((100% - width) / 2) on both sides, negative on its own
Section header
the page rhythm: kicker, title, standfirst, stacked
Section kicker
The section title
A standfirst that says what this section argues, in one sentence.
kicker aka-kicker title mt-2 aka-section-title stand aka-standfirst
Components
Demo image
a screenshot on a write-up: intrinsic size, a build-time blur, quality 90

<DemoImage src="/blockpad/blockpad-hero.webp" alt="…" width={1600} height={1003} />Demo video
a screen recording: controls, the poster until play, nothing fetched before it
<DemoVideo src="/ubik/search" poster="/ubik/search-poster.webp" width={1280} height={952} label="…" />Plate video
a clip inside a plate: autoplay and muted, one still under reduced motion, no hooks

<PlateVideo src="/wrdef/wrdef-card.mp4" poster="/wrdef/wrdef-card-poster.webp" width={900} height={640} label="…" />Loop video
a recording that plays itself and cannot be stopped: no controls, one still under reduced motion, no hooks

<LoopVideo src="/ubik/marketing/home-hero" poster="/ubik/marketing/home-hero-poster.webp" width={1280} height={662} label="…" />Plain summary
the plain-language answer at the top of a write-up; the disclosure is a native details
In simple terms
Ubik Studio was a desktop program that worked like a research assistant: the AI did the reading and the drafting, and a person stayed in charge of every judgment call.
You pointed it at an ordinary folder of PDFs and papers on your own computer. It read and indexed them locally, searched across dozens at once, and pulled out what mattered. The rule it enforced was simple: the AI could not state a fact or write a sentence unless it could attach a direct quote and an exact page number from your own files.
When something needed a human decision, the agent stopped mid-task and said so. You got a review queue and approved, rejected or edited each claim before it reached the draft.
The impact. Most AI writing tools of the era tried to replace the researcher and shipped a wall of text nobody could check. Ubik did the opposite: it took the tedious half of the work and made the person verifying it the point of the product, which is the pattern the industry has spent the years since rebuilding.
Why this was ahead of its time in 2023
In 2023 the state of the art in public was a single chatbot in a text box. It hallucinated freely, could not cite anything reliably, and left you copying snippets back and forth by hand. Ubik was already doing five things the field would spend the next several years arriving at.
Multiple specialised agents, not one general chatbot
Then you talked to one model that tried to do every part of the job at once.
Ubik split the work across sub-agents, a PDF reader, a literature researcher and a drafting agent, and let you assign a different model and reasoning depth to each. A cheap fast model ingested text; an expensive one wrote.
Citations enforced in code, not requested in a prompt
Then early chat-with-your-PDF tools were notorious for inventing quotes and giving page references that did not exist.
Ubik made it a programmatic rule. No exact quote and page number meant the claim could not be written at all, and every drafted line stayed linked to the highlighted source.
Human-in-the-loop as architecture rather than a confirmation dialog
Then you got either full automation or a tedious one-prompt-at-a-time conversation.
Ubik had a dedicated review queue and a Human Needed status. The agent paused at judgment calls, queued the evidence, and waited to be approved, rejected or corrected in batches.
Local-first, on your own machine
Then nearly everything required uploading confidential research to somebody else’s cloud.
Ubik ran on the desktop against your real file system. Pointing it at an existing folder indexed it in place, with no proprietary silo to move your data into.
Reading many long documents at once
Then context windows were often four to eight thousand tokens, which made cross-referencing several papers essentially impossible.
Ubik let you mention a dozen papers in one prompt and read them in parallel against a working draft instead of summarising them one at a time.
While most companies that year were putting a wrapper around a chatbot, this was already the interaction design, the verification safeguards and the multi-agent workflow the rest of the field would go on to build.
<PlainSummary path="/demo/ubik" />
Project plate
a project at plate size: its mark on a tinted square, its name, and one word
<ProjectPlate item={project} />Project mark
the same identity at any size; which art it is gets decided once, in the component

<ProjectMark item={project} size={96} />Project grid
the wall on /demo and the landing's six, from one component; flush inside prose, two across for a short group
<ProjectGrid items={[a, b]} columns={2} flush />Mark glyph
a drawn mark for a project that never had a logo: a character grid with one accent cell
<MarkGlyph name="zero" />
Brand logos
the product icons, inlined from each repo's own icon.svg and re-expressed in currentColor
<CircleheadsLogo size={40} /> <AkaOssLogo size={40} /> <AkaVstLogo size={40} />Project logo
the logo kits' currentColor variants, by name; LOGO_NAMES is the whole set
{LOGO_NAMES.map((name) => <ProjectLogo key={name} name={name} size={36} />)}Trickle specimen
the kit's plate: fifteen of its text animations, running, and all of it CSS
<TrickleSpecimen />
Tags
what kind of thing this is, as chips; KickerTags splits a write-up's middle-dot kicker into the same row
ProductDesktopAgents
ProductDesktopAgents
<TagRow tags={['Product', 'Desktop', 'Agents']} />
<KickerTags>Product · Desktop · Agents</KickerTags>Five pieces of components/ui are not rendered above, on purpose. DemoBack is fixed to the viewport and reads the route, so it belongs to the write-ups it sits beside rather than inside a card. ConditionalFooter and ThemeProvider are infrastructure: they decide what renders and draw nothing of their own. SiteHeader and SiteFooter are whole-page fixtures, the head and foot of the site rather than pieces a page composes.
The measurable half, foundations, carries the spacing scale, radii, motion timings, and breakpoints these primitives are built on. Take both and the system travels intact.
