# bake-ops — design system

The design language for **bake-ops**, a working bakery's daily tool. It replaces
paper recipes and guesswork: staff look up recipes, scale batches and tick off
bakes; the owner gets low-stock alerts and manages settings; the complex work —
recipe editing, supplier catalogues, costing — happens at a desk.

It should feel like **dependable professional kit with the warmth of a
bakehouse**. Not a startup dashboard. Not a consumer app. Not cute.

This project contains foundations and a component library only. Screens will be
designed from it in a follow-up; there are deliberately no UI kits yet.

## Sources

| What | Status |
| --- | --- |
| Written product brief (surfaces, principles, component list, real content) | Supplied in-conversation and treated as the source of truth |
| Codebase / repo | **Mentioned by the requester but not attached.** Nothing was read from code. If a repo exists, import it and this system should be reconciled against it. |
| Design docs / PRD | **Mentioned but not attached.** Same caveat. |
| Logo files | None supplied — the wordmark is type-only (see BRAND). |
| Icon set | None supplied — Lucide is a documented substitute (see ICONOGRAPHY). |
| Font binaries | None supplied — Archivo + IBM Plex load from the Google Fonts CDN (see VISUAL FOUNDATIONS). |

## The three surfaces

Every component states which surface it belongs to. Read `guidelines/brand-surfaces.html`
for the full table.

- **Kiosk (primary)** — landscape tablet, wall-mounted in the bakehouse. Read at
  arm's length with floury hands in a bright room. Touch targets 56px minimum
  (44px absolute floor), primary actions 72px, body text 16px floor and 20px
  preferred. No hover-dependent and no swipe-only interactions.
- **Phone** — the manager's own device, portrait. Notifications, quick stock
  checks, receiving deliveries, admin settings. 44px targets.
- **Desktop** — dense, keyboard and mouse. Recipe editor, supplier catalogues,
  planning calendar, cost analysis. 36px targets, 28px in table rows.

## Non-negotiable principles

1. **Numbers are the hero.** A baker reads quantities at a glance across a
   bench. Quantities, multipliers and yields get the largest, boldest treatment
   on any screen they appear on, always in the display face with tabular
   figures so columns align. `NumberReadout` is the house treatment; the
   `--fs-display-*` sizes exist for this and nothing else.
2. **Section colour is navigation, not decoration.** Staff learn "orange means
   recipes". Five sections, five hues, each used for that section's fills,
   active states and accents — and nowhere else. Status colour is kept
   separate. Danger is reserved for destructive or urgent, never decoration.
3. **Warm but engineered.** Cream and flour surfaces, dark-roast ink, stone
   accents — that is the Bakehouse variant; the character survives a re-skin
   because every token is named for its role, not its colour. Generous
   whitespace, confident hierarchy, zero decorative clutter.
   No gradients. No shadow beyond a subtle card lift.
4. **Minimal typing on the kiosk.** Taps, presets and big numeric keypads. A
   text field on a kiosk screen needs a justification.

---

## CONTENT FUNDAMENTALS

**Voice.** Plain and operational, with the specificity of a person who works
there. Not chatty, not corporate, never enthusiastic. The app tells you what is
true and what it will do about it.

**Person.** Second person for instructions ("Weigh the flour first"), no first
person plural — bake-ops never says "we". Staff are named by first name, the way
the bakehouse talks: "counted by Steve", not "S. Adeyemi" or "User 4".

**Casing.** Sentence case everywhere. Uppercase only for micro-labels
(`--fs-micro`, 0.08em tracking) such as ORDER TOTAL and YIELD AT 2.5&#215;.
The product name is always lowercase and hyphenated: **bake-ops**. Never
"BakeOps", "Bake Ops", "BAKE-OPS".

**Numbers in copy.** Always numerals, always with the unit, always the real
figure. "Bread flour is down to 1 bag", not "stock is low". Multipliers use the
multiplication sign: 2.5&#215;. Money to two decimals: $2.80/kg.

**Ordering language is the bakery's, not the database's.** Flour is bought as
"1 &#215; 25KG bag". Orders round up to whole bags — the app says "order 3 bags",
never "add 75kg". Pack formats are quoted verbatim from the supplier, including
their capitalisation.

**Buttons say what happens.** "Confirm and receive", "Mark bake done", "Count it
now", "Order 3 bags". Never "Submit", "OK", "Continue" or "Save" where something
more specific is true.

