The code is the source of truth (lib/design, lib/themeTokens, components/ui);
this page mirrors it and writes down the rules. A component without a
when-to-use rule is just decoration. When code and this page disagree,
one of them gets fixed the same day.
Color
Five themes ship (arnd-modern default, react95, neon-noir, paper, candy), each in light and dark. Swatches below are the default theme; the rules hold for all of them.
Brand and semantics
Performer vibes
Eight identity colors a performer's surfaces wear (hero wash, genre chip, glyph, poster tiles). Derived from genre, overridable per artist in Edit Profile.
Rules
→Bellflower is the only UI accent. Primary actions, active states, links, focus. If a control is a different color, it needs a rule on this page.
→Green means money or verified. Nothing else. Tip buttons, verified attendance, checked-in states, claim confirmations. Never decoration.
→Vibes belong to performers. They color a performer's identity surfaces and never the app's own chrome. The Follow button is bellflower, not the artist's color.
→Live red means happening right now. The live dot, on-site markers, pulsing pins. Not errors (danger owns those), not emphasis.
✕No hardcoded hex in screens. Every color goes through useThemeTokens() or a vibe. Hardcoded tints were the single biggest source of drift found in the July audit.
→Every text pair clears WCAG AA (4.5:1) in every theme, both modes. Measured, not eyeballed; candy and paper accents were re-tuned for exactly this.
Shape and type
Radius scale
sm 8
md 12
lg 16
xl 20
2xl 24
pill
→Cards and callouts: lg (16). Icon tiles and buttons: md (12). Chips and counts: pill. Sheets and heroes: xl (20) and up.
✕No literal radii in screens. 14 and 18 were the two most common drift values; both are gone.
Type ramp
Inter 600 · 11 · tracking 1.4 · capsEyebrow label
Inter 400 · 13 · line 19Body copy on cards and callouts.
Inter 700 · 16 · tracking -0.2Callout and card titles
Inter 700 · 27 · tracking -0.8Screen headlines
Display faces · poster surfaces onlyPoster headlines
→Inter is the app's working face. Display faces (Baloo 2 in-app, Righteous on the site, plus the GigBanner poster set) appear only on poster surfaces.
✕Known debt: three screens still render Poppins where Inter is the standard. On the cleanup list; do not add more.
Components
The shared pieces in components/ui and components/composed, each with the only question that matters: when is it used, and when is it not.
CalloutCard, the one promo and info card
Every accent promo, invite, CTA, and info note in the app. One shape (radius 16, a 44x44 tile at radius 12, one type ramp), three volumes. Replaced twenty hand-rolled cards in the July audit.
🗺
● Happening now
6 live sets around you
Open the map to see what's on nearby
loud · solid accent, poster arc · max ONE per screen, the time-sensitive headline action
🎤
You play?
Your page could be on this map
See a sample performer page. A park counts.
soft · tinted, 1px accent border, solid tile · the default invite and CTA
Thanks for testing ARND early
Passive info that should recede while staying on-brand.
quiet · tinted, no border, tile optional · settings notes and disclosures
→loud: one per screen, maximum. If two things shout, nothing does.
→soft: the default whenever the card asks the user to act. Whole card pressable; dismiss sits top-right as a sibling control.
→quiet: anything that explains rather than asks. tone="success" flips the family green, for confirmations only.
✕Never hand-roll an accent-tinted box. If CalloutCard cannot express it, the component grows, not the screen.
Buttons
Primary action →
Secondary
🎁 Tip artist
🎁 Tip
Button primary / secondary · TipButton solid / soft (always the green gift)
→One primary per view. Everything else is secondary or a text link. Labels are sentence case, say what happens ("Get on the map", "Save my pitch"), and truncate rather than wrap.
→Tipping is always the green gift, one TipButton component at any size or emphasis. Never a bespoke tip control; the green is the money signal.
→One press feel. Every tap runs through PressableScale's single spring, gated by reduce-motion. No screen hand-rolls press animation.
Chips, badges, and the honesty label
🎙 Jazz · Vocal
✓ Free
4
⚗ Sample artist page
✓ 81 verified
vibe genre chip · trust chip · count pill · flask sample label · verified sticker
→The flask means "not real yet". Sample artist pages, sample gigs, preview inboxes: anything demonstrative wears the flask before the user can act on it. Hard rule.
→The verified sticker stays inked: white card, near-black border, slight tilt, identical in both color modes. It is a stamp, not a badge.
→Hand-placed tilt (1.5 to 5 degrees) belongs to poster surfaces and stickers only. Forms and settings sit straight.
Poster art: GigBanner and ArtistCover
→GigBanner draws every show's poster procedurally: the family is picked content-aware from the title and the result is deterministic per show. No stock photos, no gray placeholders, ever.
→ArtistCover gives every performer a generated cover (pulse, halo, or field) in their vibe color, deterministic per artist, overridable in Edit Profile.
→Real captures live in the App atlas; this page stays schematic on purpose so it never rots.
Map pins
| State | Treatment | Rule |
| Default | Dot pin in the show's genre color | Genre color is the base identity, web and native alike |
| Live now | Pulsing ring in live red | Only while the set is inside its window or the artist is on site |
| Starting soon | Amber ring | Within the pre-show window |
| Trending | Flame marker | Computed from activity, never hand-assigned |
States: loading, empty, error
→Loading is a skeleton, not a spinner-then-flash. Lists show shaped placeholders (FeedCardSkeleton, Skeleton).
→Empty states are branded and honest (EmptyState): icon circle, one human line, one action. A genuinely quiet catalog says "quiet out there", which is different from "nothing matches this filter".
→Errors are reachable and retryable. Live-data failures surface the error card with a retry. The app never silently swaps in demo data.
Honesty patterns
ARND's product is trust, so these are design rules with the same force as layout rules.
→Never render success for something that did not happen. No "sent" for an undelivered pitch, no "you tipped" for money that never moved. If the real thing is not wired yet, the UI says what actually happened: "saved on this device".
→Samples wear the flask, before the user can act on them.
→Counts are measured or absent. "RSVPs sync soon" beats a fabricated zero. The feed banner counts only real upcoming sets, and hides at zero instead of shouting about nothing.
→Device-local features say so, in plain language, where the action happens.
→Money moves person to person through the artist's own payment app, and the UI never implies ARND holds it.