# Frontend Design Principles

How we design app frontends across the estate. **Consistency here is consistency of METHOD, not one identical look** — every app starts from the shared `createAppTheme` baseline and owns its identity overrides (principle 1); the shared surface (`ui-core-micha` theme, components, charts, layout) carries the structural consistency. hram is the worked example throughout, not a look other apps must copy.

These principles are the spec a **prototype** encodes and the implementation matches. `frontend-engineering` (build) and `ui_reviewer` (review) point here; the prototype workflow is the `frontend-design` skill.

## Identity

1. **Honor the shared baseline and the app's identity; don't invent a parallel system.** `ui-core-micha`'s `createAppTheme` deliberately owns the complete visual baseline. Each app supplies its identity through `palette.primary` and `fontFamily`, plus reviewed overrides where its real design requires them (hram: primary blue `#468AB2`). **Match first, then elevate.** Never invent a parallel language — a made-up teal / pills / breadcrumb vocabulary was the v1 mistake.
2. **One accent, deliberately placed.** Interactive = the app's one accent colour. **Data colours are a separate layer with a shared default that apps may override.** Status (green / amber / red) is semantic and is NEVER repurposed as a series colour. Restraint over decoration.
3. **Commit to the app's theme reality — with intent.** If the app is single-theme (hram is light-only), commit to that deliberately rather than bolting on dark mode as an afterthought — a decision, not an omission. The estate does NOT mandate light-only; it is per-app — but whichever it is, it is on purpose.

## Information design (it's a tool, not a document)

4. **Scan and operate, not read.** Summary before detail; state encoded in form (chips, knee-markers); interactive things look interactive.
5. **The user's language, not the system's.** Humanize labels (`incidence_scale` → "Incidence scale"); keep the raw name reachable on hover for the technicians — readability without losing traceability. Words are design material.
6. **Reduce help, don't remove it.** Explanation lives in on-demand affordances (ⓘ-popover, provisional-chip), not permanent banners — less noise, same information.
7. **Design the edge-states, not just the happy path.** An empty / "no frontier" state gets a real design (statement + CTA), never a bare alert.

## Charts (read by humans, executed by code)

8. **Every chart earns:** axis labels (with units), a legend only when >1 series, a tooltip, theme-token colours, container-responsive sizing, and empty/loading states. **Single-hue for magnitude; diverging (blue/red around zero) ONLY for signed data.** The concrete data palette is a separate layer with a shared `ui-core-micha` default that apps override when their domain calls for it.
   **Dual-axis is a default-off exception, opt-in only** (CHART-5, operator decision): the shared `TimeSeriesChart` preset supports a second y-axis via a per-series `axis: 'secondary'` field specifically for two series sharing one axis at very different scales/units (e.g. a user count vs. hours) where series-toggles alone leave one series unreadable. Reach for it only under that same condition, not as a general layout choice — a single shared axis (or toggles) stays the default everywhere else.