**Empty and error states name the thing and the next step.** "No bakes planned
for Thursday yet" not "No data available". "That PIN did not match. Try again."
not "Authentication failed".

**Punctuation.** No exclamation marks anywhere. No emoji anywhere — this runs in
a commercial kitchen and gets read at speed. Em dashes are fine in prose,
middots separate metadata ("1 &#215; 25KG bag &#183; $2.80/kg").

See `guidelines/content-voice.html` for the say/don't-say table.

---

## VISUAL FOUNDATIONS

### Colour

bake-ops gets re-skinned: a bakery brings its own brand colours, and a baker
starting at 3am needs low glare. Two rules make that survivable, and they are
not negotiable.

**1. Tokens are named by role, never by appearance.** A component asks for
`--surface-raised`, `--text-primary`, `--section-recipes-fill`. It never asks for
cream, clay, stone or `--ink-1` — those stop being true the moment the theme
changes. The warm names live in the comments of `tokens/colors.css` and nowhere
else. See `guidelines/colors-naming.html`.

**2. Every fill has a paired on-colour.** Anything that can carry text ships the
text colour that is legible on it: `fill` / `on-fill` and `tint` / `on-tint`.
Nothing puts `--text-primary` on a coloured fill and hopes. See
`guidelines/colors-on-pairs.html`.

Surfaces are five roles: `--surface-page`, `--surface-raised` (cards, sheets),
`--surface-sunk` (wells, inputs, table stripes), `--surface-muted` (disabled
fills), `--surface-inverse` (+ `-raised`) for chrome bars, plus
`--surface-scrim`. Note that *inverse* means "the high-contrast chrome bar", not
"dark" — in Night shift it resolves darker than the page rather than lighter.
Text is a five-step ladder by job: `--text-primary`, `--text-body`,
`--text-secondary`, `--text-muted`, `--text-disabled`, and
`--text-on-inverse` / `--text-on-inverse-muted` for the two inverse surfaces.
Borders are `--border-subtle` / `-default` / `-strong` / `-inverse`.

Five section hues, all defined in oklch so they sit at comparable lightness and
read as a family: **Home** slate — the quietest, lowest-chroma hue, because Home
is the overview — **Tasks** blue, **Recipes** crust orange, **Inventory** teal,
**Suppliers** moss green. Each has five role tokens: `fill` (fill and active
state), `on-fill` (text on that fill), `tint` (pale surface), `on-tint` (text on
the tint, and section-coloured text on the page), `edge` (border on tint).

Components read `--section-fill` and friends, which a `[data-section="recipes"]`
wrapper rebinds. That is what enforces principle 2 — put the attribute on a
section root and everything inside is correct by construction.

Section colour answers one of two questions, and which one decides the token:

- **Where you are.** Chrome, surfaces, tints, focus rings, headings: read
  `--section-*` and inherit. This is almost everything.
- **Where this goes.** A control that navigates *to* another section may name
  that section's hue — `SupplierLink`'s linked row is suppliers green inside an
  inventory screen, because tapping it goes to a supplier product. A recipe
  reference inside a task stays recipes orange; an ingredient reference inside a
  recipe stays inventory teal.

Decoration never borrows a section hue. Naming one is a claim that the element
navigates there, and the component's `.prompt.md` has to say where and why. Carry
it on a **fill** — section tints are too close together to tell apart, so
`SupplierLink` puts suppliers green on its icon disc and leaves the ground
neutral.

That licence has a cost: hues now sit inside each other, so **cross-section pairs
need contrast checking too** — suppliers green on an inventory teal tint, in all
three themes. Each section against its own background is no longer sufficient.

Status colours (`--status-success|warning|danger|info|neutral-*`, same five
roles) are deliberately different in character from the section hues: success is
a cleaner, brighter green than the earthy Suppliers moss; warning is amber, well
away from the crust orange — and amber rather than gold because a solid warning
fill has to clear 3:1 against the page as well as 4.5:1 against its own text.
Warning is the one token whose `on-fill` is dark ink in every theme.
`--status-info-*` is the one alias of a section hue, because "in progress"
genuinely *is* a task state.

**Danger is allowed in four cases only** — destructive actions, out of stock,
shortfall on a scaled batch, and urgent. See `guidelines/colors-danger-rule.html`.

