Starting with constraints
Good design starts with constraints. We gave ourselves three rules:
1. No decorative gradients as backgrounds — use them sparingly, only in hero areas.
2. One accent colour — near-black (#111). Everything else is neutral.
3. Cards must breathe — generous padding, consistent border radius (12px), subtle shadow.
Typography
We chose Geist as the body typeface. It's clean, modern, and reads beautifully at small sizes — perfect for event metadata (locations, categories, host names).
For display text (hero headings), we use a tight letter-spacing: -0.02em and a fluid clamp() font size so the heading scales gracefully from mobile to desktop.
The card system
Every event card follows the same rhythm:
- Cover image (16:9, object-cover)
- Category badge (coloured pill)
- Title (2-line clamp, 15px font-weight 600)
- Location (12px, muted colour, icon prefix)
- Avatar stack of attendees
Consistency across all cards means users can scan the discovery page in a single glance.
Micro-animations
We use three animation patterns across the whole app:
- Fade + slide up on page mount (staggered by 100ms per element)
- Scale 1.02 on card hover (combined with shadow deepening)
- Scale 0.97 on button press (the "click feel")
These are implemented with a simple animate-fade-slide-up utility class and Tailwind's transition utilities. CSS handles the vast majority of animation — no heavy libraries needed.
The design system
The design system lives in globals.css as CSS custom properties. Any component can access var(--accent), var(--text-secondary), var(--ef-shadow) — making it trivially easy to keep the UI consistent as the codebase grows.
This approach also means theming is a first-class concern from day one. Swapping the accent colour or adjusting the shadow scale requires changing one value in one place.