freepass
Design system

Design system

Every colour token, every primitive and every state freepass is built from. Inherited from direbot without changing a single value — this page is where you check that claim rather than take it.

Two tests lean on this page: one fails if a primitive is not reachable from it, one fails if a colour token has no caller. It is a fixture as much as a document.

Colour

One theme, dark. Thirty-six tokens, and the list below is checked against globals.css by a test — a token added there and forgotten here fails the build.

backgroundстраница
foregroundосновной текст
cardвсё поднятое: карточка, панель, диалог
card-foregroundтекст на карточке
popoverплавающая панель
popover-foregroundтекст в плавающей панели
primaryакцент как ЗАЛИВКА
primary-hoverакцентная заливка под курсором
primary-foregroundподпись на акценте
contrastсветлая плашка — обычное главное действие
contrast-hoverсветлая плашка под курсором
contrast-foregroundподпись на светлой плашке
secondaryтихий залитый контрол, скелетон
secondary-foregroundподпись тихого контрола
mutedподсветка строки под курсором, мягкий чип
muted-foregroundподписи и вторая строка
subtle-foregroundлейблы полей, 5.2:1
faint-foregroundплейсхолдеры и мета, 4.1:1
accentто же, что muted — под фокусом в примитивах
accent-foregroundтекст на accent
accent-softподложка под акцентным текстом
linkакцент в ТЕКСТОВОМ весе, 8.5:1
link-hoverссылка под курсором
moneyвеличина в пользу читателя: размер бонуса, срок
destructiveнеобратимое действие и отказ
destructive-softподложка под ним
borderконтур карточки и линейка под её шапкой
border-secondaryлинейка МЕЖДУ строками внутри
border-hoverлюбая рамка под курсором
inputконтур того, что нажимают или в чём пишут
sunkenниже карточки: поле ввода, утопленная строка
ringкольцо фокуса
successподтверждение
success-softподложка под ним
warningпредупреждение
warning-softподложка под ним

The accent has two weights, and this is the rule broken most often

Fill with --primary, write with --link. As text on the page, the fill measures 4.9:1; the link weight measures 8.5:1.

Terracotta as text — 4.9:1, do not

fill

Pairings, with the measured ratio

A swatch shows a colour. It does not show whether text reads on it — which is the half that actually breaks.

Commit4.65:1

подпись тёмная, а не белая: белая дала бы 3.81:1 — ниже AA

Continue15.4:1

обычное главное действие — эта пара, а не терракота

акцентный текст лежит только на этой подложке

Verified7.9:1
Not checked8.2:1
Offer expired7.4:1

Sunken, card, page

A card sits above the page.

A field is a well cut into it, not a plate laid on it.

Type

A closed ladder of ten steps, written by the pixel. Naming them sm / base / lg would invent relationships the design does not have.

  • text-12 · 12px/1.45Free access to AI services, and what it costs after
  • text-13 · 13px/1.5Free access to AI services, and what it costs after
  • text-14 · 14px/1.5Free access to AI services, and what it costs after
  • text-15 · 15px/1.55Free access to AI services, and what it costs after
  • text-16 · 16px/1.5Free access to AI services, and what it costs after
  • text-17 · 17px/1.45Free access to AI services, and what it costs after
  • text-18 · 18px/1.45Free access to AI services, and what it costs after
  • text-19 · 19px/1.45Free access to AI services, and what it costs after
  • text-20 · 20px/1.35Free access to AI services, and what it costs after
  • text-32 · 32px/1.15Free access to AI services, and what it costs after

Three families

font-sans · Onest

Body copy, labels, everything the interface says. Кириллица тоже есть.

font-serif · Source Serif 4

Fifteen pixels — the text cut.

Thirty-two

Different letterforms, not the same cut scaled — that is the opsz axis. If they look identical, it was dropped from next/font.

.tnum · JetBrains Mono

1 111 111

8 000 000

Tabular figures: the column does not jitter when a number changes.

Radius & elevation

Six on everything you click, twelve on everything that holds content, 999 on pills. Elevation is carried by the border, not by a shadow.

4px

6px · controls

12px · content

14px

pills

--shadow-sm is none, deliberately

--shadow-md