#### Theme variants

Three variants are decided, and every one of them redefines the same token
names, so no component changes:

| Variant | Selector | What it is for |
| --- | --- | --- |
| **Bakehouse** | `:root` | The warm default. Flour surfaces, dark-roast ink. |
| **High contrast** | `[data-theme="high-contrast"]` | Sunlit shop floor: whiter paper, deeper ink, visible borders, darker section fills. |
| **Night shift** | `[data-theme="night-shift"]` | Pre-dawn, low glare. No pure-white text (primary caps at L 0.905, warm), no pure-black surface; fills lift and every on-colour flips to dark ink. |

In all three, every text pair clears **WCAG AA 4.5:1** (`--text-disabled` is the
one deliberate exception and never carries meaning), solid fills clear 3:1
against the page, and the five section *fills* stay at least ΔE 0.09 apart in
oklab so section colour still works as navigation. Their **tints do not** — the
closest pair falls to 0.017, close enough to read as one colour side by side,
which is why a cross-section reference carries its hue on a fill and never on a
tint. Bakehouse is the binding variant, not Night shift. Measured figures per variant are in
`guidelines/colors-contrast.html`; the variants sit side by side in
`guidelines/colors-themes.html`.

Shadow colour is themed too (`--shadow-1/2/3`) — a 7% warm brown in the light
variants would be invisible in Night shift, so there it is near-black and
stronger.

**Theme switching is not built here.** The token values are the design decision;
applying them — root attribute, class, generated stylesheet, `prefers-color-scheme`
— is the codebase's call.

### Type

| Role | Face | Notes |
| --- | --- | --- |
| Display / hero numbers | **Archivo** 600–700 | `--tr-display` (-0.03em), `--lh-display` (0.92), tabular |
| Headings h1–h2 | **Archivo** 600 | `--tr-tight` |
| h3, body, UI, labels | **IBM Plex Sans** 400–600 | high legibility at arm's length |
| Units, pack sizes, prices, spec | **IBM Plex Mono** 400–500 | units are always mono |

`font-variant-numeric: tabular-nums` is set on `body` in `tokens/base.css`, so
every figure aligns by default. Do not turn it off.

Sizes: `--fs-display-1` 88px (kiosk yield, the largest type in the product) down
to `--fs-micro` 11px (uppercase labels, desktop and phone only). 16px is the
kiosk body floor; 24px is the floor for anything a baker reads standing back.

**Fonts load from the Google Fonts CDN** via `tokens/fonts.css`. No binaries were
supplied — if bake-ops has licensed files, drop them into `assets/fonts/` and
replace that `@import` with `@font-face` rules.

### Spacing and layout

A 4px grid where the token number is the multiplier (`--sp-6` = 24px). Card
padding is surface-specific: 24px kiosk, 20px desktop, 16px phone.

Sibling groups are laid out with flex or grid and `gap` — never margins on
children, never whitespace-as-spacing. Kiosk grids are 3 across at 1280px with a
300px minimum tile.

### Shape, elevation, borders

Radii are moderate: 4px tags, 6px desktop controls, 10px buttons and keypad
keys, 14px cards and kiosk buttons, 20px kiosk hero panels, pill for chips and
badges. Nothing is fully round except chips, badges and avatars.

Borders do the work shadows would do elsewhere. `--border-subtle` is the hairline on
cards and table rules; `--border-default` is a control edge; `--border-strong` is the
kiosk-contrast edge used on keypad keys and PIN cells.

Elevation is three steps and no more: `--elev-1` is the standard card lift,
`--elev-2` is hover or dragged, `--elev-overlay` is a sheet or modal. The shadow
colour comes from the themed `--shadow-1/2/3` tokens, not from the elevation
recipe. **No gradients anywhere.** No inner shadows. No glows.

Cards: warm near-white fill, 1px hairline border, 14px radius, `--elev-1`. On
hover an interactive card takes a section-hue border and `--elev-2` — it does
not move.

### Motion and states

Short, flat, no bounce: 110 / 170 / 260ms on `cubic-bezier(0.2, 0, 0.2, 1)`.
Motion confirms a tap; it never entertains. Nothing animates on page load.

- **Hover** — fills darken 10% via `color-mix`; outlined controls take a
  section-hue border and a tint fill. Kiosk designs must never *depend* on hover.
