The revised corporate identity, the band system, and the copy standards. Tokens here mirror the shipped app exactly — emealia-app/frontend/src/design/tokens.ts is the single source of truth, a build lint fails on any hard-coded colour in a component, and a drift guard fails on any mismatch between the token module and the generated CSS.
The original forest-green ground (#1A201C / #232B26) was replaced with a cool blue-grey ground (#11161B / #1C242C). The peach accent #F0956A is retained and is now the anchor the whole palette is built around. The reason is functional, not aesthetic: the darker, cooler ground raises band-fill contrast, which matters because the band fill is the primary information carrier on the main view. emealia-mgmt/3-design.md still documents the old palette and needs updating.
| # | Rule |
|---|---|
| 1 | No red in nutrition feedback — ever. Warm amber is the maximum urgency signal. |
| 2 | No numbers on the main daily view. Bands and qualitative labels only. Numbers one tap away. |
| 3 | No state may look like a failure. Technical errors may be transparent; nutrition states may not. |
| 4 | Dark mode only. No light mode planned. A light token set exists behind the same semantic variables so components consume meaning rather than dark assumptions — it is an architectural hedge, not a shipped theme. |
| 5 | No loading spinners on the main view. Optimistic UI. |
| 6 | Logging is single-tap accessible from the main view. Never buried, never covering content. |
--text-muted is a lighter mix of the band marker toward the text colour, chosen to hold AA contrast on the dark ground. --band-marker draws the target-zone boundary on a band — a mid blue-grey that reads against both the dark track and the bright category fills, painted above the fill so the target is never occluded.
There is no red token in the palette. --caution is the maximum urgency signal in nutrition feedback, and even that is used sparingly. This is not a style preference — it is regression-locked by story 2.4, whose test suite sweeps the whole band-state space and asserts a category colour is used in every state, never a semantic one.
A band's fill identifies which nutrient it is. It never changes with status. This is the mechanism that makes "no red" structural rather than merely careful — a band cannot signal failure because its colour carries no status information at all.
The band is emealia's core UI primitive — it replaces a calorie number with a qualitative sense of the day. Colour and label only, no numbers by default.
| Level | EN | DE | Tone |
|---|---|---|---|
| very_light | Very light | Sehr leicht | Neutral |
| light | Light | Leicht | Neutral-positive |
| balanced | Balanced | Ausgewogen | Positive — the target zone |
| hearty | Hearty | Herzhaft | Neutral — a filling day |
| rich | Rich | Reichhaltig | Warm, never shame |
An unlogged day returns none, never "very light" — absence is not a verdict.
Each nutrient line carries a text label from a closed set of five calm backend states: empty · getting_started · room_to_round_off · on_track · treat_today. The backend maps percentages of the target range to a state and sends the opaque key — never a translation key, never a percentage. The frontend owns the copy.
It is the same pattern as the tip library: the mechanism knows the rule shape and never what a key means. Meaning is decoupled from mechanism, so the whole state space can be swept in tests — all 2⁴ geometries classify to a calm state, the classifier never escalates, above_zone is the ceiling no matter how far over, and an EN + DE copy deny-list proves no state reads as a failure. 23 backend and 23 frontend assertions.
This is the sharpest rule in the design system and it was a real decision, not an inherited convention. No day-budget or contribution band ever appears on a meal screen — that framing reads as "calories to spend down", which is the food-as-currency mechanic the product exists to reject. Meal screens show the meal's own honest number ranges (~700–860 kcal, muted, marked "estimate"). The daily view stays bands-only.
The contribution panel obeys the same rule from the other direction: it shows shares as bar widths, never as percentage text, so expanding a band on Today does not put a number on the main view.
Today starts with five default bands. Any of the 32 tracked nutrients, vitamins or minerals can be starred onto Today from All-details, up to a cap of 10. Starred values pin first within their section and re-order live. Everything else stays one tap away in All-details.
| Use | Family | Weight | Size | Tracking |
|---|---|---|---|---|
| Page title / h1 | Hanken Grotesk | 700 | 32–40px | −0.03em |
| Section heading / h2 | Hanken Grotesk | 700 | 12–13px | +0.06em, all caps |
| Card title | Hanken Grotesk | 700 | 16–20px | −0.02em |
| Body | Hanken Grotesk | 400 | 14–15px | normal |
| Muted / label | Hanken Grotesk | 400–500 | 12–13px | normal |
| Band label | Hanken Grotesk | 600 | 14px | normal |
| emealia's own voice | Newsreader italic | 400 italic | 17–20px | normal |
| Mono / code | SF Mono, Menlo | 400 | 12–13px | normal |
Newsreader italic is reserved for emealia speaking as itself — the welcome line, the assumptions ("I estimated assuming…"), the tagline. It is never used for interface chrome. Fonts are self-hosted as woff2; the Google Fonts CDN is a development convenience only and must not ship to production for GDPR reasons.
emealia
e + meal (in --brand-meal) + ia — "meal" is always coloured.[ ɛ-ˈmiːl-jə ] — "eeh-MEE-lyah".| Component | Pattern |
|---|---|
| Card | bg --surface · border --border · radius 14–16px · padding 18–24px |
| Nested surface | bg --surface-2 (nested cards, inputs) |
| Primary button | bg --accent · text --on-fill · weight 600 · radius 12–14px |
| Secondary button | border --border · text --text · transparent bg |
| Input | bg --surface-2 · border --border · focus border --accent · radius 10px |
| Tag / pill | bg --surface-2 · border --border · radius 999px · text --text-muted |
| Success state | --success as text or border accent — never as a fill |
| Caution state | --caution — maximum urgency, never red |
| Navigational link | Text plus a trailing arrow, everywhere and identically: "All details →", "Choose from your favorites →", "all assumptions ▾" |
| Disabled button | A defined visual state, used when re-estimate is spent (1× cap) |
| Tap target | ≥44px, always |
| Element | Duration | Easing |
|---|---|---|
| Card hover / focus | 150–180ms | ease-out |
| Band fill on load | ~380–400ms | spring — cubic-bezier(.22, 1, .36, 1) |
| Modal / sheet enter | 240ms | cubic-bezier(.4, 0, .2, 1) |
| Processing state (meal log) | ~1s loop | gentle pulse in --success |
| Tip appear | 300ms fade | ease-in |
All animation respects prefers-reduced-motion: reduce.
| 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 könnte 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 für deinen nächsten Termin. |
| Trial ended | Your trial has ended. Your history stays available. | Dein Testzeitraum ist abgelaufen. Dein Verlauf bleibt verfügbar. |
| Contribution, missing values | Some meals have no value for this nutrient yet. | Für einige Mahlzeiten gibt es dazu noch keinen Wert. |
| Contribution, empty | Nothing logged for this yet. | Dazu ist noch nichts erfasst. |
Never: "you failed" · "cheat meal" · "bad food" · "you must" · "doctor-approved" · an exact calorie verdict on Today. The one-tip rule: each day produces at most one forward-looking suggestion. Not a list. Not a score. One nudge, warmly phrased.
Bilingual parity means the same warmth, not the same words. German copy is written to sound native — the register that makes "Morgen darf einfach leichter sein" land is not reachable by translating "Tomorrow can simply be lighter" literally.
| Input | Pattern |
|---|---|
| Goals | Multi-select chips plus 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 be given alongside an exact weight. |
| Daily movement | Radio: little / some / lots |
| Sport | Radio: none / occasionally / weekly / almost daily, with a sport-type multi-select beneath |
| Cooking | Radio: almost never / getting interested / regularly / love it |
| Nutrition knowledge | Radio: none / basics / solid / professional |
other, so there is no null state to caveat.Draft — pending legal counsel (H10). A calm reassurance, never a warning label: no red, no alarm, no legalese wall on the happy path.
| Placement | Copy |
|---|---|
| Registration beside the ToS acceptance |
"emealia is a wellness companion, not a medical service. It doesn't give medical advice, diagnoses, or treatment — for anything health-related, your doctor decides." "emealia ist ein Begleiter für dein Wohlbefinden, kein medizinischer Dienst. Es gibt keine medizinische Beratung, Diagnose oder Behandlung — bei allem Gesundheitlichen entscheidet deine Ärztin oder dein Arzt." |
| Free-text context field onboarding and profile, inline |
"You can tell emealia anything that helps it understand how you eat — including something your doctor suggested. It keeps it as gentle context and won't act on it as medical advice, and will never override your doctor." "Du kannst emealia alles erzählen, was hilft zu verstehen, wie du isst — auch etwas, das deine Ärztin oder dein Arzt dir geraten hat. Es behält es als behutsamen Kontext, handelt nicht danach wie eine medizinische Anweisung und stellt sich nie über den Rat deiner Ärztin oder deines Arztes." |
Rules: never make a disease-treatment or prevention claim anywhere (this is what keeps emealia outside EU MDR and German HWG); never say "clinically proven", "accurate calories" or "doctor-approved"; a medical note is framed as something to mention at your next appointment, never as an instruction. The long-form disclaimer lives on Settings → Legal and in the ToS. AI tips carry no per-tip banner — safety lives in a versioned medical_safety prompt snippet.
| Token | Value | Use |
|---|---|---|
--sp-xs | 4px | Icon gap, tight inline |
--sp-sm | 8px | List item gap |
--sp-md / --sp-lg | 12 / 16px | Card padding, section gap |
--sp-xl | 24px | Page horizontal padding |
--sp-2xl / --sp-3xl | 32 / 48px | Section-to-section, hero margin |
--r-lg / --r-xl | 14 / 16px | Cards |
--r-md / --r-lg | 12 / 14px | Buttons |
--r-pill | 999px | Tags, chips, small labels |
--maxw-screen | 640px | App content width |
src/design/tokens.ts is the only place a raw colour or size value may live.scripts/generate-tokens.mjs generates src/assets/tokens.css from it.--surface, --text, --accent), never literals.scripts/check-tokens.mjs fails the build on any hard-coded colour or size in a component and on any drift between the token module and the generated CSS.This documentation set's docs.css mirrors those values by hand. When the app palette changes, update it here too.