--shadow-lg · only things that float

Buttons

default is the light plate; accent is the terracotta and belongs to the action that commits. A screen with two accents is a screen where one is wrong.

default
accent
outline
quiet
ghost
secondary
destructive
link
asChild, as a link

An icon inside a button is sized by the base class, so it never needs a size of its own. `asChild` puts the chrome on the element the pointer actually reaches.

Badges

A tag is something the system tells you. A pill is something you click to narrow a list. Different objects, so shape is a separate axis.

tag · radius 6softneutraloutlineselectedclayfreesuccesswarningdestructive
pill · radius 999softneutraloutlineselectedclayfreesuccesswarningdestructive

No dot beside the word: a dot next to a 12px label reads as an unloaded avatar.

Surfaces

One raised surface for everything: card, panel, dropdown, dialog. Hover lifts the border and nothing else.

A plain card

Border, no shadow.

A card with a header

  • First row
  • Second row

The header rule is stronger than the rules between rows. Swapping them turns a list into a grid.

A card that is a link

free tier

Surface and hit area are the same rectangle.

asChild

Forms

A label is quieter than the value it names. A field is sunken; a picker sits on the surface.

Focus takes a 3px ring at 35% and a terracotta border. Hover lifts the border only.

Table

Built on the mock catalogue rather than lorem ipsum: real names truncate, real offers wrap, and an unchecked entry has to say so.

Placeholder data. Nothing below has been verified, and nothing links out yet.
ServiceCategoryOfferChecked
ClaudeLong-context assistant with a code-capable agent mode.chatFree tier, no cardMessage limits reset every few hoursnot checked
PerplexitySearch that answers in prose and shows where each claim came from.chat1 month ProNew accounts onlynot checked
MidjourneyImage generation with the strongest house style of the lot.images25 free generationsnot checked
ElevenLabsSpeech synthesis and voice cloning in about thirty languages.audio10k characters a monthAttribution required on the free tiernot checked
RunwayText- and image-to-video, plus the editing tools around it.video125 credits oncenot checked
CursorAn editor built around a model that can read the whole repository.code14 days ProCard required to start the trialnot checked
Replit AgentBuilds and deploys a small app from a prompt, in the browser.agents$10 in creditnot checked
NotebookLMGrounds every answer in documents you upload, and cites them.agentsFree, no paid tier yetnot checked
8 services in the mock0 verified

States & motion

Loading, empty, confirmation. The pulse is slower and shallower than Tailwind's, and reduced-motion kills it outright.

Skeleton

The 110×40 block is the shape the header’s identity slot used in direbot, where the guest answer was a button that size. Here the guest answer is nothing, so the header ships no visible placeholder — see site-header-shell.tsx.

Empty

No services match that filter

An empty state says what is missing, not that something went wrong.

Confirmation

Pulse

animate-pulse-soft · 1.4s, 0.35→0.7

Tailwind’s animate-pulse, for comparison

Both stop dead under prefers-reduced-motion: the media query at the bottom of globals.css kills every animation and transition on the page.

The cn() seam

The one failure that typecheck, unit tests and the build all miss: a class written in the source that never reaches the DOM.

cn('text-contrast-foreground', 'text-15')text-contrast-foreground text-15
cn('text-15', 'text-contrast-foreground')text-15 text-contrast-foreground
cn('text-13', 'text-16')text-16
cn('text-destructive', 'text-success')text-success

The first two must keep both the size and the colour. The last two must resolve — a real conflict still has to have a winner. Printed rather than described, because the two are indistinguishable in a diff.

Chrome

The header and footer hang 60px outside the text column on purpose. Above 1240 the inset grows on its own.

px-[max(60px,calc((100%-1240px)/2))] max-lg:px-8 max-sm:px-[18px]
content column

Header states

The identity corner is the one part of the header that is not in the prerendered HTML: it reads the session at request time, inside a Suspense boundary. All three states are drawn below directly, out of the real components — the live header above shows whichever one applies to you.

pending — what ships inside every prerendered page
guestSign in

All three are the same width band by design: the pending block is exactly the guest button’s 110×40, so the common answer lands without moving anything. The signed-in corner is wider and grows leftward — the nav never moves.