Dark-first, themeable via semantic tokens. Calm, warm, guilt-free. Intuition over arithmetic. No red. No shame.
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.ts → frontend/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.
--caution) at most.frontend/src/design/tokens.ts)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.
: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.
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.
| Band | Tone | Color | Numbers behind? |
|---|---|---|---|
| Very light / sehr leicht | Neutral — just had a light day | --sk (blue) | Yes, one tap |
| Light / leicht | Neutral-positive | --gn (green) | Yes, one tap |
| Balanced / ausgewogen ✓ | Positive — target zone | --gn (green) | Yes, one tap |
| Hearty / herzhaft | Neutral — that was a filling day | --wa (amber) | Yes, one tap |
| Rich / reichhaltig | Warm, 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.
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.
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.
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.
| Input | Pattern |
|---|---|
| Goals | Multi-select chips (canonical set → strategy) + free text — the only required step |
| Sex | Radio |
| Height | Compact input, minimal placeholder copy |
| Build | Radio chips without the word "build" (light / medium / sturdy); can coexist with exact weight |
| Daily movement | Radio: little / some / lots |
| Sport | Radio: none / occasionally / weekly / almost daily + sport-type multi-select beneath |
| Cooking | Radio: almost never / getting interested / regularly / love it |
| Nutrition knowledge | Radio: none / basics / solid / professional |
5 testers shown the band view unaided — ≥3/5 must understand what the band communicates without explanation. Required before Epic 2 UI is finalized.
| Use | Weight | Size | Tracking |
|---|---|---|---|
| Page title / h1 | 700 | 32–40px | −0.03em |
| Section heading / h2 | 700 | 12–13px | +0.06em, ALL CAPS |
| Card title | 700 | 16–20px | −0.02em |
| Body | 400 | 14–15px | normal |
| Muted / label | 400–500 | 12–13px | normal |
| Band label | 600 | 14px | normal |
| Editorial / tagline | 400 italic | 17–20px | normal (Newsreader) |
| Mono / code | 400 | 12–13px | normal |
| Do | Don'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 DE | Direct translation — DE needs different phrasing |
| Treat foods named neutrally — "pizza night" | Moralizing — "indulgence" / "cheat meal" |
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."
| Context | EN | DE |
|---|---|---|
| Balanced day | Looks like a balanced day so far. | Sieht bisher nach einem ausgewogenen Tag aus. |
| Low protein | A little more protein could make tomorrow feel steadier. | Etwas mehr Protein koennte den morgigen Tag stabiler machen. |
| Rich day | Looks like a hearty day. Tomorrow can simply be lighter. | Sieht nach einem herzhaften Tag aus. Morgen darf einfach leichter sein. |
| Medical context | This 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 ended | Your 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 | Pattern |
|---|---|
| Card | bg: --sf · border: --bd · radius: 14–16px · padding: 18–24px |
| Surface layer 2 | bg: --sf2 (nested cards, input bg) |
| CTA / primary button | bg: --ac · text: #1A201C (dark) · font-weight: 600 · radius: 12–14px |
| Secondary button | border: --bd · text: --tx · transparent bg |
| Input field | bg: --sf2 · border: --bd · focus: border --ac · radius: 10px |
| Tag / pill | bg: --sf2 · border: --bd · radius: 999px · text: --mu · size: 11–12px |
| Divider / border | 1px 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 |
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/
| Element | Duration | Easing |
|---|---|---|
| Card hover / focus | 150ms | ease-out |
| Band fill (on load) | 400ms | ease-in-out |
| Modal / sheet enter | 240ms | cubic-bezier(.25,.8,.25,1) |
| Processing state (meal log) | loop ~1s | Gentle pulse, --gn |
| Tip appear | 300ms fade | ease-in |
All animations must respect prefers-reduced-motion: reduce — disable or replace with instant transitions.
| Token | Value | Use |
|---|---|---|
space-xs | 4px | Icon gap, tight inline |
space-sm | 8px | List item gap, small padding |
space-md | 14–16px | Card padding, section gap |
space-lg | 24px | Page horizontal padding, section break |
space-xl | 40–44px | Section-to-section, hero margin |
| Border radius card | 14–16px | All cards |
| Border radius button | 12–14px | CTAs |
| Border radius pill | 999px | Tags, small labels |
emealia-app/docs/design/figma-exports/:root tokens above — use exact hex valuesscreen-01-demo.png, screen-02-today.png, … screen-09-auth.png