The TIER EVERYTHING design system
Everything on this page is read live from the stylesheet the product actually ships. If a token changes in assets/app.css, this page changes with it — a design document that restates its own palette is wrong the moment someone edits one of the two.
An accent pair, a five-rung ramp, and six tones
The palette is the accent pair (cyan → blue) plus the five-rung tier ramp, and that is the whole colour story. Every label, button, link, glow, focus ring and the page's radial wash is written against var(--cyan) / var(--tier-*) — never a literal — which is what makes a tone switch repaint the entire site from nine values.
Five rungs, one vocabulary: GOATED → Top Tier → Chad → NPC → Cooked, short forms GOAT / TOP / CHAD / NPC / COOK. Rating maps linearly: index 0 = GOATED, 4 = Cooked; a 1–5 score of r lands on index 5−r. Anything that paints a rung asks tierColor(i) — a JS copy of these colours is a bug by definition.
Rungs bright enough carry the dark label #04121b; the rest carry #dfe7f5. The ramp always keeps that near-white-to-near-background shape, so both label colours stay legible in every tone.
A tone is not a theme: it is the accent pair plus the five rungs, and nothing else. The user's choice lives in localStorage['tierhq.tone.v1'] and lands on <html data-tone> before first paint, so a saved tone never flashes the default. Swatch menus reuse the same nine values through .tone-sw[data-tone] — one source of truth, no JS copy.
aqua — default
The brand toneblue #2e93ff
goated #2ee7ff
cooked #364159
violet
Purple accent pairblue #8b5cf6
goated #c4a2ff
cooked #3b3a55
ember
Warm accent pairblue #ff8a3d
goated #ffc46b
cooked #563c35
lime
Green accent pairblue #4fd97a
goated #b6ff72
cooked #344f42
rose
Magenta accent pairblue #ff4fa8
goated #ff9ed6
cooked #573a4c
ice
Pale accent pairblue #9dc2e8
goated #d8ecff
cooked #38424f
color-mix(in srgb,var(--cyan) N%,transparent) — never an rgba() literal, because the rgba literal survives a tone switch and the mix does not.tierColor(i); the TIER_COLORS map exists to document, not to paint.#ff2b31 never appears in the product; alarm states wear --pink.Two families, one dense scale
This is an instrument panel, not a magazine: the scale runs from 9.5px to 19px in half-pixel steps, because a board full of tiles cannot afford 14px body copy. Both families ship as self-hosted variable woff2 with font-display:swap — no third-party font requests, no layout jank.
| Family | Role | Range / rules |
|---|---|---|
| Inter | Body, UI, inputs | Variable 100–900, stack Inter, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif |
| Space Grotesk | Display, numbers, scores | Variable 300–700. h1–h3 at 700 with letter-spacing:-.01em; .px utility at 700 / +.5px; the logo at 17px / +1px |
| Mono | Tokens, code, IDs | ui-monospace, "Cascadia Mono", Consolas, monospace — used for token names and any value a human may need to copy |
| Specimen | Size | Where it earns its place |
|---|---|---|
| GOATED | 19px | Score readouts, hero numbers |
| GOATED | 17px | Logo wordmark, big-widget score |
| Rank anything | 16px | Section intros, form labels |
| from GOATED to Cooked | 15px | Page sub-heads, lede copy |
| from GOATED to Cooked | 14px | Default body in prose blocks |
| Top Tier | 13.5px | Buttons, inputs, chatty UI |
| Top Tier | 13px | Nav links, table cells, item names |
| Chad | 12.5px | Meta lines, descriptions |
| Chad | 12px | Tags, chips, dense lists |
| NPC | 11.5px | Footnotes, code blocks |
| NPC | 11px | Uppercase micro-labels, kickers |
| Cooked | 10.5px | Legal, credits, disclaimers |
| Cooked | 9.5px | Max density — short rung labels only |
Weights in active use: 400 (body), 600 (emphasis), 700 (display). Anything above 19px belongs to the hero, which is the only place the scale may be broken.
One wrap, two radii, an honest ladder
The page is a single centred column; density changes at five breakpoints and nowhere else. Grids use minmax(0,1fr) tracks because a flex/grid item that refuses to shrink below its content is how a board ends up wider than the phone it is on.
| Token | Value | Meaning |
|---|---|---|
| .wrap | max-width:1180px; padding:0 20px | The one content column — every page shares it |
| --r | 14px | Cards, panels, modals, popovers |
| --r-sm | 9px | Chips, inputs, inner surfaces |
| --glow | 0 0 34px color-mix(--cyan 38%) | The signature cyan bloom |
| --glow-sm | 0 0 16px color-mix(--cyan 45%) | Small controls, focus |
| Width | What changes |
|---|---|
| 1260px | Nav condenses — search collapses into the account row |
| 1240px | Home drops its sticky sidebars into the single column |
| 900px | Footer network: 3 columns → 2 |
| 820 / 760px | Board and cover editor reflow; previews shrink |
| 720px | Cards, vote boxes, sort tabs, hero lines stack |
| 620px | Footer network → 1 column; nav final collapse |
| 560 / 520px | Touch targets, avatar previews, pairwise voting |
hover:none and pointer:coarse are treated as first-class: things that only exist for a mouse (hover reveals, drag hints) are replaced by always-visible touch affordances, not left broken.
Official marks only, inlined
Every social icon is the platform's own artwork, inlined as SVG paths from the single icon set (cdp-tools/icons/inline.json, 24×24 grid, fill:currentColor). No redrawn logos, no mixed stroke weights, no icon fonts. Where a brand needs an alias for a region, the alias points at the official mark: douyin → tiktok, CWS → googlechrome.
Icons outside this set are product-drawn UI glyphs (chevrons, dice, stars) and follow the same rule: geometric, currentColor, no fills outside the tier ramp.
The machine nods
The delight layer answers one question per interaction: did that land? Every animation is short (240–700ms), spring-eased, decorative nodes clean themselves up, and none of them ever change state — state changes first, motion reports it. Press the demos; they are the product's own classes.
| Animation | Trigger | Spec |
|---|---|---|
| dx-roll (score) | Any vote that changes the count | 320ms cubic-bezier(.3,.9,.35,1) — ghost out, value in |
| dx-punch | Vote button, 1–5 rating choice | 260ms cubic-bezier(.2,1.6,.4,1) — 1 → .86 → 1 |
| dx-ring out / in | Vote cast / vote taken back | 420ms — cyan out, pink in; the shape says the direction |
| dx-delta | Every counted action | 700ms, lifts from the widget's own centre (cards clip overflow) |
| dx-bloom | An item lands in a rung | 420ms, rung's own tier colour, brightness 1.32 |
| dx-shimmer | GOATED rung only | 600ms single sweep — never a loop |
| dx-settle | Tile finishes dropping | 300ms cubic-bezier(.2,1.6,.4,1) |
| dx-card-in | Feed first paint | 340ms, 34ms stagger, cap 12, one-shot latch |
| dx-fade-in | Item page status line | 240ms |
prefers-reduced-motion:reduce every animation is none !important and the JS skips building the decorative nodes entirely — state still changes, nothing is left mid-flight..vscore holds the final value in the same frame; the old value lives in the ghost. Writing the old number back into .vscore "temporarily" is banned.no-preference. Headless Chrome reports reduce by default, so a fixture that forgets to emulate no-preference tests an empty set and passes green while nothing moves.Parts, not pages
Every interactive part of the product is a class in app.css, composed from the tokens above. The specimens below are the real components — the same markup the product ships.
| Family | Parts |
|---|---|
| Frame | nav (with narrow-screen collapse) · hero · section · cards grid · empty state · footer network · modal |
| Input | buttons (default / primary / ghost / danger / small) · forms · sort tabs · topic chips · place picker · avatar picker · board picker · duplicate guard |
| Opinion | vote box (sm / big) · rating a comment (tap = good, hold = pick a rung) · community verdict · pairwise · place chip |
| Board | tier rows + labels · items & sub-items · drag ghost · tap-to-place · drop-hot · immersive board · board wall · keep-going |
| Context | author link · comments · revision history · sponsored flag · ad slots · community suggestions |
| Identity & money | signed-in chip · who you are when you post · profile · pricing / Pro · remix |
| Editors | multi-line item entry · preview gallery · list cover guide · cover picker · tab covers |
| Easter eggs | the Konami code · the falling rat · the app's own right-click menu · the tone menu |
Each family above is one commented chapter in app.css — the stylesheet is organised as the system's table of contents.
Talk like a person who built it
How to change the system without breaking it
These are the rules that came from real incidents. Each one exists because breaking it once cost something.
bash cdp-tools/_mirror_tierhq.sh before wrangler deploy in tierhq-web/ — skip it and the probes test the previous build while you debug this one. Propagation ≈30s.run_worker_first paths are exact matches, not prefixes: forget it and the asset handler answers first — the page returns 200 while meta tags silently never change.<!-- seo:start -->…<!-- seo:end --> appears in the page, in _seo_inject.mjs and in the Worker together, or canonical/og/JSON-LD drift apart.[hidden] is protected. [hidden]{display:none!important} stays in the stylesheet — without it, any class that sets a display beats the UA rule and "hidden" elements stay on screen.no-preference; counts come from the API, never hardcoded; a probe whose selector misses must fail loudly, not silently pass.Store.init() first, or it renders the seed boards and looks broken in a way no error explains.A change is not done when it looks right — it is done when a probe has failed at least once proving it can fail.