- **Press** — darken 18% and `scale(0.985)`. Every tappable thing has a press
  state, because a wall tablet gives no other feedback.
- **Focus** — 3px halo in the current section hue (`--ring`), danger-tinted on
  destructive controls (`--ring-danger`). Never removed.
- **Disabled** — `--surface-muted` fill, `--text-disabled` text, no transform, no ring.
- `prefers-reduced-motion` is honoured globally in `tokens/base.css`.

### Imagery

No photography was supplied. Components that expect a photo (`RecipeCard`,
`ScanConfirm`) render a **striped placeholder** — a 135&#176; repeating stripe in
stone and sunk tones with a mono caption naming what belongs there ("recipe
photo", "delivery note"). Keep the placeholders until real bakery photography
exists; do not substitute stock imagery. When photos arrive they should be warm,
natural-light, close-in on product and process — no people mugging at camera.

There are no illustrations, no patterns and no decorative background art in this
system, by design.

### Transparency and blur

Used in exactly two places: `--surface-scrim` (55% dark roast behind modals) and
the 34% section-hue focus halo. No frosted glass, no translucent panels.

---

## ICONOGRAPHY

**No icon set was supplied with the brief.** The documented substitute is
[**Lucide**](https://lucide.dev) — 24&#215;24 grid, 2px stroke, round caps and
joins, geometric and unfussy, which matches "warm but engineered" better than a
filled or duotone set. **Flag this to the requester:** if bake-ops has its own
glyphs, they replace Lucide and only `Icon.jsx` needs to change.

Rules:

- Icons enter a layout only through `<Icon name="wheat" />`, which renders a
  Lucide glyph inheriting `currentColor`.
- Stroke weight is 2. Go to 2.25 only for small glyphs sitting beside hero
  numbers or inside badges, where 2 looks thin.
- Size follows the control, never the reverse: 16 desktop-dense, 20 default, 24
  kiosk body, 28–32 kiosk primary action.
- **Never hand-roll an SVG** for a bake-ops screen and never approximate a
  missing glyph. If Lucide lacks it, raise it.
- **No emoji, ever.** No unicode pictographs used as icons. The only non-alpha
  glyphs in the product are &#215; (multiplication), &#183; (metadata separator),
  &#189; (fractions) and currency symbols.
- Icons are decorative by default (`aria-hidden`); pass `title` when a glyph is
  the only label.

The page must load the Lucide UMD build:

```html
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
```

Section-typical glyphs: Home `house`, Tasks `list-checks`, Recipes `croissant` /
`wheat`, Inventory `package` / `scale`, Suppliers `truck`.

---

## BRAND

The wordmark is **type-only** — no logo files were supplied, and none were
invented. `Wordmark` sets the name in Archivo Bold, lowercase, with the hyphen
as a monospace glyph in crust orange, optionally preceded by a dark-roast tile
carrying a lowercase "b". Clear space is at least the tile height. The hyphen is
the only crust-orange accent permitted outside the Recipes section.

If a real mark arrives, replace the tile and keep the type rules.

---

## Index

### Root

- `styles.css` — the global entry point. **Consumers link this one file.** Imports only.
- `ds-globals.js` — card bootstrap: resolves the generated component bundle, and
  transpiles the `.jsx` sources in-browser as a fallback so specimen cards render
  before the bundle exists.
- `SKILL.md` — Agent Skills wrapper, for use in Claude Code.

### `tokens/`

`fonts.css` · `colors.css` · `typography.css` · `spacing.css` · `radii.css` ·
`elevation.css` · `targets.css` · `motion.css` · `base.css`

### `components/`

| Group | Components |
| --- | --- |
| `brand/` | `Wordmark`, `Placeholder` |
| `core/` | `Icon`, `Button`, `Chip`, `SegmentedControl`, `StatusBadge`, `MetaBadge`, `NumberReadout`, `NavTile`, `EmptyState` |
| `layout/` | `KioskBar`, `DesktopBar`, `PhoneBar`, `SectionLabel`, `InlineBanner`, `FieldBlock`, `Sheet` |
| `forms/` | `TextInput`, `TitleField`, `BigNumberField`, `SearchInput`, `Keypad`, `PinEntry`, `PackFill`, `MeasureChoice` |
| `recipes/` | `ScalerPanel`, `YieldReadout`, `IngredientRow`, `EditorIngredientRow`, `MethodStep`, `EditorMethodStep`, `RecipeCard`, `RecipeHero` |
| `inventory/` | `StockRow`, `ShelfCheck`, `CountEntry`, `ReorderLimitRow`, `IngredientListRow`, `NotificationCard`, `ScanConfirm`, `PackConsequence`, `SupplierLink` |
| `people/` | `StaffTile`, `ActiveUserChip`, `LeaderRow`, `StaffAdminRow` |
| `suppliers/` | `SupplierRow`, `CatalogueRow`, `OrderSendBar` |
| `data/` | `DataTable`, `OrderLines`, `StatCounter`, `ProgressTally`, `CalendarDayCell`, `TaskRow` |

Each has a sibling `.d.ts` (props contract and adherence rules) and
`.prompt.md` (what, when, the rules that are easy to get wrong, and a **Colour
tokens** list generated from the component's own source, so code wires up the
same way). Every directory has one `@dsCard` HTML showing variants and states
side by side. `guidelines/colors-component-tokens.html` is the same information
as one table, part by part.

**Intentional additions** beyond the brief's list:

- `Icon` — a wrapper is required to get Lucide glyphs in at all, and it is the
  hook that makes swapping icon sets a one-file change.
- `NumberReadout` — principle 1 needs a single canonical hero-number treatment,
  otherwise every screen re-invents it at a different size.

Added while designing the **recipe detail + scaler** screen, because the screen
could not be built from the existing set:

- `KioskBar` — every kiosk screen needs the same dark-roast top bar, and it is
  the natural place to set the screen's `data-section`.
- `Sheet` — the custom-multiplier keypad needs a modal surface. It is the only
  thing allowed to use `--surface-scrim`.
- `MetaBadge` — prep time, bake time and oven temperature are spec figures, not
  states, so `StatusBadge` was wrong for them and `Chip` is for filtering.
- `MethodStep` — numbered method rows, with the fixed-width disc that keeps a
  method in a column and a `last` flag so a one-step method has no dangling rule.
- `RecipeHero` — the detail-screen counterpart to `RecipeCard`: photo, name,
  one line, MetaBadges, and deliberately no numbers.

Added while designing the **kiosk home** and **recipe list** screens:

- `NavTile` — the "Today at a glance" destinations. `StatCounter` is a metric;
  this is a tap. It carries the hue of the section it routes to, so the tile
  looks like its destination before you read the word.
- `EmptyState` — no search results, no staff configured, an empty category.
  Says what happened in the baker's words, then gives exactly one way out.
- `TaskRow` — work with a time on it, as a list row or as a card in an "up next"
  strip. The time leads, mono and tabular, because that is what gets scanned.
- `RecipeCard` gained an optional `subtitle`, and `baseYield` became optional —
  a browse grid shows what a thing is, not a batch figure nobody is scaling.
- `IngredientRow` gained an optional `index`.

**One correction to an existing component.** `ActiveUserChip`'s avatar disc used
`--section-fill`, which meant the same baker changed colour walking from Home to
Recipes. Identity is not navigation, and letting it borrow the section hue
weakens the one thing section colour is for. The disc is now inverted ink —
`--text-on-inverse` on the bar, `--surface-inverse` on light surfaces — in every
section and every theme. The recipe-detail screen was updated to match.

**One correction to the tokens.** `--ring` was declared only at `:root`
(`tokens/elevation.css`), and custom-property substitution resolves the inner
`var(--section-fill)` where the property is *declared* — so every focus halo in
the product came out Home slate regardless of section, contradicting the stated
rule. `--ring` is now redeclared in each `[data-section]` block in
`tokens/colors.css` alongside the five hue tokens. `--ring-danger` reads
`--status-danger-fill`, which is never rebound, so it was already correct.

Added while designing the **desktop recipe editor** and **ingredient setup**
screens. All three exist because the office screens ask something the kiosk never
does: they take data *in*.

- `EditorIngredientRow` — the editable twin of `IngredientRow`. Grip, amount,
  unit, typeahead name, and a live preview column showing what the kiosk will
  display at the chosen multiplier. That column is the component's reason to
  exist: it demonstrates the unit conversion instead of explaining it. Exports
  `EditorRowHeader` so the headings line up with the rows, and `splitQuantity()`
  because "4 kg" typed into the amount box has to split.
- `MeasureChoice` — Weighed / Poured / Counted. It replaces the phrase "base
  unit", which is a database word no baker should meet, and it decides which
  units every other screen may offer for that ingredient.
- `PackConsequence` — need &#8594; one package &#8594; whole packages to order. The answer
  to "why am I filling this in?", shown as arithmetic that recomputes as the
  number is typed.

Then, when the desktop screens were rewired to **import** these components
rather than mirror them, the gaps that mirroring had been hiding surfaced. Eight
more, all of them things an office screen needs and the kiosk never did:

- `DesktopBar` + `DesktopBarButton` — the office counterpart to `KioskBar`.
- `InlineBanner` — what just happened to the screen (a scan landed, a scan
  failed). It reports; only `Sheet` may block.
- `FieldBlock` — the card a group of fields sits in, with the question/label
  distinction and a consequence `footnote`.
- `TitleField` — the name of the thing, edited at the size it is read.
- `BigNumberField` — a figure the app then reasons with.
- `EditorMethodStep` — the editable twin of `MethodStep`.
- `IngredientListRow` — the master list as a punch-list, not a directory.
- `SupplierLink` — linked, or deliberately not.

`PackFill` gained `lead`, `size` and `verbatim` instead of being forked to fit
the ingredient screen. That is the rule: **extend the component, never copy it
into a screen.**

The three kiosk screens were rewired next, and the same thing happened again:
three more components, four more extensions.

- `Placeholder` — a name we do not have yet, bracketed and mono. It makes the
  "never invent a plausible business name" rule something the code enforces
  rather than something a reviewer has to remember.
- `SectionLabel` — the uppercase band name plus its quiet right-hand count. It
  was being rebuilt six times across three screens and drifting each time.
- `YieldReadout` — "2× = 288 pastries". It **owns the scaling arithmetic**:
  `scaledYield` and `scaleIngredient` live here, so the kiosk scaler, the keypad
  mid-type and the editor's preview cannot disagree, and the g→kg promotion rule
  exists once.

Extended rather than forked: `EmptyState` gained `size="inline"` for a hole
inside a populated screen; `IngredientRow` gained `size="compact"` — kiosk ink at
the 56px touch floor, so seven lines fit two-up without scrolling; `KioskBar`
gained `onSwitchUser` and node `title`/`subtitle`; `SectionLabel` gained a
heading weight.

`ScalerPanel` no longer draws its own big figure — it mounts `YieldReadout`.
Components import components, for the same reason screens do.

Closing the loop from scaling a recipe to stock coming off the shelf added three
more, and the pattern held for a third time.

- `ShelfCheck` — "is there enough for THIS batch?". Deliberately **not** an
  extension of `StockRow`: that answers an inventory question (on hand against a
  reorder point), this answers a production one (on hand against what a batch
  needs). Two meanings on one bar would have made neither legible. The marker
  sits at a fixed position in every row so a column reads as one vertical line
  and only the fills have to be checked.
- `ProgressTally` — "how much is left?" as one shape rather than three numbers
  to add up.
- `LeaderRow` — the owner's staff leaderboard. Counts of finished work only: a
  board that can show someone last *and* in danger colour stops being looked at.

Extended rather than forked: `TaskRow` gained `kind`, `urgent`, `note`,
`completedAt` and an `actionLabel` advance button — and because a button cannot
nest inside a button, setting `actionLabel` makes the root a `div`, which also
enforces the one-deliberate-tap rule. `ScalerPanel` gained `dense` for a kiosk
screen where it is a control rather than the subject; its chips stay `xl`,
because a wall tablet never gets smaller touch targets however tight the layout.
`KioskBar` gained a `KioskBarButton` sibling at the 56px floor.

**Where ceremony goes.** Completing a bake moves real stock, so it opens a sheet
listing exactly what comes off the shelf before it commits. Completing a prep
task changes nothing, so it completes on the tap. Ceremony where there is no
consequence only teaches people to tap through it.

The inventory trio — stock list, receiving and spot-checks, needs-ordering — was
the first batch on the **phone**, and it added three more components.

- `PhoneBar` — the pocket surface's chrome. Deliberately light rather than dark
  roast: the kiosk and desktop bars are dark so they recede, but a phone is read
  one-handed in daylight where a dark bar is a glare trap.
- `CountEntry` — a spot-check of one ingredient. It exists to make one thing
  unmissable: a count **sets** the figure, receiving **adds** to it. Confusing
  those is the single error that silently corrupts every number downstream.
- `SegmentedControl` — two mutually exclusive jobs on one screen. A filter chip
  changes what a list contains; this changes what the screen is for.

Extended rather than forked: `StockRow` gained a `phone` size that stacks in
portrait, a `notReady` state for an ingredient whose setup was never finished,
and both its figures now run through `formatQuantity`; `OrderLines` gained
`suggestion`, a per-line `context`, and a footer hand-off, because a suggested
order and a real one are the same object at different confidence.

Screen data for the batch lives in `inventory-data.js`, loaded by all three
screens, so a figure cannot say one thing on the stock list and another on the
order draft. `ShelfCheck` on the bake screen and `StockRow` here read the same
flour figure and correctly disagree in their verdicts — 60 kg is enough for a
50 kg batch **and** below a 75 kg reorder level. Two questions, two answers.

## Tweaks and shared figures

Two classes of defect worth naming, both found by moving a control rather than
reading code.

**A tweak that moves and changes nothing.** Eight screens seeded state from props
in the constructor, so every declared control went dead after first render. Each
screen now reconciles props into state in `componentDidUpdate`. Declaring a prop
is a promise that it drives the screen; the promise has to be tested by using it.

**Per-batch data.** Each batch of screens had brought its own figures, and they
had drifted: the kiosk home strip and Today's work showed the same baker two
different jobs at 06:15, and the recipe editor's master list header said thirteen
over twelve rows. `inventory-data.js` now holds the ingredient master, the
schedule, the staff and the recipes for every screen. It also owns `convert()`,
because joining a recipe line in grams to a master record in kilos silently made
four ingredients read as short.

## The supplier cluster

Closing the reorder loop needed a folder of its own.

- `SupplierRow` — who they are, how to reach them, and what buying from them is
  like. Contact details sit in the row: "how do I reach them" is half the reason
  anyone opens a supplier list.
- `CatalogueRow` — one product, and **which of our ingredients it restocks**.
  That link is the point of the row: without it the product cannot appear on a
  suggested order, so an unlinked product gets warning treatment and reads as
  unfinished rather than looking fine and quietly doing nothing.
- `OrderSendBar` — what is going, to whom and what it costs, next to the button.
  Nobody should scroll away from a total to commit to it, so the facts being
  present *is* the safeguard and a confirm dialog would only teach people to
  click through. Once sent it becomes a receipt, and its note carries the fact
  that matters: the order is **outstanding until someone receives it**, and stock
  moves then, not now.

`OrderLines` was extended twice rather than forked: `minimumPacks` clamps the
stepper on a single-line order so a supplier minimum cannot be breached by
fumbling, and `size="kiosk"` raises the steppers from 32px to the 56px floor —
the component was built for the back office and is now on a wall tablet.

Two target defects surfaced only by measuring: `SupplierRow`'s name was a bare
25px text button, and `OrderLines`' remove button sat at 48px on the kiosk. Both
are fixed in the components, so every future screen gets them right.

## The owner's screens

The last batch: dashboard, work management, settings.

- `StaffAdminRow` — admin is not decoration. It gates settings, stock limits,
  costing and approving orders, so who holds it is a badge on the row, and the row
  says what admin unlocks. PINs are reset, never shown. Hourly rate is not
  rendered at all for a non-admin viewer rather than masked — a masked figure
  still tells you it exists.
- `ReorderLimitRow` — tuning what "low" means, **phone-first**, because the baker
  described doing this from a phone with admin credentials. Its label is "Warn me
  at", not "reorder point", and the LOW badge appears live as you type so the
  consequence of a level is visible while it is being chosen.

Creating a job leans on templates — this bakery runs the same prep most days, so
his six repeated jobs are pickable and picking one fills the form rather than
skipping it. Typing from scratch is never worse than one extra field.

## Targets: one systematic pass

Desktop components had been caught undersized on the kiosk three times
(`InlineBanner` buttons, `OrderLines` steppers, `SupplierRow`'s name). That is a
pattern, so the whole library got swept at once: 45 cases mounted at their kiosk
size with every interactive target measured against the surface floor.

Eleven cases failed. The common cause was not a wrong number but a **wrong
owner**: a field's shell was the right height while its `<input>` sat unstretched
inside it, so only a ~26px band actually focused. `SearchInput`, `TextInput`,
`ScanConfirm` and `ReorderLimitRow` all had it. `PackFill` had the same shape one
level up — its shared `Slot` hardcoded `--tap-md`, so patching the selects
individually left the quantity input behind; the height belongs on the Slot.

Also fixed: `SearchInput`'s clear button was sized off its glyph
(`icon + 12` = 40px) rather than a target token, `OrderLines`' footer hand-off was
`--tap-md` on every surface, and the editor rows sat at 32px, which is neither the
desktop standard (36) nor the dense exception (28).

Two 28px controls remain and are correct: the per-row remove buttons in
`EditorIngredientRow` and `EditorMethodStep` are `--tap-xs`, which the targets
guideline sanctions for a dense desktop row action.

**The lesson worth keeping:** a padded wrapper is not a target. Measure the
control, not the box around it.

## Pin your CDN majors

Twice now an unpinned dependency has broken everything at once. `@babel/standalone`
moved to 8.x, whose `preset-react` defaults to the **automatic** JSX runtime and
emits `import { jsx } from "react/jsx-runtime"` — an import statement neither
`new Function` nor an inline `<script>` can execute. The first time it took out
the component loader; the second time every specimen card, which went blank with
nothing but "Script error" to show for it.

Both now pin the major **and** name `runtime: 'classic'` explicitly, so a future
default cannot do it again. The cards register a `bakeops` preset rather than
using bare `data-presets="react"`, because an inline script tag has no other way
to pass preset options.

## Screens import components

Screens mount the real components from `components/` through `ds-globals.js`,
which populates `window.DS`. Nothing is re-created inline. The reason is drift:
a mirrored copy is a fork that nobody remembers to update, and it also means the
screens are never proof that the component APIs work. Importing makes every
screen an integration test of the library. See `CLAUDE.md` for the mechanics.

The loader tolerates a stale compiled bundle: every component file's main export
is named after the file, so it can spot which names are missing and load just
those from source. Sources are fetched in parallel and evaluated in dependency
order.

### `guidelines/`

27 specimen cards, grouped in the Design System tab as **Colors**, **Type**,
**Spacing**, **Shape** and **Brand**. The ones that carry rules rather than
values: `colors-naming.html`, `colors-on-pairs.html`, `colors-danger-rule.html`,
`brand-principles.html`, `brand-surfaces.html`, `content-voice.html`,
`type-tabular.html`. The theme work is in `colors-themes.html` (three variants
side by side), `colors-contrast.html` (measured AA audit) and
`colors-component-tokens.html` (component → token map).

### Reference content

Real bakery content is used throughout — no lorem ipsum:

- **Dobladas** (folded triangle pastry): 4 kg Fat, 150 g Salt, 300 g Beef
  booster, 15 Eggs, 600 g Baking powder, 200 g Improver, 25 kg Plain flour.
- Other recipes: Honey Cake, Milhoja Masa, Empanada masa, Alfajores Blanco,
  Sponge mix, Meringue, Vanilla custard.
- Staff: **Basil** (head baker), **Steve** (baker). These two, no others — do not
  invent a third name to fill a row.
- Supplier: **Premium Flour Mills** — Bread Flour $2.80/kg, minimum order 25 kg,
  sold as "1 &#215; 25KG bag".

**Names we do not have yet are written as bracketed placeholders, in mono:**
`[bakery name]`, `[supplier 2]`, `[supplier 3]`. A plausible invention is worse
than an obvious gap — it gets mistaken for a decision and ships. This is the
typographic twin of the striped image placeholder: same job, same honesty. Swap
them for the real names when the requester supplies them; the mono treatment is
what marks them as unfinished.

## Using it

```html
<link rel="stylesheet" href="styles.css">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
```

```jsx
<section data-section="recipes">
  <ScalerPanel recipeName="Dobladas" baseYield={143.6} yieldUnit="pieces"
    multiplier={2.5} onMultiplier={setM} onCustom={openKeypad} />
  <IngredientRow quantity="62.5" unit="kg" name="Plain flour" baseQuantity="25" baseUnit="kg" />
  <Button variant="primary" size="xl" icon="check">Mark bake done</Button>
</section>
```

Wrap a section root in `data-section` and the hue is handled. Pick the size that
matches the surface. Let the numbers be big.