8a. **Axis titles: categorical axis gets none by default; numeric axis carries the quantity and unit, once.** A categorical axis' ticks are names (`"Session 1"`, ward names) — a title only restates their type; add one only when the ticks are codes or abbreviations whose kind is not self-evident. A numeric axis is meaningless without a title naming the quantity and its unit — unless the ticks already show the unit (`%`, a currency symbol) and the panel heading already names the quantity, in which case a title repeats both. **The unit belongs in the title once, never in every tick** (`"Accessibility (minutes) (min)"` over ticks `"0.0 min"` was measured live — the unit three times on one axis). This is a decision the caller makes per axis, not something the chart preset silently applies or suppresses (THEME-9) — a label the caller deliberately set is never auto-dropped.
8b. **Scatter: neutral by default; colour only when it encodes a real dimension.** An undifferentiated point cloud is one neutral mass (the role register's neutral, not a series/KPI-identity hue) — colour is earned by a genuine data dimension (a category, a continuous measure), never applied just to look intentional. `ScatterChart` (THEME-10, drawn from three real consumers — hram's allocation-performance, access-gap, and optimization-results panels) supports three modes: neutral (default, single series, no colour set), categorical (multiple series, one colour per series, discrete legend — draw the colours from the app's own family/role register, not a hand-indexed array), and continuous (the caller resolves a per-point colour from its own scale and applies it via `getPointStyle`; this package does not ship a colour-scale/legend component). Bubble sizing (`sizeAccessor`) always draws largest-first — an unordered bubble chart with no defined draw order is silently wrong, not a stylistic choice. Individually marked points (a hollow status-quo point against filled candidates) go through the same `getPointStyle` hook. Reference geometry (a computed curve, a labelled reference line) is composed as `children`, reading the chart's own live scale (`ScatterReferenceCurve`/`ScatterReferenceLine`) rather than a second, hand-rolled pixel mapping. **Not this preset:** row-based dot plots (a forest plot, a tornado) — categorical y-axis, 150–220px of row labels, row-density as the layout problem — are a different chart family with their own preset, not a scatter with a tall left margin.
9. **Ground it in the real subject matter.** Faithful structures from the real panels (hram: Morris-tornado, Pareto/knee, CI-strip, allocation-scatter with shape-encoded sources) — no lorem, no generic charts. Hence: **scout the real subject before building.**

## Craft

10. **Prototype = spec, parity = guardrail — both directions, within a DECLARED coverage.** Purely visual/UX — function stays; **"No behaviour / permission / data-contract change"** is the hard condition in the WO. Prototypes are routinely **partial** (tabs undrawn, flows stubbed), and nobody downstream can tell a deliberate omission from a required removal — so **prototype silence never authorizes a removal; the Envelope's "Replaces / removes" list does.** Every prototype therefore **declares what it covers and what it leaves untouched**. Inside the covered area each omission is resolved at authoring time into *removes* or *deliberately keeps*, leaving nothing ambiguous; outside it the prototype says nothing. Parity is then checked both ways **within that coverage**: everything drawn is present, and nothing listed as removed survives. **Inventory is not enough.** Presence and absence say nothing about weight, density or colour — an implementation can pass this check completely and still read heavier and older than the prototype. Parity therefore also requires a **rendered side-by-side at two widths** before the change lands (the Orchestrator's — `AGENTS.md` → Reviews), and the token values themselves come from the artifact's `<style>` block via the Envelope's token delta list. **A redesign that ends up net-additive has failed**, however good its new parts — shipping MORE UI than before is the estate's recurring cleanup failure (status bands, duplicated badges, tip text layered on top of the very legacy the redesign was meant to replace).
11. **Align to the shipped reality.** When related features land, pull the prototype onto them so it is a truthful spec, not a contradicting vision.
12. **Avoid the generic "AI look"; take details seriously.** No cream / serif / neon default; embed the real font (CSP-safe as a data-URI, not a silent-fallback risk); `tabular-nums` for aligned numbers; a11y (role/aria on chart regions, focusable info). **Template tells to CUT** (unless the app's design system genuinely specifies them): monospace-uppercase **eyebrow / kicker labels** (e.g. `IMARA · ADVANCING RESILIENT HEALTH SYSTEMS`), decorative **pills / badges**, `FIG. 01 ·` / `F.01` **figure captions**, `↳`-prefixed **micro-links**, **oversized display** headlines, **sketchy / hand-drawn** styling, and **explanatory paragraphs that restate the UI** (help is on-demand — see #6). Restraint is the default; each of these is added only with a real reason.

For the **design craft** of a prototype (an HTML artifact) and its charts, load Claude's built-in **`artifact-design`** (design fundamentals) and **`dataviz`** (charts) skills and apply them WITHIN these principles — this doc adds the estate method (honor the app's OWN tokens, restraint), it does not replace that craft, and it must not crowd it out (load both). Do NOT default to **`swisstph-design`**: it imposes the Swiss-TPH house brand and conflicts with #1 (honor the app's own tokens).

## Also — structural dimensions

13. **Spacing / density = tool density, not document width.** A defined spacing rhythm, dense enough to scan and operate — not airy like a reading document. Use the theme's spacing scale; don't hand-pick pixel gaps.
14. **Responsive = container-sized, not fixed px.** Layouts and charts size to their container; define breakpoint behaviour deliberately (what stacks, what hides, what scrolls) — the same container-responsive rule the charts already follow.
15. **Motion = restrained and functional.** Transitions serve orientation/feedback (state change, reveal), never decoration. Short, few, purposeful; respect `prefers-reduced-motion`.
16. **Prefer icons for secondary actions where the meaning is conventional.** A dense action row reads better as one labelled primary action plus **icon buttons** (preview, download, edit, delete, overflow) than as a row of text buttons — and a labelled `Button` is simply the cheapest thing to type, so implementations drift toward text-heavy rows the prototype never had. Conditions: use an **established glyph** (never invented iconography); always pair it with a **`Tooltip` AND an `aria-label`**, since an icon-only control otherwise has no accessible name; and **keep the text label** when the action is primary, destructive without a confirm step, or app-specific enough that no conventional icon exists. A tooltip may NAME an action — if it has to explain a concept, the control needs a label, or on-demand help (#6).
