Design System

Dark-first, themeable via semantic tokens. Calm, warm, guilt-free. Intuition over arithmetic. No red. No shame.

Corrected below

This page previously listed an older, unshipped token set (--bg:#1A201C, --sf/--sf2, no category colors) and said "dark mode only." The real, generated tokens live in frontend/src/design/tokens.tsfrontend/src/assets/tokens.css (do not hand-edit the CSS file — it says so at the top). emealia is dark-first, not dark-only: a full light theme exists behind :root[data-theme="light"] as long as the no-red/no-failure and WCAG contrast rules still hold. The tables below reflect the generated tokens.

Design non-negotiables

Color tokens (dark — the default; generated from frontend/src/design/tokens.ts)

--bg
#11161B
--surface
#1C242C
--surface-2
#26313B
--border
#33414D
--text
#EBF1F6
--text-muted
#93A6BB
--accent
#F0956A · accent / CTA / focus ring
--brand-meal
#F3B89E · "meal" in wordmark
--success
#8FD3B0
--info
#93C3E6
--caution
#DDB85C · max urgency signal

Category colors (band fills — fixed per nutrient, never a value judgment)

--cat-energy
#F0956A
--cat-protein
#93C3E6
--cat-carbs
#DDB85C
--cat-fat
#C4A882
--cat-fiber
#8FD3B0
--cat-vitamin
#C49FC8
--cat-mineral
#B9C77E
--cat-other-nutrient
#C4A882
--band-marker
#607790 (dark) / #687789 (light) · drawn above the fill so the target zone is never occluded
Critical rule

Never use any red-family color for nutrition feedback. --caution (amber) is the maximum urgency signal. The warm palette is intentional: success = good, caution = okay, never red.

Light theme (also shipped)

:root[data-theme="light"]: --bg:#F7F2EA · --surface:#FFFFFF · --text:#1A201C · --accent:#B85633 · --brand-meal:#9D4F33 — darker/higher-contrast variants of the same palette, still no red-family hue, still WCAG AA.

Band system

The band is emealia's core UI primitive. It replaces a calorie number with a qualitative sense of the day. Bands use color + label only — no numbers visible by default. Today starts with the 5 prototype bands, but users can personalize the Today band set in Profile from any tracked nutrient, mineral, or vitamin. Today is capped at 10 bands; everything else remains one tap away in All details.

Very light
Very light/ sehr leicht
Light
Light/ leicht
Balanced ✓
Balanced/ ausgewogen
Hearty
Hearty/ herzhaft
Rich
Rich/ reichhaltig
BandToneColorNumbers behind?
Very light / sehr leichtNeutral — just had a light day--sk (blue)Yes, one tap
Light / leichtNeutral-positive--gn (green)Yes, one tap
Balanced / ausgewogen ✓Positive — target zone--gn (green)Yes, one tap
Hearty / herzhaftNeutral — that was a filling day--wa (amber)Yes, one tap
Rich / reichhaltigWarm, never shame — you enjoyed it--ac (orange)Yes, one tap

Scope note: the five-level scale above is the day level vocabulary — one derived label for a whole day, used for History calendar dots and the statistics distribution. It is not the fill color of the per-nutrient bands on Today: those always keep their fixed category color (--cat-*), and band status is conveyed by text label only.

Band status labels on Today (shipped — core/band_states.py)

Each nutrient line carries an opaque state key from the versioned rule table BAND_STATE_RULES: empty · below_zone_early · below_zone_late · within_zone · above_zone (an earlier prototype draft used getting_started · room_to_round_off · on_track · treat_today — superseded). The evaluator (classify_band_state) is a generic first-match over objective BandFacts (no_data, end_below_zone, start_above_line, after_pivot) — it knows the rule *shape*, never what a key means; copy lives only in the frontend locale map (states.empty = "nothing yet", below_zone_early = "off to a good start", below_zone_late = "room to round off", within_zone = "right on track"). below_zone_early vs. _late is decided by a data-defined pivot time (LATE_DAY_PIVOT = 15:00). Gap: the shipped locale files (frontend/src/locales/*.json) are missing copy for the above_zone state and carry an extra no_reference key not produced by BAND_STATE_RULES — a real drift between backend and frontend to fix at the source. The line is always 100% wide (open end — over-target clips at the line end); the subtle range bar is the user's saved target zone; the filled bar is the day's accumulated min/max estimate.

Meal rows (Today + History)

AI-generated title (≤60 chars) + AI-generated tag pill (≤24 chars) + user-selected category (breakfast / lunch / dinner / snack / beverage — only when chosen) + time in the user's timezone and region format + heart (one identical favourite toggle in both places). Today shows at most 5 meals; more → a "more" link to History.

Today header

Bands-mark icon horizontally aligned with the wordmark · beneath it "Welcome back, {username}" in Newsreader italic (the companion voice) · profile icon right-aligned, height-matched · no day/date/time line. The log button stays visible while scrolling and never covers content.

Copy & CTA standards

Onboarding input patterns

InputPattern
GoalsMulti-select chips (canonical set → strategy) + free text — the only required step
SexRadio
HeightCompact input, minimal placeholder copy
BuildRadio chips without the word "build" (light / medium / sturdy); can coexist with exact weight
Daily movementRadio: little / some / lots
SportRadio: none / occasionally / weekly / almost daily + sport-type multi-select beneath
CookingRadio: almost never / getting interested / regularly / love it
Nutrition knowledgeRadio: none / basics / solid / professional
Gate G1

5 testers shown the band view unaided — ≥3/5 must understand what the band communicates without explanation. Required before Epic 2 UI is finalized.

Typography

--fn display
700 · −0.03em
Hanken Grotesk
--fn body
400 · normal
Body text — readable, warm, neutral. No serifs in UI chrome.
--vo editorial
italic · 400
"Your day in one glance."
Code / mono
SF Mono, Menlo
claude-haiku-4-5 · 202 accepted
UseWeightSizeTracking
Page title / h170032–40px−0.03em
Section heading / h270012–13px+0.06em, ALL CAPS
Card title70016–20px−0.02em
Body40014–15pxnormal
Muted / label400–50012–13pxnormal
Band label60014pxnormal
Editorial / tagline400 italic17–20pxnormal (Newsreader)
Mono / code40012–13pxnormal

Logo & wordmark

Wordmark
emealia — "meal" colored in --bm (#F3B89E), rest in --tx
Pronunciation
[ ɛ-ˈmiːl-jə ] — "eeh-MEE-lyah"
Spacing
Letter-spacing −0.5px for the wordmark at nav scale
Sizes
Nav: 17–18px · Hero: 40–56px · App icon: square, no padding needed
Don'ts
Never capitalize. Never use light background. Never use logo without the --bm color on "meal".
App icon
Dark bg (#1A201C) with stylized "em" or wordmark — Figma source: emealia-app/docs/design/figma-exports/

Voice & tone

DoDon't
Calm, warm, peer — "looks like a balanced day so far"Clinical, prescriptive — "you need 20g more protein"
Forward-looking — "tomorrow you could…"Backward-judgment — "you ate too much"
Curious — "how did that feel?"Alarming — "warning: over your limit"
Honest about ranges — "roughly 400–550 kcal"False precision — "423 kcal"
Bilingual parity — same warmth in EN and DEDirect translation — DE needs different phrasing
Treat foods named neutrally — "pizza night"Moralizing — "indulgence" / "cheat meal"
One-tip rule

Each day produces exactly one AI-generated forward-looking tip. Not a list. Not a score. One nudge, warmly phrased. Example: "Tomorrow, a handful of nuts in the morning could balance today's lighter start."

Microcopy examples

ContextENDE
Balanced dayLooks like a balanced day so far.Sieht bisher nach einem ausgewogenen Tag aus.
Low proteinA little more protein could make tomorrow feel steadier.Etwas mehr Protein koennte den morgigen Tag stabiler machen.
Rich dayLooks like a hearty day. Tomorrow can simply be lighter.Sieht nach einem herzhaften Tag aus. Morgen darf einfach leichter sein.
Medical contextThis is not medical advice; use it as a tracking note for your next appointment.Das ist keine medizinische Beratung; nutze es als Notiz fuer deinen naechsten Termin.
Trial endedYour trial has ended. Your history stays available.Dein Testzeitraum ist abgelaufen. Dein Verlauf bleibt verfuegbar.

Avoid: "you failed", "cheat meal", "bad food", "you must", "doctor-approved", exact calorie verdicts on Today.

Component patterns

ComponentPattern
Cardbg: --sf · border: --bd · radius: 14–16px · padding: 18–24px
Surface layer 2bg: --sf2 (nested cards, input bg)
CTA / primary buttonbg: --ac · text: #1A201C (dark) · font-weight: 600 · radius: 12–14px
Secondary buttonborder: --bd · text: --tx · transparent bg
Input fieldbg: --sf2 · border: --bd · focus: border --ac · radius: 10px
Tag / pillbg: --sf2 · border: --bd · radius: 999px · text: --mu · size: 11–12px
Divider / border1px solid --bd always
Success state--gn (green) text or border accent — never as background fill
Caution state--wa (amber) — max urgency, never red
Muted text--mu for secondary info; --tx for primary

Screen inventory (9-screen prototype)

Screen 1
Pre-signup demo
Hardcoded day view. Value before registration. Zero AI calls.
Screen 2
Today view
Configurable bands (default 5, max 10) + meal list (≤5, "more" → History). One-tap log access. Daily tip. No numbers by default. Header: aligned icon + wordmark, italic welcome line, no date line; log button always visible.
Screen 3
Meal detail (stored)
Already-saved meal. Merged assumptions/ingredients (≤5 dominating tags + meal size, "all assumptions" toggle). Meal's honest number-ranges (no day-budget band). Edit / confirm.
Screen 4
Log a meal
"Log a meal" + "use your own words" subtext. "Log for" date picker (Today/Yesterday/date; backdate ≤1 week, no future). Meal type (breakfast·brunch·lunch·dinner·snack·beverage·other; never "optional"; unpicked → other). Meal-type-based suggestions. "Choose from your favorites →" (keeps date+type). Growing 3→6-line input. Log now → estimating.
Screen 4b
Meal log detail (pre-save)
Draft estimate preview. Favourite + meal-type tag ("Change") + title on top. "I estimated assuming…" (Newsreader italic, ≤5 assumptions + "all assumptions"). Re-estimate box (max 1×, then disabled). No day-budget band. "Save log →" persists — nothing saved before that.
Screen 5
History
Calendar view. Day-band dots. Tap day → Today view for that day.
Screen 6
Stats
7/30-day trends. Band distribution. Macro averages. No red ever.
Screen 7
Profile
Goal, reference params, optional weight tracking, dietary preferences + free-text context (inline medical-info notice: kept as context, never acted on as medical advice), Today band personalization.
Screen 8
Trial / Paywall
Trial ended. Warm tone — not punitive. Annual default. Monthly toggle.
Screen 9
Auth
Social-only sign-in, no passwords. Shipped: Google (GoogleAuthBoundary, story 7.1a). Planned: Apple Sign-In (7.1b). Minimal — no custom credential UI, the provider's own consent screen handles it. Registration carries the passive medical-disclaimer notice beside the ToS/Privacy acceptance.

Medical / general-wellness disclaimer (DRAFT · legal counsel H10): emealia is a general-wellness / informational product — not a medical device, not medical advice. A calm passive notice appears at registration (beside the ToS acceptance) and inline under the free-text preferences/context field ("kept as respectful context, never acted on as medical advice; doctor guidance welcome as context"). No disease-treatment/prevention claim appears anywhere (keeps us outside EU MDR / German HWG). Full research + EN/DE copy: emealia-app/docs/research/medical-disclaimer.md; build story 13.14.

Prototype: emealia-web/public/prototype/ · Figma exports: emealia-app/docs/design/figma-exports/

Animation & motion

ElementDurationEasing
Card hover / focus150msease-out
Band fill (on load)400msease-in-out
Modal / sheet enter240mscubic-bezier(.25,.8,.25,1)
Processing state (meal log)loop ~1sGentle pulse, --gn
Tip appear300ms fadeease-in

All animations must respect prefers-reduced-motion: reduce — disable or replace with instant transitions.

Spacing scale

TokenValueUse
space-xs4pxIcon gap, tight inline
space-sm8pxList item gap, small padding
space-md14–16pxCard padding, section gap
space-lg24pxPage horizontal padding, section break
space-xl40–44pxSection-to-section, hero margin
Border radius card14–16pxAll cards
Border radius button12–14pxCTAs
Border radius pill999pxTags, small labels

Figma references

Exports location
emealia-app/docs/design/figma-exports/
Color tokens
Figma variables mirror the CSS :root tokens above — use exact hex values
Handoff
All screens exported as PNG + SVG · No Zeplin · Figma links shared as needed
Naming
screen-01-demo.png, screen-02-today.png, … screen-09-auth.png