Design System
The visual, interaction, and copy language for the project's two visible surfaces — the consumer site at flighthelp.net and the contributor PWA at contribute.flighthelp.net. A common system runs across both; specific tunings differ.
The aesthetic is editorial-utilitarian: information-first, reference-shaped, calm. The reference points are the Financial Times, the NYT's data-graphics work, Wikipedia, Bloomberg Terminal, the BBC News mobile site, and travel-industry tools like FlightAware and FlightRadar24 — but more restrained than any of them.
The single most important rule: the design must let the content be the loudest thing on the page. Branding, decoration, illustrations, and design flourishes all defer to the data.
Core principles
1. Information density is a feature, not a bug
The user is checking a fact. They are not browsing for entertainment. Show them the fact. Then the source. Then the freshness. Then the next-most-likely-needed adjacent fact. All on the same screen.
Generous whitespace, large hero images, and "breathing room" are the wrong instincts for this project. They work for marketing sites. The reference site is not a marketing site.
2. Numbers and codes lead
When the page has a number to communicate (a distance, a weight, a wait time, a compensation amount), the number is the largest type on the page. The label is smaller. The unit is smaller still.
A baggage rule presents as:
10 kg
Maximum carry-on weight · Iberia Economy
Verified 19 May 2026 by 47 travelers
Not:
Your carry-on baggage allowance for Iberia Economy class is 10 kilograms maximum.
3. Provenance is visible, not hidden
Every fact shows:
- The verification timestamp
- The verifier count
- A "view source" affordance
- A "still accurate?" thumbs button
- An "edit" link
These are not power-user features. They are first-class UI elements. Their visibility is what makes the project's data trustworthy in a way that no closed competitor can match.
4. Stale is shown, not concealed
If a fact hasn't been verified in 8 months, the page shows "Not verified in 8 months" in soft amber. It does not pretend the fact is current. It does not omit the freshness indicator. Travelers can decide what level of staleness they're comfortable with.
5. Speed is part of the design
The design system enforces the performance budgets:
- No web fonts that block paint (use system fonts, or self-host with
font-display: swap) - No hero images that demand attention
- No carousels
- No client-side rendering for primary content
- No layout shifts after initial paint
A page that takes 4 seconds to render is broken, regardless of how it looks.
Color
Palette
Two themes: light (default) and dark. Both designed to meet WCAG AAA for body text and AA for everything else.
Light theme:
--bg-primary: #FAFAFA (the page)
--bg-secondary: #FFFFFF (cards, code blocks)
--bg-tertiary: #F1F1F0 (table headers, selected states)
--text-primary: #1A1A1A (body)
--text-secondary: #4A4A48 (labels, captions)
--text-tertiary: #767672 (meta, footnotes)
--accent-primary: #1E3A8A (links, primary CTAs; deep blue)
--accent-warm: #B85C00 (numbers, prices, key facts; warm orange)
--accent-fresh: #2D7D2D (verified status, success states; green)
--accent-stale: #A87000 (stale data warning; amber)
--accent-error: #B91C1C (errors, rejections; red)
--accent-disputed: #6B21A8 (active disputes; purple)
--border-subtle: #E4E4E1 (table dividers)
--border-strong: #B0B0AD (active focus, primary borders)
Dark theme:
--bg-primary: #0E0E0D (the page)
--bg-secondary: #1A1A18 (cards, code blocks)
--bg-tertiary: #242420 (table headers)
--text-primary: #E8E8E5 (body)
--text-secondary: #B4B4B0 (labels, captions)
--text-tertiary: #807F7A (meta)
--accent-primary: #7DA3F2 (links, lighter for contrast)
--accent-warm: #E9A55E (numbers)
--accent-fresh: #6FB36F (verified)
--accent-stale: #D9B164 (stale)
--accent-error: #E66B6B (errors)
--accent-disputed: #B69BE0 (disputes)
--border-subtle: #2C2C28 (dividers)
--border-strong: #4A4A45 (active borders)
Color rules
- Each color has a specific semantic role. No decorative use.
- Accent colors are used sparingly — typically one accent per screen section, never as a background.
- The "warm" accent is reserved for numbers and headline facts. It draws the eye to what matters.
- Status colors (fresh, stale, error, disputed) appear in indicators only, never in body text.
- Contrast is tested across both themes for every component.
What the palette deliberately avoids
- Bright primary blues, reds, or greens that suggest social-network UI
- Pastels or muted "calm" colors that suggest wellness apps
- Gradient backgrounds, shadows, or other "modern" decorative effects
- Brand colors that signal the project itself; no logo bleed into the data presentation
Typography
Type stack
--font-display: 'Inter Variable', system-ui, sans-serif;
--font-body: 'Inter Variable', system-ui, sans-serif;
--font-mono: 'JetBrains Mono Variable', 'SF Mono', monospace;
--font-serif: 'Source Serif Variable', Georgia, serif;
- Inter for almost everything. It's free, distributable, ubiquitous on this kind of site, and its variable nature lets us tune weight and slant precisely.
- JetBrains Mono for code samples, IATA codes, flight numbers, currency amounts when alignment matters.
- Source Serif for long-form scenario explanations and knowledge pages — gives those pages a "reading" feel distinct from the dashboard-shaped airline pages.
Fonts are self-hosted to avoid third-party requests.
Scale
--text-xxs: 0.75rem (12px) — micro labels, sources
--text-xs: 0.875rem (14px) — captions, meta
--text-sm: 1rem (16px) — body small
--text-md: 1.125rem (18px) — body default (16px on mobile, 18px on desktop)
--text-lg: 1.5rem (24px) — subheaders
--text-xl: 2rem (32px) — section headers
--text-2xl: 2.625rem (42px) — page titles
--text-3xl: 3.5rem (56px) — display numbers (the warm accent)
The display sizes are used for headline numbers (compensation amounts, distance, weight). They are intentionally large.
Type rules
- Body text defaults to 18px on desktop. Smaller than that is for meta information.
- Line height is generous: 1.6 for body, 1.4 for headings, 1.2 for display.
- Letter spacing is tight on display sizes, normal on body, slightly loose on micro labels.
- Numbers in tables use tabular figures (
font-variant-numeric: tabular-nums). - Never use thin or light weights for body text; minimum weight is 400.
- Never use italic for emphasis in body text; use medium or semibold.
Spacing and layout
Base unit
4px. Every spacing value is a multiple of 4px. This makes vertical rhythm consistent and easy to maintain across components.
Spacing scale
--space-1: 4px
--space-2: 8px
--space-3: 12px
--space-4: 16px
--space-6: 24px
--space-8: 32px
--space-12: 48px
--space-16: 64px
--space-24: 96px
Container widths
- Reading width (for long-form content): max 720px
- Default content width: max 1100px
- Wide content (tables, comparison): max 1400px
- Full bleed: no max width, but content within still respects max widths
Page structure
Every page has the same skeleton:
[ Global header — 56px height, fixed ]
[ Page-specific header — entity name, status strip ]
[ Quick actions row — 4 pill buttons or links ]
[ Main content — tabbed when complex, scrollable ]
[ Footer — contributors, license, edit link, last verified ]
[ Global footer — about, transparency, languages ]
The page-specific header and quick actions row appear on entity pages (airline, airport, scenario). Other pages skip them.
Components
Buttons
Three variants, used sparingly:
- Primary: Solid background in
--accent-primary, white text. Used for the main action on a page (e.g., "Submit edit" in the contributor app). At most one primary button per page section. - Secondary: Border only, no fill. Used for "Edit," "Source," "Report" actions.
- Tertiary: Text only, no border, blue color. Used for navigation, "see all," etc.
Buttons are 40px tall by default, 48px on mobile. Padding inside is generous (16px horizontal, 8px vertical).
Cards
Used to group related content. Subtle border, white background (or --bg-secondary in dark mode). No drop shadows. Internal padding 24px.
Tables
The workhorse of the site. Specific rules:
- Tabular figures throughout
- First column left-aligned, subsequent columns right-aligned for numbers, left-aligned for text
- Row hover highlights the row in
--bg-tertiary - Sortable headers have a visible sort indicator
- Pagination at bottom right
- Mobile: tables become stacked cards, one row per card
Tabs
Used on entity pages to organize sections (Contact, Baggage, Fees, Scenarios). Underline-style tabs, current tab highlighted in --accent-primary.
Forms (contributor app only)
- Single column layout
- Labels above inputs, never inside (placeholder text is not a label)
- Required fields marked with subtle asterisk
- Inline validation; errors appear below the field in
--accent-error - Submit buttons full-width on mobile, right-aligned on desktop
- Save-draft option always available
Modals
Avoided. The consumer site has no modals. The contributor app uses modals only for confirm-cancel of irreversible actions and for image enlargement.
Toasts and notifications
Short-lived, top-right corner. Auto-dismiss after 4 seconds. Click to dismiss earlier. Non-blocking.
Status indicators
Used on every fact-carrying entity:
- Fresh (verified within 30 days): small green dot
- Aging (30–180 days): no indicator
- Stale (180+ days): small amber dot with "Not verified in N months" label
- Disputed: small purple icon with "Disputed: 4 contributors disagree"
- Auto-fact-checked: gear icon when the value was set by a scraper
Iconography
Lucide React for icons. Single-color, line-style, 24px default. Used as labels for actions, not as decoration.
Specific icons for specific things:
- Search:
Search - Edit:
Pencil - Source:
Link - Verified:
CheckCircle2 - Stale:
Clock - Disputed:
AlertCircle - History:
History - Settings:
Settings - Languages:
Globe - Filter:
Filter - Sort:
ArrowUpDown
Custom icons are added only when Lucide doesn't cover the case. Custom icons follow the same line-style and 24px sizing.
Photography
Two categories: contributor-uploaded and editorial.
Contributor-uploaded:
- Gate sizer photos: standardized framing where possible (sizer with bag in it, sizer dimensions visible)
- Lounge interiors: enough context to see the space
- Terminal layouts: wide shots
- All photos: EXIF stripped, faces blurred where auto-detected
- Captions include the contributor's display name and date
Editorial: None. The site does not use stock photography. The site does not use hero images. The home page does not have an illustration. This is intentional — the visual language is the data itself.
Motion
Minimal. The site is a reference, not an experience. Motion is used only when it serves comprehension:
- Hover states transition in 100ms
- Tab changes use a 150ms fade
- Modal entry uses a 200ms slide-up (contributor app only)
- Sort arrow animations use a 100ms rotate
- No parallax. No scroll-triggered animations. No looping animations. No autoplay video.
Users with prefers-reduced-motion get static UI throughout.
Voice and copy
The voice is direct, useful, never marketing-y. Specific rules:
- Address the reader as "you" when speaking directly. Otherwise impersonal.
- Numbers and codes lead. "LH 441 · Munich → JFK · 2h 14m delay" not "Your Lufthansa flight from Munich..."
- Verification states are explicit. "Verified by 12 travelers · 19 May 2026" not "Trusted." Stale data shown as "Not verified in 8 months" not hidden.
- Disclaimers and uncertainty visible. "Disputed: 4 contributors disagree" appears inline, not in a footnote.
- No exclamation points except in error messages that genuinely warrant them.
- No "we" in body copy except on About / Mission pages. The site speaks impersonally.
- No "amazing," "great," "best," "top," "premium," "exclusive," or other marketing adjectives.
- Numbers are spelled out under 10 in prose, numerals from 10 up. Always numerals in tables, headlines, and data displays.
- IATA codes always uppercase (LHR, JFK, LH 441).
- Currency uses ISO codes (EUR, USD, GBP) with the symbol where space allows (€250 EUR, $400 USD).
Localization implications
The copy needs to translate without losing structure. Voice and tone notes are part of the translator's guidelines. Some idioms (cracked jokes about Ryanair, for instance) are stripped from translated copy unless an equivalent exists.
Accessibility
WCAG 2.2 AA across the board, AAA on body text and primary content.
- Every interactive element is keyboard-reachable in logical order
- Focus indicators are visible (a 2px outline in
--accent-primarywith offset) - Skip-to-content link as first focusable element
- Form labels properly associated
- Images have alt text (decorative ones have empty alt)
- Color is never the sole carrier of information (status indicators have icons + text + color)
- Screen-reader tested with VoiceOver, NVDA, and TalkBack
- Heading hierarchy is correct (no skipped levels, one h1 per page)
- Tables have proper headers and captions
- All animations respect
prefers-reduced-motion - Mobile tap targets are 48×48px minimum
Accessibility audits run quarterly. Findings become PRs.
Variations by surface
The consumer site
The defaults described above apply directly. Mobile-first, performance-critical.
The contributor PWA
Same design system, but:
- Slightly denser layouts (contributors are power users)
- More form components and workflow patterns
- A persistent "queue" sidebar
- Offline indicators are first-class
- Notification surfaces (in-app and push)
- Reputation/badge displays integrated into navigation
The docs site
Powered by Nextra. Same color palette, same typography. Adds:
- Code blocks with syntax highlighting (Shiki)
- API endpoint documentation with try-it-now panels
- Sidebar navigation with deep linking
- Search across docs
The status page
Hosted on Better Stack or Cloudflare's status page. Same colors mapped to the status page's templating system. Same voice in incident write-ups.
Implementation
The design system lives in packages/design-tokens/ in the flighthelp/web monorepo. It exports:
- CSS custom properties for every token
- A Tailwind config that maps the tokens to utility classes
- Typography styles as Tailwind components
- Restyled shadcn/ui components
Both the consumer site and contributor app import from this package. Changes to the design system propagate to both.