One source of truth

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.

Version 1.0 Updated 2026-10-17 Applies to tier.fcthq.com Source of truth tierhq/assets/app.css + app.js
01 — COLOUR

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.

Core tokens — read live from :root
--bg#0a0f1aPage ground
--panel#0e1526Cards, popovers
--panel2#131c30Raised inner surface
--line#1c2740Hairline borders
--cyan#2ee7ffPrimary accent, links, focus
--cyan-d#0fb9d8Accent pressed / deep
--blue#2e93ffSecondary accent, gradients
--blue-d#1a6ad4Secondary deep
--pink#ff2e94Alerts, down-votes, warning
--white#ffffffLogo, max emphasis
--off#e6eefcBody text
--dim#7f8ca6Secondary text, meta
The tier ramp — the product's spine

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.

GOATED
GOAT
#2ee7ff
Top Tier
TOP
#2e93ff
Chad
CHAD
#1a6ad4
NPC
NPC
#52678f
Cooked
COOK
#364159

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.

Six tones — nine values each

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 tone
cyan #2ee7ff
blue #2e93ff
goated #2ee7ff
cooked #364159

violet

Purple accent pair
cyan #c4a2ff
blue #8b5cf6
goated #c4a2ff
cooked #3b3a55

ember

Warm accent pair
cyan #ffc46b
blue #ff8a3d
goated #ffc46b
cooked #563c35

lime

Green accent pair
cyan #b6ff72
blue #4fd97a
goated #b6ff72
cooked #344f42

rose

Magenta accent pair
cyan #ff9ed6
blue #ff4fa8
goated #ff9ed6
cooked #573a4c

ice

Pale accent pair
cyan #d8ecff
blue #9dc2e8
goated #d8ecff
cooked #38424f
Colour laws
L1Everything paints from variables. A new emphasis colour is written 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.
L2Tier colours have exactly one painter. Painting asks tierColor(i); the TIER_COLORS map exists to document, not to paint.
L3The alarm red is banned. #ff2b31 never appears in the product; alarm states wear --pink.
02 — TYPOGRAPHY

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.

Families
FamilyRoleRange / rules
InterBody, UI, inputsVariable 100–900, stack Inter, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif
Space GroteskDisplay, numbers, scoresVariable 300–700. h1–h3 at 700 with letter-spacing:-.01em; .px utility at 700 / +.5px; the logo at 17px / +1px
MonoTokens, code, IDsui-monospace, "Cascadia Mono", Consolas, monospace — used for token names and any value a human may need to copy
The scale — every size the stylesheet actually uses
SpecimenSizeWhere it earns its place
GOATED19pxScore readouts, hero numbers
GOATED17pxLogo wordmark, big-widget score
Rank anything16pxSection intros, form labels
from GOATED to Cooked15pxPage sub-heads, lede copy
from GOATED to Cooked14pxDefault body in prose blocks
Top Tier13.5pxButtons, inputs, chatty UI
Top Tier13pxNav links, table cells, item names
Chad12.5pxMeta lines, descriptions
Chad12pxTags, chips, dense lists
NPC11.5pxFootnotes, code blocks
NPC11pxUppercase micro-labels, kickers
Cooked10.5pxLegal, credits, disclaimers
Cooked9.5pxMax 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.

03 — LAYOUT

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.

Frame
TokenValueMeaning
.wrapmax-width:1180px; padding:0 20pxThe one content column — every page shares it
--r14pxCards, panels, modals, popovers
--r-sm9pxChips, inputs, inner surfaces
--glow0 0 34px color-mix(--cyan 38%)The signature cyan bloom
--glow-sm0 0 16px color-mix(--cyan 45%)Small controls, focus
The breakpoint ladder
WidthWhat changes
1260pxNav condenses — search collapses into the account row
1240pxHome drops its sticky sidebars into the single column
900pxFooter network: 3 columns → 2
820 / 760pxBoard and cover editor reflow; previews shrink
720pxCards, vote boxes, sort tabs, hero lines stack
620pxFooter network → 1 column; nav final collapse
560 / 520pxTouch 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.

04 — ICONOGRAPHY

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.

05 — MOTION

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.

Vote — answer, don't hint
128
The real vote widget. Old score rolls out as a ghost while the new one rolls in, the button punches, a ring leaves (or collapses into) the button, and the delta drifts up.
GOATED
🍜 Ramen
Click the GOATED label for its bloom + one shimmer sweep; click the tile for its settle.
First screenful only — the stagger is bounded to 12 cards on purpose, so paging to the end never re-plays the entrance.
34ms between cards, one pass, then the latch closes.
Reduced motion: cards are simply there.
The catalogue
AnimationTriggerSpec
dx-roll (score)Any vote that changes the count320ms cubic-bezier(.3,.9,.35,1) — ghost out, value in
dx-punchVote button, 1–5 rating choice260ms cubic-bezier(.2,1.6,.4,1) — 1 → .86 → 1
dx-ring out / inVote cast / vote taken back420ms — cyan out, pink in; the shape says the direction
dx-deltaEvery counted action700ms, lifts from the widget's own centre (cards clip overflow)
dx-bloomAn item lands in a rung420ms, rung's own tier colour, brightness 1.32
dx-shimmerGOATED rung only600ms single sweep — never a loop
dx-settleTile finishes dropping300ms cubic-bezier(.2,1.6,.4,1)
dx-card-inFeed first paint340ms, 34ms stagger, cap 12, one-shot latch
dx-fade-inItem page status line240ms
Motion contracts
M1Reduced motion is a contract, not a switch. Under 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.
M2Decorations never receive pointer events — a ring or a delta must not eat the next click.
M3The score value is never faked. .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.
M4Verify motion with 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.
06 — COMPONENTS

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.

Live specimens
BUTTONS
10px radius, 700 weight, 1px hairline; primary wears the cyan→blue gradient and the glow.
SORT TABS & CHIPS
Pills everywhere; the selected state is the only place the gradient appears twice.
TOAST
Vote counted — thanks for the argument.
One toast at a time, bottom centre; errors swap cyan for pink, nothing else.
The component index
FamilyParts
Framenav (with narrow-screen collapse) · hero · section · cards grid · empty state · footer network · modal
Inputbuttons (default / primary / ghost / danger / small) · forms · sort tabs · topic chips · place picker · avatar picker · board picker · duplicate guard
Opinionvote box (sm / big) · rating a comment (tap = good, hold = pick a rung) · community verdict · pairwise · place chip
Boardtier rows + labels · items & sub-items · drag ghost · tap-to-place · drop-hot · immersive board · board wall · keep-going
Contextauthor link · comments · revision history · sponsored flag · ad slots · community suggestions
Identity & moneysigned-in chip · who you are when you post · profile · pricing / Pro · remix
Editorsmulti-line item entry · preview gallery · list cover guide · cover picker · tab covers
Easter eggsthe 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.

07 — VOICE & CONTENT

Talk like a person who built it

V1English everywhere in the product. UI, buttons, navigation, toasts, errors, sample data, titles and meta descriptions are all English — the audience is the internet, and a mixed-language interface reads as a bug.
V2Use the vocabulary. The scale runs GOATED → Top Tier → Chad → NPC → Cooked. Copy that speaks this dialect ("rank it", "cooked", "the argument") is on-brand; copy that speaks generic SaaS ("leverage", "empower", "supercharge") is off-brand.
V3Never ship a number you did not measure. Counts, ranks and stats are derived from the API at render time. Placeholder totals, fake "users online" and invented testimonials are forbidden — a product that lies about small numbers never gets to tell big ones.
V4The maker is disclosed. Any promotion is posted by the real author, saying so. No sock puppets, no manufactured engagement, no astroturfed "I found this site" posts.
V5Errors say what happened and what to do. "Couldn't reach the board — check your connection and retry" beats "Something went wrong". Empty states offer the next action, not an apology.
08 — THE LAWS

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.

L1Deploy is a snapshot, and the mirror runs first. 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.
L2A new edge-rewritten page joins the whitelist. 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.
L3The SEO block is a marked contract. <!-- seo:start -->…<!-- seo:end --> appears in the page, in _seo_inject.mjs and in the Worker together, or canonical/og/JSON-LD drift apart.
L4[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.
L5Fixtures must not be able to pass vacuously. Motion is verified under emulated no-preference; counts come from the API, never hardcoded; a probe whose selector misses must fail loudly, not silently pass.
L6One selected rung per box, and the ratio is honest. srcset ladders pick the smallest candidate that is ≥1 and <1.5× the real box — under is blurry, over is waste, and the ceiling running out is reported, not hidden.
L7New pages wait for the store. Anything that lists data calls Store.init() first, or it renders the seed boards and looks broken in a way no error explains.
Pre-deploy checklist
1 bash cdp-tools/_mirror_tierhq.sh # source tree → deployment tree, every file byte-compared 2 cd tierhq-web && wrangler deploy # full-site snapshot 3 wait ~30s, then smoke: / , /sitemap.xml , /design , one list , one item 4 run the matching probe: _verify_pages · _verify_seo · _verify_delight · _verify_anonvote

A change is not done when it looks right — it is done when a probe has failed at least once proving it can fail.

Press release distribution by SubmitPRFeatured on DebutPinListed on JustHunt