:00Thu 24 Sep

UI kit

Every component in every variant and state, with its accessibility contract. Switch Night shift in the account menu to see the night register.

Import from app/kit/index.ts · styles /assets/kit.css
On this page

Foundation

Colour

Paper for work, navy for presence, night for the control room. One action colour (accent). LEDs are device truth: never re-tinted, never decoration.

Call
var(--sheet) · var(--text-2) · var(--accent) · var(--led-amber)
Keyboard
Not focusable.
Name
Text meets 4.5:1 on its surface; lines that carry information (cells, bars) meet 3:1.
Live region
None.
Target
Not a target.

Surfaces

--bgthe floor--rail--sheet--sheet-2--sheet-sunk

Text and lines

--text--text-2--text-3--line--line-2--line-strong

Action and focus

--accent--accent-hover--accent-soft--focus--selected--sel-ring

Device truth (LEDs)

--led-red--led-amber--led-yellow--led-green--led-blue--led-white

Chart fills (3:1 on paper)

--fill-ok--fill-warn--fill-bad--fill-info--fill-idle

Tone

Meaning is an attribute: data-tone="ok|warn|bad|info|idle" on any tone-aware component. The tones layer maps it to --tone-fg/bg/line/surface/dot/fill; each component sets the idle quintet on itself, so a child never inherits its parent's tone.

Call
st('warn', 'Needs review') · badge('Late', { tone: 'bad' })
Keyboard
Not focusable.
Name
Tone is never colour alone: a word, a square or a shape always carries it.
Live region
None.
Target
Not a target.

data-tone="ok"

StoredStoredStored--ok-dot--ok-surface

data-tone="warn"

Needs reviewNeeds reviewNeeds review--warn-dot--warn-surface

data-tone="bad"

OfflineOfflineOffline--bad-dot--bad-surface

data-tone="info"

CountingCountingCounting--info-dot--info-surface

data-tone="idle"

PlannedPlannedPlanned--idle-dot--idle-surface

Type

Jost for identity and big numbers, Inter for everything read, mono for identifiers. Display 44 (clamped), page 34, title 24, section 19, lead 17, body 15, data 14, small 13, caption 12.

Call
.display · .title · .lead · .small · .cap · .mono · num(v, { size })
Keyboard
Not focusable.
Name
Headings are real headings; one h1 per page. Sizes scale with the station scale.
Live region
None.
Target
Not a target.

Ramp

Put in 14 reels

Choose working rack

Needs review · 2

Checks each bay: reel or no reel, against the record.

Body 15 · The reel goes back to SR-01 and the bay light turns off.

Small 13 · Recorded with your name.

Caption 12 · as of 14:32

R-2026-0042 · SR-01-T3-L14

Numbers (Jost, tabular)

1 260pcs48%714:32

Space and radius

A 4 px step. Radius by role: control r2 (6), card and inset r3 (8), sheet r4 (12), hero and station surface r5 (16).

Call
var(--s1…s16) · var(--r-control|card|sheet|hero)
Keyboard
Not focusable.
Name
Its text.
Live region
None.
Target
Not a target.

Scale

Space
s1 4 · s2 8 · s3 12 · s4 16 · s5 20 · s6 24 · s7 28 · s8 32 · s10 40 · s12 48 · s16 64
Radius
r1 4 · r2 6 · r3 8 · r4 12 · r5 16 · pill
Row density
compact s2 s4 · default s3 s5 · roomy s4 s5
Shadow
shadow-1 resting · shadow-2 hover · shadow-3 floating layers

Size scales

One name set everywhere: xs sm md lg xl 2xl 3xl. Each scale is a token (--avatar-md, --icon-lg, --led-sm …), so [data-scale="station"] on a station frame steps type, controls, icons, avatars and LEDs up at once.

Call
--h-sm 36 · --h-md 44 · --h-lg 56 · --h-xl 64 · station: 56 · 56 · 64 · 72 · --target-min · --target-primary
Keyboard
Not focusable.
Name
Its text.
Live region
None.
Target
Controls ≥ 24 px everywhere; ≥ 44 px under pointer: coarse; on the station scale ≥ 56 px, primaries ≥ 64 px (Neo NFR-E2).

Control height

Icon

smmdlgxlhero

Avatar

Icon tile

LED

Station ladder (Neo NFR-E2, glove use): nothing pressed under 56 px, every primary 64, the thumb bar's primary 72

Body text on a station

Station must-read type (Neo NFR-E3, 2 m): the one instruction ≥ 64 px on the 1280 × 800 kiosk; a handheld stays proportionate

--t-display: clamp(32px, 5.2cqi, 72px) and --t-step-lg follow the station frame's width: 64.9 px in a 1248 px kiosk pane, 72 on a wider wall screen, 32 on a 480 px handheld. The bay address a task leads with is larger still (bayAddress(bay, { size: 'hero' }), 88–168 px).

Motion

Durations and curves are named by purpose. Leaving is always faster than arriving. One authored moment per surface.

Call
var(--m-enter) var(--e-enter)
Keyboard
Not focusable.
Name
Reduced motion drops movement to 0 ms; Pause blinking (html[data-motion=paused]) stops every loop and draws a blink still, split corner to corner (lit / off). Colour and information stay.
Live region
None.
Target
Not a target.

Tokens

Press · state
--m-press 90 · --m-state 160
Enter · exit · travel
--m-enter 240 · --m-exit 160 · --m-travel 320
Settle · seam step · shake
--m-settle 480 · --m-seam-step 280 · --m-shake 320
LED (hardware, never tuned)
--m-led 500 · slow = 2 × LED
Curves
--e-standard · --e-enter · --e-exit · --e-travel · --e-settle · --e-progress

Blink, slow blink, paused

Live valuePause blinking in the account menu to see the dashed ring.

Layout

Pages compose with page, stack, row, grid, split and section. split children never overflow (min-width 0); the side column goes under the main one below 1180 px.

Call
.stack--0…8 · .row--wrap|between|end|center|top|gap-N · .grid--2|3|4|auto · .split--left|side-sm|md|lg
Keyboard
Not focusable.
Name
Reflows to 320 px and 200 % zoom without loss.
Live region
None.
Target
Not a target.

stack--3 · row--between · grid--3

row--between: start
end
grid--3 · one
two
three
split: main
side (300)

Utilities

A small closed set allowed in page markup. No literal inline styles: style="" only carries data-driven custom properties set by a kit component.

Keyboard
Not focusable.
Name
.sr is the one visually-hidden utility.
Live region
None.
Target
Not a target.

Text

mutedfaintsmallcapstrongmono1 260nowrap

Spacing and measure

mt-0…8 · mb-0…8 · self-start · self-end · measure (66ch) · measure-sm (44ch) · h3 · lead · prose · title · display

Shared states

State comes from ARIA and native state, defined once in the states layer: the sheet chip on a track (aria-current, aria-pressed, :checked), the selected ring on a chosen card, the pressed pill, disabled, busy and invalid.

Keyboard
Each control keeps its native keyboard model.
Name
Forced colours: selected chips use Highlight, selected cards get a Highlight outline.
Live region
None.
Target
Not a target.

Selected chip on a track

Selected ring

Ring demo

Pressed pill · disabled · invalid · busy

Primitives

Icon

Lucide outlines at 1.75 stroke from one sprite. Decorative next to words; named when it stands alone.

Call
icon('scan-line', { size: 'sm' | 'lg' | 'xl' | 'hero', label? })
Keyboard
Not focusable.
Name
aria-hidden by default; with label it is role=img with that name.
Live region
None.
Target
Not a target.

Sizes · decorative and named

Button

One primary per surface; footers put the secondary first, then exactly one primary or one danger. A link that looks like a button is still a link (href).

Call
btn(label, { kind, size, icon, iconEnd, block, align, href, type, pressed, disabled, busyLabel, meta })
Keyboard
Tab; Enter (links and buttons) or Space (buttons).
Name
Its words; an icon is decorative.
Live region
While a request runs the label changes (data-busy-label).
Target
44 px (36 px sm; 44 px sm under coarse pointers).

Kinds

Sizes

States

Disabled link

Block, aligned, with meta

Icon button

A control shown as an icon only. It always has a name; a badge is decorative and the name carries the count.

Call
iconBtn('bell', 'Alerts, 5 need attention', { size, badge, pressed, href, line })
Keyboard
Tab; Enter or Space.
Name
aria-label, required.
Live region
None.
Target
40 px (36 sm, 44 lg); 44 px under coarse pointers.

Sizes, badge, pressed

Status word

Square + word: the default way to state a state inline. Hollow means “not yet”; live breathes while a value updates.

Call
st(tone, text, { size, hollow, live, wrap })
Keyboard
Not focusable.
Name
The word is the meaning; the square is decoration.
Live region
None.
Target
Not a target.

Tones

ok · wordwarn · wordbad · wordinfo · wordidle · word

Sizes and variants

SmallMediumLargeNot confirmed yetReading baysTier 3 left out: bays 1–16 are Unknown since 09:41

Badge, chip, tag

Badge: a state on a ground (r2, 24). Chip: a count or filter value (pill). Tag: a code or language key (mono, r1).

Call
badge(text, { tone, outline, plain, solid }) · chip(text, { tone, icon, href }) · tag(text, { size })
Keyboard
Not focusable.
Name
Their words.
Live region
None.
Target
A chip with href is ≥ 24 px tall.

Badge

okwarnbadinfoidleStays until fixedLateBlocked

Chip

12 reelsok 3warn 3bad 3info 3idle 3Shift BSR-01

Tag

ENHURC0603FR-0710KL

Identifier, number, key

Identifiers never break inside. Numbers are Jost with tabular figures; the unit is smaller and quieter.

Call
ident(id, { size: 'sm' | 'md' | 'lg' | 'hero' }) · num(v, { unit, size }) · kbd(k) · mono(text) · fmtInt(n) → '48 380' (server twin of kit.js fmtInt)
Keyboard
Not focusable.
Name
Its text.
Live region
None.
Target
Not a target.

Identifiers

R-2026-0042SR-01-T3-L14WO-260714-07R-2026-0042

Numbers and keys

5 000pcs48%1 260pcs⌘ KGRM155R71H104KE14D

Stat

The big-number pattern: label, value with unit, one line of provenance. Meters are rows of stats.

Call
stat({ value, unit, label, foot, size, tone, kind: 'number' | 'text' })
Keyboard
Not focusable.
Name
Read in order: label, value, foot.
Live region
None.
Target
Not a target.

A row of numbers with word values (kind: text)

Quantity
5 000pcs
Lot
LOT-2614-042
Status
Stored
Due back
15:10

Sizes and tone

On hand
48 380pcs
master data · 14:32
Reels
22
SR-01 and SR-02
Short
310pcs
WO-260714-09
Now
14:32

Avatar and person

Initials in a circle; system actors are navy with an icon. A person is avatar + name (+ role), the one recipe wherever a person appears in a row.

Call
avatar({ ini | icon }, { size, sys, ghost, idle, label }) · person(p, { idle, wrap, role, size, as, href }) · avatarStack(people, { max, label })
Keyboard
as: 'button' or 'link' makes it a control.
Name
The avatar is decorative next to the name; a stack is one image named by its label.
Live region
None.
Target
A person button is ≥ 24 px.

Avatars

?LV

Person

Bence TóthSupervisorMárta KovácsOperatorEszter SzabóSite administratorJanussystemLászló VargaSign-in offGábor Nagy-Szentmiklósiwrap: names wrap in table cells

Stack

LED

Device truth: the colour a bay or tower lamp shows. Blink is 500 ms, slow is 1 s, both 1:1. Paused or reduced motion turns a blink into a dashed ring.

Call
led(color, { mode: 'steady' | 'blink' | 'slow' | 'off' | 'unknown', size, label, explain })
Keyboard
With explain: Tab to it; Enter or Space opens the hint; Esc closes it.
Name
Decorative beside words; label names it; explain makes it a named button.
Live region
None.
Target
The explain button is ≥ 24 px (44 on station scale).

Colours

Modes and sizes

Explains itself

Amber blinking: this bay is waiting for a reel. It stops when the sensor sees one.Press the light to read what it means.

Icon tile

An icon on its tone ground: dialog icons, route lists, empty states. Square (r3) or round.

Call
iconTile(icon, { size: 'sm' | 'md' | 'lg' | 'xl', tone, round, label })
Keyboard
Not focusable.
Name
Decorative unless label is given.
Live region
None.
Target
Not a target.

Sizes and tones

Rank, source, freshness, as-of

Rank is an ordinal, never a severity. Source says where a value came from. Freshness and as-of say how current it is.

Call
rank(n) · src(kind, text) · fresh('ok' | 'stale' | 'failed', text) · asof(text) · sr(text)
Keyboard
Not focusable.
Name
Words carry each mark; sr() adds words only screen readers hear.
Live region
None.
Target
Not a target.

Marks

12LabelMasterOperatorERPLiveStale · 6 minFailed1 260 pcsas of 14:26Screen-reader only words

Permit

Custody under a permit: who may work on what, until when. An accent ring, never a status tone (a permit is not good or bad).

Call
permit(words, { icon?: 'key-round' | false })
Keyboard
Not focusable.
Name
Its words; the key icon is decorative.
Live region
None.
Target
Not a target.

Permit

Dávid Horváth · until 18:00Tier 3 · wiring v4 · Dávid Horváth until 18:00No icon

Release mark

What ships in Neo R1 and what is parked (the owner's decision, 2026-09-28). later marks a parked surface: a solid ink pill with its clock, "Later · not in R1", in the rail's Later group, the Preview control's Later list, a Later note and the atlas. r1 is the quiet outline a list uses when it marks both. Metadata, never a tone: nothing about it is good or bad.

Call
release('later' | 'r1', { size: 'sm' | 'md' })
Keyboard
Not focusable.
Name
Its words ("Later · not in R1", "R1"); the clock is decorative.
Live region
None.
Target
Not a target: it labels one.

Marks

Later · not in R1Later · not in R1R1R1

Meta, cite, toned icon

meta: a small icon + words in a row's meta line. cite: an Activity entry id with a copy-link button (shown when script runs). A toned icon takes the tone's foreground; the words still say it.

Call
meta(icon, text) · cite(ev, { href }) · icon(name, { tone })
Keyboard
cite: the copy button is a 36 px icon button (44 on touch).
Name
meta: its words; cite: "Copy link to EV-…"; toned icons are decorative.
Live region
Copy says "Link to EV-… copied." as a toast.
Target
Not a target.

Marks

Work order · expediteAt the rackEV-2493

Battery, problem link, phone primary

battery: a glyph filled to the level with the number (under 20 % bad, under 40 % warn). problemLink: status · id · why as one wrapping link. btn({ primaryOn: "phone" }): the primary look only under 560 px. chip({ wrap }): a chip whose words wrap. iconTile({ ping }): one radar pulse (stops with motion).

Call
battery(pct, { label, hideNumber }) · problemLink({ tone, status, id, text, href }) · btn(label, { primaryOn: 'phone' }) · chip(text, { wrap }) · iconTile(icon, { ping })
Keyboard
Not focusable.
Name
battery: role=img "Battery 45 %"; the link reads status, id and why in order.
Live region
None.
Target
Links in text; buttons 44 px.

Station variants

btn({ wrap }): a long label wraps and the button grows (a station bar at 320 px). iconBtn({ variant: "overlay", round }): a control over a picture (the camera). person(p, { size: "tile", as }): a big avatar over the name (the sign-in people grid; aria-current marks the chosen one). person(p, { as: "button", chip: true }): the person's menu button in a station head, a ringed pill with a chevron that folds to its avatar on a phone.

Call
btn(label, { wrap }) · iconBtn(icon, label, { variant: 'overlay', round, size }) · person(p, { size: 'tile', as: 'link' | 'button', href, role }) · person(p, { as: 'button', size: 'md', chip: true })
Keyboard
Not focusable.
Name
Their words; icon buttons are named by label.
Live region
None.
Target
44 px and up.

Variants

Person chip (station head)

Person tiles

Márta KovácsOperator

Forms

Field

Label, control, then the error (server) and the hint. A client error shows only after the person has interacted (:user-invalid).

Call
field({ label, id, control, hint, error, clientError, optional, size })
Keyboard
The control’s own.
Name
The label (for); hint and error are its description (aria-describedby).
Live region
None: errors arrive with the page or the swapped form.
Target
Controls are 44 px.

Resting, optional, server error, client check

Shown on the rack label and in every list.

More than the reel holds (5 000 pcs).

Pieces on the reel now.

Like SR-01-T3-L14.

That is not a bay address.

Input, select, textarea

Sizes sm 36 · md 44 · lg 56 · xl 64 · code (one-time codes). Widths xs · sm · md · lg · full.

Call
input({ name, type, size, mono, width, … }) · select({ name, options: [value, label, { disabled, lang }], value, size, width }) · textarea({ name, rows, code })
Keyboard
Native.
Name
Always inside field() or given aria-label.
Live region
None.
Target
36–64 px tall.

Sizes

Select, textarea, states

Input group

The search and scan field family: icon, input, unit, action. xl is the scan field on task pages.

Call
inputGroup({ icon, input, unit, action, size, mono })
Keyboard
Native; the action is its own tab stop.
Name
The input carries aria-label (or a field label).
Live region
None.
Target
36–64 px.

Sizes and parts

pcs

Check, switch, fieldset

Checkbox and radio rows with their words; the one switch (a checkbox with role=switch) for settings that apply at once; fieldsets group them, and can require at least one box.

Call
check({ label, name, type, hint }) · switchRow({ label, name, checked, hint }) · fieldset({ legend, hint, gate: { minChecked } }, body)
Keyboard
Space toggles; arrows move between radios.
Name
The row’s words; the fieldset legend names the group.
Live region
None.
Target
≥ 24 px rows (22 px boxes under coarse pointers).

Fieldset with a gate

Notify

At least one.

Radios and switches

Option cards

One choice group of cards: radio or checkbox, with or without a drawn indicator, a tone (danger), meta, a why note, media, and a Then: line that shows only while chosen.

Call
options({ name, legend, mediaAt: 'end' | 'top' | 'start', items: [{ value, title, desc, then, tone, meta, why, media, disabled, checked }], type, indicator, cols, size })
Keyboard
Arrows move between radios; Space toggles checkboxes. The focus ring sits on the card.
Name
The legend names the group; each card is its input’s label.
Live region
None.
Target
The whole card.
What happens to the reel
Export
Without an indicator (rack choice)

Content that follows a choice

Words or a block that follow which radio is checked: a submit that says what the chosen answer does, a hint, or fields only one answer needs. Every case is rendered; the kit emits the :has() rules for this one choice set, so no script is involved and page CSS never lists values. A case key may name several values ('list|none').

Call
byChoice({ scope: formOrDialogId, name, cases: { value: Part, 'v1|v2': Part }, fallback, as: 'span'|'div'|'p', cls })
Keyboard
The radios' own; hidden cases are display:none, so their controls leave the tab order.
Name
A submit is named by the one visible case.
Live region
None: the change follows the person's own choice.
Target
The content's own.

A submit label, and fields for one answer (CSS only)

What did you find?

Segmented control

One track, one current chip. nav: links (aria-current) · radio: native radios · submit: name/value buttons (aria-pressed) · toggle: buttons that act at once.

Call
seg({ label, items, mode: 'nav' | 'radio' | 'submit' | 'toggle', name, value, size, block })
Keyboard
nav and submit: Tab between items. radio: one tab stop, arrows move.
Name
aria-label names the group; items by their words.
Live region
None.
Target
36 px items (40 under coarse pointers; 28 sm; 44 lg).

Modes

Toggle group

Short square toggles on a track: the days a rule runs on. Each is a real checkbox (a radio without multi), so the form posts its values as they are; checked wears the one selected chip. A gate asks for at least one.

Call
toggleGroup({ name, legend, legendHidden, hint, items: [{ value, label, checked, disabled, title }], multi = true, gate: { minChecked } })
Keyboard
Tab to each toggle, Space toggles (native checkboxes).
Name
The legend names the group; each toggle its label (give a title with the full day name).
Live region
None.
Target
44 × 40 px (44 tall on touch).

Runs on

Runs on

Setting rows

Site and personal settings: the label in a 200 px column, the control (≤ 320 px, or the rest with wide) and its hint under it; ruled rows; one column under 700 px. Edited is a visible word, not a colour. The working rack as a form control is rackPick({ size: "field" }). Inputs and selects never grow past their column (max-width 100 %), so a long option cannot push a 320 px station sideways; the rack picker and the Preview button reach 44 px under a coarse pointer and on the station scale.

Call
settingRows(rows, { label }) · settingRow({ label, labelFor, id, control, hint, wide, edited }) · rackPick({ rack, tone, size: 'bar' | 'field' })
Keyboard
The control's own.
Name
labelFor makes the label a <label>; the hint is wired with aria-describedby.
Live region
None.
Target
The control's own (44 px).

Rows

Edited

The rack beeps when a reel is out longer than this.

Working rack

Scans and bay lights on this station use it.

Filter chips and toolbar

A GET form of pills: the URL holds the state (a link to it lands on the same results) and htmx swaps the results on change. The toolbar lines up search, chips, selects and what the list is.

Call
urlWith(base, { f: ['low', 'esd'] }) (arrays repeat a param) · chips({ name, label, bare, attrs, items: [{ value, name, checked, label, count }], value, multi, hx, keep, action, trailing }) · toolbar({ search, filters, selects, trailing })
Keyboard
Arrows between single-choice chips; Space toggles multi-choice chips.
Name
The form is named by label; chips by their words and counts.
Live region
The count line in trailing is a role=status sibling, never the swapped region.
Target
36 px (44 under coarse pointers).

Single and multi choice

Bare chips inside one toolbar form (independent on/off chips)

Toolbar

24 people

Reason, acknowledgement, typed phrase

The standard gates in a confirm: a reason (4–500, recorded with your name), a required acknowledgement, and a phrase typed by hand (paste blocked). The submit waits (data-gated) until they are met.

Call
reasonField({ id, min, max }) · ackCheck({ label }) · phraseGate({ phrase, id })
Keyboard
Native.
Name
Each has a label; the unmet reason is the gated button’s description (app.js).
Live region
None.
Target
44 px.

Recorded with your name.

Write at least 4 characters.

Typed by hand, so it is a decision and not a reflex.

One-button post

A single action that writes: a form that adds no box, posted by htmx. Without hx it swaps nothing (the answer is 204 and a toast); hx.target names the region the answer replaces.

Call
postButton({ action, hx, fields, close }, button) · hiddenFields(record)
Keyboard
The button’s.
Name
The button’s words.
Live region
The answer is a toast (one live region).
Target
The button’s.

Inline in a row

Reel R-2026-0042 is waiting.

Suggest, toggle, file drop, counter

suggest: a quick-insert word that submits ("Add to reason"). toggle: a phone Details / Less pill; its :checked reveals row parts by CSS. fileDrop: a labelled drop zone over a real file input. textarea({ count }): a stable n / max line. reasonField({ placeholder }), ackCheck({ clientError }), inputGroup({ width }), options({ layout: "line" }) with bayStrip, OptionItem.pending.

Call
suggest(label, { name, value }) · toggle({ label, labelOn, name, checked }) · fileDrop({ id, accept, title, hint, icon }) · textarea({ count, maxlength }) · ackCheck({ label, clientError, id }) · inputGroup({ width: 'xs'|'sm'|'md'|'lg' })
Keyboard
Native: buttons, checkbox (Space), file input (Enter opens the picker).
Name
The toggle is a checkbox named by its visible words; the drop zone is the file input's label; its hint describes it.
Live region
The counter is not live (it would speak every key).
Target
44 px on touch.

Suggest and toggle

File drop · counter · ack with client error

0 / 200

Tick this to continue.

pcs

Line options with a bay strip · a pending (drafted) choice

Tiers to count
Decision (draft no longer fits)

Overlays

Dialog

One dialog: a head (optional tone icon, h2 title, sub), a body that scrolls, a foot with the secondary first and one primary. A form dialog wraps head, body and foot in a display: contents form. open renders data-modal="open" (never the open attribute): app.js shows it modally on load and after a swap.

Call
dialog({ id, title, sub, icon, tone, body, foot, kind, size: 'md' | 'wide' | 'station', close, form, open, clears })
Keyboard
Opens from its invoker; focus moves in (autofocus or the first control), is trapped, and returns to the invoker. Esc closes; clicking the backdrop closes (closedby=any).
Name
aria-labelledby its h2 title.
Live region
None; a result shows as a toast or in the swapped region.
Target
The × is 40 px; foot buttons 44 px.

Open one

Drawer

The same dialog from the right edge (560 or 760 px): alerts, a record beside the list, a long editor. It travels in and leaves faster.

Call
dialog({ kind: 'drawer', size: 'wide', media?, … }) — media: an entity lead (a person's avatar at lg) in place of the icon tile
Keyboard
As the dialog. Always has a × button.
Name
aria-labelledby its title.
Live region
None.
Target
As the dialog.

Open one

Confirm

A decision with a consequence: tone icon, what it does (consequences), then the gates it needs (reason, typed phrase, acknowledgement). No × button; the safe choice has focus; the submit waits until the gates are met.

Call
confirm({ id, tone, icon, title, sub, consequences, reason, ack, phrase, submit, cancel, form, clears, attrs, status, order: 'body-first' | 'consequences-first', returnFocus }) · dialog({ …, returnFocus: '#row-x .rowlink__a' }) for a server-opened dialog with no invoker
Keyboard
Focus starts on Cancel. Esc cancels; the backdrop does not (closedby=closerequest).
Name
aria-labelledby its title.
Live region
The unmet gate is the submit’s description.
Target
44 px.

Open one

Menu

A popover menu of links, dialog openers, menuitemcheckbox toggles that post, and plain buttons the page acts on (hold to exit). The account menu in the top bar is one. size: "station" is the glove-sized menu of a station head or thumb bar: items at the station control height, the sub line under the label, and it opens above its button when there is no room below. An item that runs a flow in place closes its menu (app.js).

Call
menu({ id, label, head, size: 'station', items: [{ href, attrs } | { opens } | { action, checked } | { button: true, id, attrs } | 'sep'] })
Keyboard
Opens from its button (popovertarget); arrows move, Home/End jump, Esc closes and returns focus.
Name
role=menu named by label; items by their words; toggles state aria-checked.
Live region
None.
Target
40 px items; station items 64 px.

Open it

Station size (a thumb-bar More)

Tip

A short hint (popover=hint) for a control that already has a name: icon-rail labels, what an LED means. Never the only place a meaning lives.

Call
tip(id, text, { side, note }) + interestfor / popovertarget on the invoker
Keyboard
Shows on hover or focus (interestfor), or opens from a button; Esc closes.
Name
role=tooltip; the invoker keeps its own name.
Live region
None.
Target
Not a target.

Hover or focus

The rack this station’s scans and bay lights use.

Toast

Confirms what happened, on navy, bottom right, at most 3. ok and info leave after 4 s, warn after 7 s, bad stays until dismissed. A toast never replaces a receipt or a Then: line.

Call
respond(ctx, { url, toast: { text, tone } }) · HX-Trigger {"toast": …} · the shell's one-time flash after a write that navigates (respond({ navigate }))
Keyboard
The × dismisses; the toast region is a top-layer popover above dialogs.
Name
Its words.
Live region
One region: role=status, polite; bad uses role=alert.
Target
The × is 28 px.

Send one

Palette and scan sheet

The shell’s own overlays: the palette (⌘K, Ctrl K or “/”) finds records, actions and pages; a scanner burst anywhere opens the scan sheet with what the code is and the one next step.

Call
Rendered by app/layout/shell.ts; opened by the omni field or app.js
Keyboard
Palette: type, arrows move, Enter opens, Esc closes. Scan sheet: the primary has focus.
Name
The palette input is a combobox named “Scan or search”; the list is a listbox.
Live region
Results replace the listbox; the count of matches is its size.
Target
46 px palette rows.

Open it

or press ⌘K anywhere.

Developer overlay

The prototype's own tools, out of every page's layout: one floating button bottom-right opens one panel with what the page registers. Its Preview states (R1, then Later with each Neo source), Simulate (what the page registered with demoBar() or a station's plays, moved here by the script), View as (the people it is designed for, ?as=) and This page (purpose, release, first-viewport budget). A section with nothing in it is not drawn. The button stays fully visible above a bar the page docks at the bottom; inside a page's own dialog with a registered simulation it rides in that dialog. Never ported.

Call
devOverlay({ page: { title, purpose?, release?, budget? }, preview?, people?, sims?, links?, station? }) — the shell renders it on every page; devSlot(body) registers a simulation; GET /__preview.json lists what every page registers
Keyboard
AltShiftD opens and closes it anywhere, also over a page's dialog; Esc closes it and focus returns to the button; the current state's link has focus when it opens.
Name
The button: “Prototype tools”; the dialog: its title.
Live region
None.
Target
44 px; 56 px on a station (NFR-E2), and every control in the panel follows the station scale there.

The page's own

or press AltShiftD anywhere.

The panel, as it opens on Put in

Preview stateReady

Later · not in R1

View as

This page

Purpose
Put reels in by evidence: scan, the bay lights, the sensor confirms.
Release
R1
First viewport
1 primary · 1 notice · 3 blocks

What “Limited” means

SR-02 · since 09:41

Part of the rack can't be read. Bays there are Unknown: nothing is put in or taken out of them until the rack is read again.

Rack
SR-02 · High-capacity rack B
Place
Paste line · position 1
Controller
CTRL-S02 · online

Rename SR-02

Shown on the rack label and in every list.

Wiring v4 · changes

Drafted by Dávid Horváth under permit · 14:30

The wide dialog holds side-by-side content: a comparison, a table, a diagram.

Tier 3, bays 1–16
Controller port 2 → port 3
Tower
Moved from the left to the right end
Reads
Every 2 s (was 5 s)

Scan the reel label

Kiosk SMT-2

Station dialogs use the station scale: bigger type, 56 px buttons, 44 px targets.

Alerts

Faults and blocked work stay here until fixed.

A drawer travels in from the right and leaves faster. It always has a × button.

Rack
SR-02 · High-capacity rack B
Place
Paste line · position 1
Controller
CTRL-S02 · online

SR-02 · Paste line

The wide drawer: 760 px.

Record and floor side by side, a rack drawing, a long form.

Bence Tóth

Supervisor · Shift B

Signed in at SMT-2

Badge
BADGE-0101
Screens in
Magyar
Team
Shift B

Mark R-2026-0042 as missing?

It leaves the stock count until someone finds it.

Stock
−5 000 pcs of PN-0603-10K
Bay
SR-01-T3-L14 becomes empty
Undo
Scan it anywhere to bring it back

Recorded with your name.

Write at least 4 characters.

Clear SR-02’s bay map?

Every bay goes back to Unknown until the rack is read again.

Typed by hand, so it is a decision and not a reflex.

Send 12 waiting messages now?

ReelSave gets them in order.

Blocks

The structures pages are made of. Each is one function with semantic props; tone is data-tone, state is ARIA or native, and a variant is a prop. Pages never write these class names.

Sheet

The white surface: a hairline, 12 px radius and a resting shadow. A head, body and foot when it holds a titled group; a tone tints surface and border.

API
sheet(body, { pad, tone, surface: 'raised'|'flat'|'sunk'|'hero', as, label }) · sheetHead(title, { meta, actions, level }) · sheetBody(body) · sheetFoot(actions)
Keyboard
None of its own.
Name
A labelled div becomes role=region; a head names the group by its title.
Live region
None.
Target
Controls inside keep their own sizes.

pad sm · md · lg

pad sm
pad md (default for text)
pad lg

surface raised · flat · sunk · hero

raised
flat
sunk
hero (r5, lift shadow)

tone

info surface and border
ok surface and border
warn surface and border
bad surface and border

head · body · foot

Controllers

4 online · read 14:31

CTRL-S01 to CTRL-S04 answer on Line 2. The next health read is at 14:36.

Inset

The 8 px sunk panel inside a sheet or dialog: before/after boxes, a step-up, a range, a code well.

API
inset(body, { tone, pad: 'sm'|'md'|'lg' })
Keyboard
None.
Name
Not a landmark; its heading or first line says what it holds.
Live region
None.
Target
—

Before

SR-01 blinks on any fault.

pad sm

pad lg

tone info
tone ok
tone warn
tone bad

Section

A titled band of a page. The head carries a title, a meta line and actions; bands are separated by a hairline. Level 3 for sub-bands.

API
section({ title, id, level: 2|3, meta, actions, flush, done }, body) — done: a finished section rests as its title and one receipt line (What shows when)
Keyboard
None.
Name
aria-labelledby its title when it has an id (a named region).
Live region
None.
Target
Head actions are 36–44 px buttons.

Racks on Line 2

The section body.

Flush band (no block padding)

Used at the top of a sheet or drawer.

List, row and group

Every entity row family: fleet, candidates, inbox, returns, queue, results, settings rows. One row recipe with lead, title + sub, meta, aside and actions; the grid template (cols) is the only per-page variation. Under 640 px of its own width the meta wraps under the title and actions go to the end.

API
list(rows, { label, as, cols, density, divided, surface: 'sheet'|'none', foot, busy, leadStack, align: 'center'|'start', stackBelow: 'sm'|'md' }) — under 640 px of its own width (480 with stackBelow 'sm') a cols list becomes text + actions, actions at the end · listRow({ lead, title, sub, meta, aside, actions, detail, open, more, form, tone, state, current, href, changed }) — at rest the sub clamps at 2 lines and the detail folds behind Details (see What shows when) · listRow.skeleton(n) · listGroup(label, { count, meta, tone, level })
Keyboard
Tab reaches the row link, then the row's own controls.
Name
The list is named (aria-label); a whole-row link is named by the title; group heads are headings.
Live region
None; rows that change flash is-changed, and the page announces what changed.
Target
The whole row is the link target; row buttons are 36 px.

default rows (flex): lead icon tile · title + sub · meta · actions

  • Needs attention 2

    oldest 18 min
  • R-2026-0042 · PN-0603-10K
    Put in at SR-01-T3-L14, not seen by the bay sensor
    Not confirmed · 18 min
  • R-2026-0317 · PN-0402-1U
    Reported missing at SR-02-T1-L03
    Missing · 6 min
  • Working normally 2

  • R-2026-0419 · PN-0805-4K7
    Stored at SR-01-T2-L07
    Stored
  • R-2026-0420 · PN-0603-10K
    Taken out for WO-260714-07
    In use
4 of 128 reels

cols: a content grid (the fleet row) · whole-row link · current · changed

density compact · no surface · done state

  • Scan the reel label
    14:28
  • Put it in SR-01-T3-L14
    14:29
  • Wait for the bay sensor
    —

density roomy · a detail under the row: folded at rest (What shows when, rule 2)

  • PN-0603-10K
    10 kΩ 0603 · 3 reels short for WO-260714-07
    Details

leadStack · align start: a wide rack glyph lead goes above the text under 640 px; rows with tall facts stay top-aligned

  • SR-02 · High-capacity rack B
    SMT Line 2 · position 2

    4 × 32 bays86 reelsTier 3 bays 1–16 Unknown since 09:41

    Limited
  • SR-01 · Reel rack A
    SMT Line 2 · position 1

    5 × 32 bays118 reels

    Online

loading: listRow.skeleton(3) in a busy list

tones on rows (the words carry the state)

  • A info row
    info
  • A ok row
    ok
  • A warn row
    warn
  • A bad row
    bad
  • A idle row
    idle

Table

Tabular data in a named, focusable scroll region. Numbers align right in head and cells; sortable heads carry aria-sort and a link. Group rows are one th across the columns. stack turns rows into cards under 640 px, using each cell's data-label.

API
table({ label, head: [{ label, align, sort, href, width, hideLabel, current }], body, foot, density: 'compact'|'dense', stack, stickyHead: true|'page', stickyFirst, stickyFoot, rowLinks, flat, caption, busy }) · tableGroup(label, colspan, { tone, meta, cells }) · sortHeader(col) · tableSkeleton(cols, rows)
Keyboard
Tab stops on the scroll region (arrows scroll it), then on sort links and row controls.
Name
The region and the table carry the label (a hidden caption unless one is shown).
Live region
None; aria-busy while skeleton rows show.
Target
Sort links 24 px tall; row buttons 36 px.

sortable head · group rows · tone rows · foot · first column sticks

Reels on SR-01
ReelPartBayQtyStateActions
Needs attention1 reel
R-2026-0042PN-0603-10K10 kΩ 0603T3-L144 200Not confirmed
Stored
R-2026-0419PN-0805-4K74.7 kΩ 0805T2-L072 950Stored
R-2026-0420PN-0603-10Kthe one you came for (aria-current)T3-L155 000Stored
3 reels12 150

current column (aria-current on its head) · summary group rows (a cell per column) · sticky foot

Decisions by shift (example)
RightShift AShift BShift C
Floor3 of 33 of 32 of 3
Put inYesYesYes
Decide a countYesYesNo
Setup0 of 21 of 20 of 2
Change a rackNoYesNo
Rights per shift342

rowLinks on a stickyFirst table · a row picked by its checkbox · a done row (data-state)

Work orders (example)
PickOrderLineState

compact · stack (cards on a phone) · visible caption

Due back today
ReelOrderDue
R-2026-0311WO-260714-0715:10
R-2026-0312WO-260714-0916:40

dense · busy (skeleton rows)

Reels (loading)
ReelPlacePcs

stickyHead: the head sticks inside a height-limited region

Audit rows
TimeWhatWho
14:31Reel R-2026-0420 stored at SR-01-T3-L14Márta Kovács
14:30Reel R-2026-0421 stored at SR-01-T3-L15Márta Kovács
14:29Reel R-2026-0422 stored at SR-01-T3-L16Márta Kovács
14:28Reel R-2026-0423 stored at SR-01-T3-L17Márta Kovács
14:27Reel R-2026-0424 stored at SR-01-T3-L18Márta Kovács
14:26Reel R-2026-0425 stored at SR-01-T3-L19Márta Kovács

Spec and facts

Key/value: spec is the dt/dd list (cols: key column + value; rows: ruled rows with values at the end). facts is label-over-value in a grid, with an optional second line and provenance.

API
spec(rows: [k, v, { tone, mono, stale }?][], { layout: 'cols'|'rows', size, align, keyWidth, label }) · facts([{ k, v, sub, src, tone }], { cols, size: 'sm'|'md'|'lg' })
Keyboard
None.
Name
dt names each dd; a labelled spec is a named group.
Live region
None; stale values dim and keep their "as of".
Target
—

cols (default)

Reference
EV-26091-0412
By
Márta Kovács
Confirmed by
Bay sensor T3-L14
At
14:31:07

rows (ruled, values at the end) · tone

Bay light
Green, steady
Sensor
Not seen
Fits
Up to 15″ reels
Tower
Yellow

rows · sm · start · fixed key width · stale value

Requires
stock.adjust
Checked
14:36 by the server
Last read
14:02

facts md (3 columns) · sub · provenance

Part Label
PN-0603-10K10 kΩ 0603 resistor
Quantity Counted
4 200 pcsof 5 000 on the label
Where
SR-01-T3-L14Reel rack A · tier 3

facts sm (4 columns) · lg (2 columns) · tone

Order
WO-260714-07
Line
SMT-2
Due
15:10
Short
3 reels
Put in
42 reels
Taken out
37 reels

Meters

The hairline-ruled strip of readings, each a stat with its key and provenance. The only metric pattern: never a KPI card mosaic.

API
meters([{ label, value, unit, foot, tone, extra }], { size: 'sm'|'md'|'lg', cols, label })
Keyboard
None.
Name
Each reading's key names it; a labelled strip is a group.
Live region
None; stale readings dim ([data-stale]).
Target
—
Reels in racks
1 284
Sensors · 14:31
Agree with the record
99.2%
10 differences open
Short for today
3parts
Planning · 14:30
Bays free
212
of 608

size sm (tallies)

New
118
Updated
42
Rejected
3
see rows 12, 40, 97

Bar

One bar/track recipe: a fill; stacked segments (with the pattern language: unknown = grey hatch, diff = amber hatch, draft = not final yet, outline = waiting, short = red outline); placed ranges on a scale; ticks for need, minimum, now or today.

API
bar({ value, max, tone, label, size: 'xs'|'sm'|'md'|'lg'|'xl', segments: [{ value, tone, kind, at }], marks: [{ at, label, tone }], width, role: 'img'|'progressbar'|'none' })
Keyboard
None.
Name
role=img with the full sentence (label); role=progressbar for a running task; none when the text beside it says it all.
Live region
None.
Target
—

sizes xs · sm · md · lg · xl

tones (fills are 3:1 against paper)

segments: done · part · unknown · diff · draft · outline · short

placed ranges · marks with labels · progressbar · fixed width

width: 96px

Timeline

One timeline for the audit trail, the ledger and the shift lane: mono time, a pip on a line, then the body. A square pip is a step Janus decided, a round pip one a sensor confirmed, a ring one still open.

API
timeline([{ t, datetime, title, sub, tone, pip: 'square'|'round'|'ring', who, action, current, state }, { group, meta }], { density: 'compact'|'default'|'ledger', label, stickyGroups, groupLevel })
Keyboard
Tab reaches actions in the rows.
Name
An ordered list, named by label; group heads are headings.
Live region
None; the page announces new entries.
Target
Row actions 36 px.

default · tones · pips · group · current · archived

  1. Today

  2. 14:31
    Reel stored at SR-01-T3-L14
    Bay sensor confirmed
    Márta Kovács
  3. 14:29
    Put-in started
    R-2026-0042 · PN-0603-10K
  4. 14:12
    Sensor did not confirm
    Sent to Review
  5. Yesterday

  6. 16:02
    Tower rule changed
    Replaced by v14
  7. 15:40
    Controller CTRL-S02 offline

compact · ledger

  1. 14:31
    Stored
  2. 14:30
    Scanned
  3. 14:29
    Assigned bay
  1. 14:31:07
    Reel R-2026-0042 stored
    SR-01-T3-L14 · EV-26091-0412
  2. 14:30:52
    Reel label scanned
    SMT-2 station
  3. 14:12:40
    Count difference 1
    SR-02-T1-L03

Steps

One stepper: an ordered list with the current step aria-current="step" and a screen-reader word for done, needs attention and skipped. Horizontal bars, numbered, or a vertical path; horizontal steps keep only the current step's words when narrow.

API
steps([{ label, sub, state: 'done'|'now'|'warn'|'todo'|'skip', href }], { label, orientation: 'h'|'v', numbered, compact, words, done })
Keyboard
Tab reaches step links (a wizard path).
Name
The ol is named by label; state words are in the text.
Live region
None; the page moves focus to the new step title.
Target
Step links are the label text (≥ 24 px row).

horizontal · every state

  1. Scanthe reel label (done)
  2. Put inSR-01-T3-L14 (done)
  3. Confirmthe bay sensor
  4. Labelprint if asked (skipped)
  5. Close

numbered · warn

  1. Upload (done)
  2. Map columns2 columns unmatched (needs attention)
  3. Review
  4. Commit

compact · narrow (only the current step keeps its words)

  1. Read (done)
  2. Compare (done)
  3. Decide
  4. Close

vertical · numbered · links (a wizard path)

  1. Welcome (done)
  2. Name this siteLine 2 · Budapest (done)
  3. RacksConnect the first rack
  4. People
  5. PrintersLater (skipped)

vertical · pips

  1. Controller found (done)
  2. Address set (done)
  3. Bus checkBoard 3 did not answer (needs attention)
  4. Self-test

Disclosure, technical details, versions

Native details with one chevron. Plain rows, a compact line (technical details), a sheet card with an aside in the summary, or a ghost-button trigger. Versions: who changed a setting, and a way back that makes a new version.

API
disclosure(summary, body, { open, variant: 'plain'|'sheet'|'button'|'compact', flush, aside, level, name }) · techDetails(rows, { summary, open }) · versions(rows: [{ version, when, by, note, current, changed, changes }], { summary, meta, goBack, seeChanges, inUse, open })
Keyboard
Enter or Space on the summary toggles; the summary is one tab stop.
Name
The summary text names it; no heading or control inside a summary.
Live region
None (expanded state is native).
Target
Summaries are 36–52 px tall.

plain · open

What happens to the reel

It stays in SR-01 until someone takes it out for an order.

level 2 · flush

Advanced

Section-sized summary.

button (a ghost trigger)

Type or paste a code

A form goes here.

techDetails

Technical details
Reason
Right missing
Requires
planning.material.view
Checked
14:36 by the server

sheet · aside

Others on it2 people

Bence and Anna are in SR-02 now.

versions · go back · see changes

Versions · 3Who changed it, and a way back
  1. v14Blink only on blocking faultsEszter Szabó · today 09:12In useSee changes in v14
  2. v13Tower yellow for low stockBence Tóth · 22 SepSee changes in v13
  3. v12First rulesEszter Szabó · 2 SepSee changes in v12

Facets, then, status line

Facets: one physical fact and the places it shows, dot-separated. thenLine: the consequence before commit. statusLine: the "safe line", a square, a bold lead and a sentence; a live line is in the page (empty) before it speaks.

API
facets(parts) · thenLine(text, { lead }) · leadLine(lead, text) · statusLine(tone, lead, text, { live })
Keyboard
None.
Name
Plain text.
Live region
statusLine with live is role=status (polite), rendered even when empty.
Target
—

facets

Count difference 1Reported missingBay light red

thenLine · leadLine

Then: The reel goes back to SR-01 and the order is short 1 reel.

1 decision covers it: release 350 pcs of R-2026-0066 from quarantine.

statusLine · every tone

Safe to take. The bay sensor sees the reel.

Next: Scan the reel label.

Not yet. The bay sensor has not seen it.

Stop. This reel is quarantined.

Waiting for the scanner.

Callout and notice

Callout: an inline question or note with its actions (rack mismatch, ask for a right, locked until known, dispatch, supervisor deciding, draft note). Notice: a tinted surface with an icon, a 650 title and a body; spacing comes from the parent stack. Under 560 px its trailing actions drop under the words, aligned with them.

API
callout({ tone, body, title, icon, actions, layout: 'row'|'stack', live, unknown }) · notice({ tone, icon | false, title, body, actions, size: 'sm'|'md'|'lg', role: 'status'|'alert', trailing, titleLevel })
Keyboard
Tab reaches the actions.
Name
The title or first sentence says what it is; icons are decorative.
Live region
role=status or role=alert only when the notice arrives after load (never on a swapped container).
Target
Actions 36 px.

callout · unknown (dashed frame: no report yet)

Usage rate Unknown

No readings from SMT Line 1 counters since 13:48.

callout row (info) · stack (idle, the sunk ask-for-a-right) · warn with icon

This bay is on SR-02. Your working rack is SR-01.

You need the right to change bay lights

Eszter Szabó can give it to you.

Locked until known. The last write to CTRL-S02 has no answer yet.

notice · every tone · title + body + actions

Scanner: last read 14:31

Scan the reel label to begin.

Import finished

118 new parts, 42 updated.

Out of date

Planning last answered at 14:02.

Could not load reels

The server did not answer in 3 s.

View only

Your role shows this page without controls.

size sm · no icon · size lg (a verdict)

Reel label and master data disagree on quantity.
Patterns are examples, not rules.

Test passed

ERP answered 200 in 412 ms and accepted the sample message.

Context bar

The compact context line in a page head's actions: what you are looking at, what stays as it is, and at most one action ("Viewing SR-02 · working rack stays SR-01 · Make working rack"). Quieter than a notice: no tone, one line, 44 px.

API
contextBar({ icon, text, action, live, id })
Keyboard
Its one action is a normal button or link.
Name
Its words; the icon is decorative.
Live region
live: true when the line changes after a swap (the working rack switched).
Target
The action is a 36 px button inside the 44 px bar.
Viewing SR-02 · working rack stays SR-01
Viewing SR-01 · your working rack

Block options

sideNav items can open a dialog (opens) or be listed but not pickable (disabled). actionbar({ attached }) sits flat on its sheet. codeBlock(text | Markup, { copied }) marks parts and says its own copy words. secret({ countdownLabel }) names its timer ("Hides in 28 s"). steps({ orientation: "v", collapse: "h" }) becomes a scrolling row on phones; steps({ keepLabels }) keeps every label in narrow containers. table({ stack }) cells take data-col="status|meta" roles on phones; tr[aria-current] wears the selected look.

API
sideNav(label, [{ href | opens, disabled, … }], current) · actionbar({ attached }) · codeBlock(text | Markup, { copy, copied }) · secret({ countdown, countdownLabel }) · steps(items, { orientation: 'v', collapse: 'h', keepLabels }) · td[data-col]
Keyboard
An opens item is a native invoker button; a disabled item is not focusable.
Name
Their words; the secret timer is named "Hides in".
Live region
None (the timer is not live).
Target
Nav rows 40 px (44 on touch).

sideNav · opens a dialog · disabled

steps · vertical, a row on phones · keepLabels

  1. Rack (done)
  2. Shape (done)
  3. Controller
  4. Wiring
  5. Check
  1. Draft (done)
  2. Approved (done)
  3. In use
  4. Retired

codeBlock with marked parts and its copy words

POST /sap/orders 422
E_MATNR unknown: PN-0603-10K

secret with its timer words · actionbar attached

JNS-7Q2F-K9Hides in 30 s

Editor body

Unsaved changes

Action row

A 72 px row that is one link or button: an icon tile, a title, a sub line that wraps, a chevron (station "More options", the handheld lists). Stack them.

API
actionRow({ icon, title, sub, href | type, name, value, trailing })
Keyboard
One Tab stop per row (a link or a button).
Name
Title then sub.
Live region
None.
Target
72 px rows.
Find a reelLights the bay it is in, on the working rack or another

Action tile

A glove-scale start tile that is one link or button: an icon, a big title, a sub line and an aside chip (the LED colour the flow lights). The station home's "Put in" and "Take out". 80 px tall, 88 px on the station scale; the title follows the step-title token.

API
actionTile({ icon, title, sub, aside, href | type, name, value, id, attrs })
Keyboard
One Tab stop (a link or a button).
Name
The title (aria-labelledby); the sub and the aside describe it (aria-describedby).
Live region
None.
Target
≥ 80 px (88 on the station scale), full width.

Default scale

Find a reelLights the bay it is in

Station scale (data-scale="station")

Focus card

The one thing to do now, set above or beside the rest of a list: an accent ring, an optional eyebrow (status … act by), the title, a lead, a big figure (a number with its unit and label, or a figure of your own such as a bay address) with meta beside or under it, and one action on the right. Under 560 px of card the action drops under the words and fills the width. A tone rings it only when the card itself is the warning; the status word carries lateness.

API
focusCard({ title, eyebrow, lead, figure: { value, unit, label } | Markup, meta, action, tone, level, id, attrs })
Keyboard
Tab reaches the action (and any links in the lead).
Name
The title, a heading when level is given; the figure is text in reading order.
Live region
None; a page that repaints it announces the change itself.
Target
The action is the caller's button (xl on stations).

Number figure with an act-by bar · wide

Continue WO-260714-07

T2-L28 is lit blue at SR-01 · then Line 2 · feeder 14

18min

until feeder 14 runs out

Late · act now · 8 min · act by 14:24

Eyebrow and a bay figure · narrow, station scale

Late · act now · 8 minact by 14:24

Continue WO-260714-07 · item 1

Take R-2026-0012 from

T2-L28

Tier 2 of 5 · right half · SR-01
then → SMT Line 2 · feeder 14

No figure · tone warn

All 3 reels at SR-01 are on your cart.

Next stop SR-03 · T2-L09 · item 7

Bullet and icon lists

bulletList: sentences, each led by an 8 px tone square (what needs attention, coverage gaps, what a decision closed). iconList: rules and guards, each led by an icon. One or two columns (one under 700 px); ruled adds hairlines above and below.

API
bulletList(items, { tone, cols: 1|2, label, ruled }) · iconList([{ icon, text }], { cols: 1|2, label, muted })
Keyboard
Not focusable (links inside keep their own).
Name
A real list; label names it when the heading above does not.
Live region
None.
Target
Not a target.

bulletList · warn · two columns · ruled

  • Márta Kovács has no badge yet.
  • Two people sign in with a passcode only.
  • Shift C has no supervisor on Sunday.
  • László Varga screens in Hungarian; his team uses English.

iconList · muted

  • Nobody can remove the last Administrator.
  • A person who leaves keeps their records; only sign-in stops.
  • Every change is kept with who made it.

iconList · md · tones

  • Always kept: records, audit trail, users.
  • Can be cleared: stock and work data.

Empty and skeleton

Empty: an honest empty state with one way forward (inline for "none in this view"). Skeleton: a shimmer that stops when motion is reduced or paused; the label is plain text and announce() says when loading ends.

API
empty({ icon, title, body, actions, size: 'inline'|'sm'|'md'|'lg', level, role }) · skeleton({ lines } | { w, h }, { label })
Keyboard
Tab reaches the action.
Name
The title (a heading when level is given).
Live region
role=alert for "could not be loaded" only; skeletons are aria-busy.
Target
Actions 44 px.

md in a sheet

Nothing waiting

Reels that need a decision show here, oldest first.

sm · lg

No reel with that code

Check the label, or search by part.

No racks yet

inline · skeleton lines · block

None in this view.

Loading reels…

Later · not in R1

A parked surface says so first: what it is, why Neo R1 leaves it out (the Neo source, cited) and what R1 does instead, with the way there. Parked means kept, not deleted (the owner's decision, 2026-09-28). The shell renders it under the head of every parked page and parked Preview state (park(id, later) in the page's routes, later on a Preview state); a page renders it compact where an R1 page keeps a parked part. A note on the quiet surface with the ink release rule down its edge: out of scope, not wrong, so never a warn tone, a hatch or a dash. Links inside it are the one sanctioned way from an R1 page into a parked one (tests/parking.spec.ts).

API
later({ what, why, source, instead, insteadHref, insteadLabel, size: 'page' | 'compact' })
Keyboard
Its one link, if any.
Name
role=note, named "Later · not in R1"; the facts are a description list.
Live region
None: it is there on arrival.
Target
The link is inline in text.

page (the shell, under a parked page's head)

Later · not in R1Neo 0033 §7 · 0058 §1

The station capture journal: scans, count marks and reports kept on the device while it is offline, sent later in order

Not in R1
R1 keeps no client-side queue or capture journal: a station without its connection stops and says so
R1 instead
The stop signal: “Not connected — nothing is being booked”, and every unanswered action reconciled by its intent key See the stop signal

compact (a parked part inside an R1 page)

Later · not in R1Neo 0040 §4

While I’m away: hand your supervisor duties to someone until a time

Not in R1
Delegation is Later in Neo
R1 instead
Access roles with scoped grants, changed on Access control

Receipt

After a write: a stamp (square or icon + outcome word), an optional lead sentence, By / Confirmed by / Reference, everything it closed, actions and technical details. Nested in a sheet, inset or dialog it drops its shadow. With focus it takes focus after the write.

API
receipt({ stamp: { tone, text, icon }, lead, rows, closed: [{ verb, text }], actions, details, focus, flat, changed, label })
Keyboard
With focus: tabindex=-1, focused after the write (data-focus); Tab continues into its actions.
Name
The stamp word states the outcome; a labelled receipt is a named group.
Live region
None; focus moves to it (the stamp is read).
Target
Actions 36 px.

ok · icon · lead · closed · actions · details

Recorded

R-2026-0042 is in SR-01-T3-L14.

By
Márta Kovács
Confirmed by
Bay sensor T3-L14
Reference
EV-26091-0412
At
14:31:07
  • Closed count difference 1
  • Off bay light T3-L14
  • Stock +4 200 PN-0603-10K
Technical details
Event
reel.stored
Controller
CTRL-S01 · board 3

square stamps: warn · bad · info

Needs review

By
Bence Tóth
Reason
Sensor did not confirm in 8 s

Rejected

By
Eszter Szabó
Reference
EV-26091-0398

Outcome unknown

Sent
14:30
Last answer
none yet

nested in a sheet: flat automatically

Request sent

Sent

To
Eszter Szabó
At
14:36

Legend

One key for racks, bars and diagrams. Swatch specs follow the pattern language: dashed = Unknown, amber hatch = a Difference; a lit bay is its LED colour with a ring.

API
legend([{ swatch: 'cell:occ'|'cell:target:green'|'seg:short'|'seg:bad-hatch'|'cover:<kind>'|'led:amber[:blink]'|'dash'|'hatch'|'tone:warn'|'pip:round'|'mark'|'due'|'band', label }], { label })
Keyboard
None.
Name
A named list; swatches are decorative, the words carry the meaning.
Live region
None.
Target
—

cover keys · due · band · bad hatch

  • Available
  • Taken out
  • Short
  • Unknown
  • Due
  • Planned window
  • Late

rack cells

  • Empty
  • Occupied
  • Reserved
  • Difference
  • Fault
  • Unknown
  • Off
  • Decided
  • Lit (green)
  • Found

bar segments

  • Available
  • Reserved
  • Waiting for review
  • Unknown
  • Difference
  • Projected
  • Short
  • Taken out
  • Being put in
  • On the way
  • Quarantine

LEDs · tones · pips · marks

  • Green
  • Amber
  • Red, blinking
  • Recorded
  • Needs review
  • Decided
  • Sensor-confirmed
  • Need
  • Unknown
  • Difference

Action bar and changes

The action bar is the one recipe for drafts, bulk actions and stop bars: pinned to the bottom (it floats, so it carries the float shadow) or in the flow. changeList is the one review-of-changes presentation: ruled rows or a table, grouped by area; change() is one inline was → now for a table cell or a sentence.

API
actionbar({ text, actions, tone: 'draft'|'danger'|'neutral', sticky, live, label, icon }) · changeList([{ label, from, to, group }], { layout: 'rows'|'table', label, head, words }) · change(from, to, { was, now })
Keyboard
Tab reaches its buttons in order (secondary first, then the primary).
Name
A labelled bar is a named region; del/ins carry "was"/"now" for screen readers.
Live region
live: the text is role=status (a bulk count); keep the bar itself out of outerHTML swaps.
Target
Buttons 36–44 px.

draft (navy) · inline

3 changes in 2 areas · not saved

neutral · live bulk count

4 reels selected

danger · pinned inside its box (sticky)

The bar sticks to the bottom of the viewport while this box is on screen.

Stop all bay lights on SR-01? The operators lose their guidance.

changeList rows · groups

Tower lights

Blink when
was Any faultnow Blocking faults
Yellow means
was Low stocknow Low stock or a difference

Sound

Quiet hours
was Nonenow 22:00–06:00

changeList table

Changes by setting
SettingNowAfter
Tower lights
Blink whenwas Any faultnow Blocking faults
Sound
Quiet hourswas Nonenow 22:00–06:00

change · inline, in a sentence and a cell

Tier 3 boards was 5, 6 now 10, 6 in the draft.

Tier 3 wiring (example)
TierSide A
T3was 5, 6 now 10, 6

Code, files, secret, QR, check list

Data blocks: a mono well that scrolls (with Copy), an export file list with sizes and checksums, a value shown once with a visible time limit, a pairing code, and may/never lists.

API
codeBlock(text, { max, copy, label, wrap, id }) · fileList([{ name, href, meta, sum, icon }], { label }) · secret({ value, state: 'masked'|'revealed', countdown, label, actions }) · qr(seed, { size, masked, label }) · checkList([{ ok, text }], { label, words })
Keyboard
The code well is a tab stop (arrows scroll it); Copy is a button; file names are links.
Name
The well is a named region; the QR is role=img with its purpose; check marks read Yes/No.
Live region
Copy confirms with a toast; the secret's countdown ticks visibly (no live region).
Target
Copy and secret actions 36 px.

codeBlock · copy · max 6 lines

{
  "event": "reel.stored",
  "reel": "R-2026-0042",
  "bay": "SR-01-T3-L14",
  "by": "marta.kovacs",
  "confirmedBy": "sensor:T3-L14",
  "at": "2026-09-24T14:31:07+02:00"
}

fileList

secret masked · revealed with countdown

Hiddenjx7Q-PL2m-88RtHides in 30 s

qr sm · md · masked · checkList

  • Yes: Reads bay sensors
  • Yes: Lights bays to guide
  • No: Never changes stock by itself

Navigation

crumbs only on a page about one thing, above its h1. viewNav is level 1 (underlined page tabs, directly under the head). tabs is level 2 inside a page (the segmented track; links, or radios that switch panels in CSS). sideNav lists views of one thing; navList is a route list with icon tiles; pager moves through pages of a list.

API
crumbs([{ label, href }], { label }) · viewNav(label, [{ href, label, key, count, tone, icon }], current) · tabs({ label, items: [{ label, href|value, count, tone, panel }], current, mode: 'link'|'radio', name }) · sideNav(label, [{ href, label, sub, meta, icon, count, tone, group }], current) · navList([{ href, icon, title, desc }], { label, plain }) · pager({ pos, prev, next, label })
Keyboard
Links: Tab and Enter. Radio tabs: arrows move and select (native radios); the panel follows.
Name
Each nav is named; the current item is aria-current (page, or true for in-page anchors); counts are in the link text.
Live region
None.
Target
Crumbs 24 px, page tabs 44 px, tabs 36 px (40 coarse), side nav 40 px, nav list 56 px, pager 36 px.

tabs, mode radio (panels switch in CSS)

R-2026-0042 · PN-0603-10K · 4 200 pcs

12 events since 2 Sep.

Label, then counted at SMT-2.

Compositions

Page-level arrangements built from blocks. Their ratios and breakpoints are fixed here, so pages only choose a width.

Page head

The one page head: crumbs above the h1 on a page about one thing, the page's explanation beside the title (K6: an ⓘ that opens a popover, or with panel the page help panel, a drawer), a badge (a status, never a session fact: View only and Support session are the top bar's), one sub-line that states something true now (a count, a state, a scope), a meta row, and the page actions on the right. The h1 names the section or the thing and stays the same on every tab; page tabs follow it.

API
PageHeader(title).explain(summary, body, { panel }).sub(text, { id }).crumbs(trail).meta([state, place] | part).tech(rows).badge(part).actions(part, attrs).tight().attrs(map).id(id).titleId(id).focus().level(n).render()
Keyboard
Tab reaches crumbs, then the actions.
Name
The heading is the page name; the badge sits beside it, outside the heading.
Live region
None; after a write that replaces the page, focus() lands on the heading.
Target
Actions 44 px; crumbs 24 px.

full: crumbs · badge · sub · meta · actions (shown here at h2)

WO-260714-07 · Line 2 paste

About this work order

A work order only creates demand. Each line takes the stock there is now and closes the rest short.

3 lines short for the 15:10 start

ShortAnna NagyPlannerDue 15:10

tight · title and actions only

Inventory

What is where, and how sure.

K0 · Page frame

Every desk page: .page → the head → the body region. The head is always the frame's first child (the shell puts the section tabs and a Later note right after it), .page takes no stack classes, sections space themselves, and a write or a live update swaps the region below the head, never the frame.

API
pageFrame({ head, width: 'default'|'narrow'|'md'|'wide', region, regionAttrs, body, id })
Keyboard
Source order: head, tabs, body.
Name
The h1 in the head names the page.
Live region
The region carries data-live; a swap keeps the head (and focus) still.
Target
—

a frame with one section (h2 here)

Counts

2 open · 1 with a difference to post

Open

The region below the head.

K1 · Index page

A list of one kind of record: the head (a count or scope, and the one create or import primary, which opens a drawer), level-2 views, one row of filters (filterBar(): search, one row of chips, the rest behind More filters, the count), then the results, open rows before final ones. The filter bar is one GET form: a chip or a field swaps the results and keeps the URL. On a phone the chips and More filters fold behind one Filters button, so the bar stays one row.

API
indexPage({ head, views, filters: filterBar({ id, label, action, hx, keep, search, chips, more, count, trailing, on }), body, region })
Keyboard
Search, then the chips (arrow keys within the radio group), More filters (a popover: Escape returns), then the results.
Name
The bar is a search landmark named by its label; the chips are a named group.
Live region
The results region swaps (hx-push-url); the count is an output.
Target
Chips 36 px (44 under a coarse pointer).

Demand, the work-order lens (h2 here)

Demand

4 open · 1 late

Demand (example)
DemandToDueState
Open · 2
WO-260714-07SMT Line 211:30In progress
WO-260714-09SMT Line 116:00In progress

K2 · Queue page

Work that needs a person, ranked by what they act on next, then severity, then scope: the head (the queue in one line), the lenses, ranked groups of two-line rows (a row opens its drawer; its action is secondary unless the row is the focus), then the done list folded. A healthy group compresses to one counted line.

API
queuePage({ head, lenses, groups, done: { summary, body }, drawers, region })
Keyboard
Rows are links to their drawer; the done list is a summary: Enter or Space.
Name
Each group is a heading; the done list is named by its summary.
Live region
The groups region re-renders as the record changes.
Target
Row actions 36 px.

a queue with one group and its done list (h2 here)

Review queue

3 open · 1 yours

  • Yours · 1

  • 1
    WO-260714-09 is 120 pcs short of PN-SMAJ24A
    Line 3 · closed short at allocation
    High
Resolved today · 4

The day's decisions, newest first.

K3 · Record page

One thing: crumbs back to its index, an h1 of its id and name, .meta([state, place]) with ids in Technical details, one primary among the actions → one summary strip → its sections in working order → related (a pager, not a second queue) → "What happened" last, folded.

API
recordPage({ head, summary, sections, related, history: { summary, body }, region })
Keyboard
Crumbs, actions, then the sections in order; the history is a summary.
Name
The h1 names the thing.
Live region
The record region re-renders as it changes.
Target
—

a demand record (h2 here)

RO-0120 · Line 2 buffer

AllocatedSMT Line 2 · buffer

Technical details
Created by
Bence Tóth · 14:20

Replenishment order · SR-03SMT Line 2 · bufferAllocated

Requested
1 reels
Allocated
1 reels
Confirmed
0 reels
Short
0 reels

Where its reel waits

R-2026-0034 in SR-03-T1-L08 · ready at SR-03

What happened

Show the history
  1. 14:20
    Created by Bence Tóth

K4 · Settings form

Titled groups of setting rows: a label, its control, one hint (an explanation goes behind the group's ⓘ, never a row's prose); edited rows are marked; one save for the whole draft (the draft bar). A person who may not change it sees the values as spec rows, not disabled controls. Policies is the reference.

API
settingsPage({ head, groups: [{ id, title, explain: { summary, body }, rows }], draft, after })
Keyboard
Rows in order; the draft bar last.
Name
Each group is a section heading; each control is labelled by its row.
Live region
The draft bar says the count of changes.
Target
Controls 44 px.

one group (h2 here)

Settings

Yours follow you to every device

Your preferences

Where your settings live

Yours follow you to every device. This station’s apply to everyone at it.

Pause blinking
Edited

K5 · Stepped flow

Commissioning, recovery, the site transition, first start and imports: only the current step is expanded, later steps are one status line each in the stepper, done steps rest as receipts, and the step title takes focus when the step changes. At desk the stepper is a rail beside the step; on a phone a strip above it.

API
steppedFlow({ label, steps: StepItem[], done, current: wizardStep(…), aside, orientation: 'v'|'h' })
Keyboard
The stepper links the reachable steps; focus lands on the step title.
Name
The stepper is a nav named by its label; the step is a section named by its title.
Live region
None; focus moves.
Target
Step actions 44 px.

a vertical flow (h2 here)

Step 4 of 6

Move it to 192.168.0.25

Split

Main plus a side column (layout classes). The side is 300, 380 or 460 px; left puts it first. It stacks at 1180 px (right) or 900 px (left).

API
split(main, side, { side: 'right'|'left', width: 'sm'|'md'|'lg', sideLabel })
Keyboard
Reading order is the source order: main, then side (left: side first).
Name
sideLabel makes the side a named aside (only when it is complementary at page level).
Live region
None.
Target
—

right · md (380 px)

Main

The work: the list, the form, the drawing.

Side

Context and help.

left · sm (300 px)

Side first

Main

Workbench

The station two-pane task surface (bench, stage, dock): a large stage and a sunk side pane in one r5 frame. 7fr/5fr, a 340 px side, or half and half; the side can come first or hide while choosing. It stacks at 1100 px.

API
workbench({ main, side, sideWidth: 'sm'|'md'|'lg', sidePosition: 'end'|'start', state, solo, label, sideLabel })
Keyboard
Source order: stage, then side (start: side first). The stage title takes focus on each step.
Name
label names the bench; sideLabel makes the side a named region.
Live region
The stage keeps one live status line (statusLine live) outside the swapped pane.
Target
Station controls 56 px (station scale).

md (7fr/5fr) · stage + lens

Put it in T3-L14

Next: Wait for the bay sensor.

  1. Scan (done)
  2. Put in (done)
  3. Confirm

SR-01 · Reel rack A

Bay light
Green, steady
Sensor
Waiting
Fits
Up to 15″

sm side, first (a picks list beside the stage)

Stage

Picks

4 of 11 taken

solo (the side hides while choosing)

Choose an order

Hidden

Master–detail

A sticky master list beside the selected item (rule list, parts, access levels, stored data). 300 px list by default (240 or 340); it stacks at 900 px.

API
masterDetail({ list, detail, label, listWidth: 'sm'|'md'|'lg' })
Keyboard
Tab through the list (links), then into the detail.
Name
label names the pair; the list is usually a named sideNav with aria-current.
Live region
None; selecting loads the detail (a URL).
Target
List items 40 px.

Supplier A reels

v14 in useMatched 412 labels this week0 conflicts

Part
P/N: (\S+)
Quantity
QTY (\d+)
Lot
LOT (\w+)

Outcome

After a write: the sentence and the next steps beside the receipt, with one receipt width (360 px). It stacks at 900 px.

API
outcome({ lead, actions, receipt })
Keyboard
Focus lands on the receipt (focus) or the lead heading; then the next steps.
Name
The lead heading or sentence; the receipt stamp states the outcome.
Live region
None; focus moves.
Target
Next-step buttons 44 px.

R-2026-0042 is stored.

The bay light went off and the count difference is closed.

Then: The next reel for WO-260714-07 is R-2026-0043 at SR-02.

Recorded

By
Márta Kovács
Confirmed by
Bay sensor T3-L14
Reference
EV-26091-0412
  • Closed count difference 1

Denied and lost

Denied: a missing right, who can give it, one request, technical details and places you can go (replaces the copied .denied/.dn-go). Lost: no page at this address, the address asked for, a search, the closest match and where you usually go.

API
deniedPanel({ what, lead, who: { name, ini, role }, whoLabel, need, ask, places, placesLabel, details, level, status }) · lostPanel({ title, lead, address, search, closest, places, placesLabel, level })
Keyboard
Tab reaches the request, the details summary, then the places.
Name
The title is a heading (h1 when the panel is the page); the places list is a named nav.
Live region
The request answers by swapping its button to "Request sent · 14:36" (focus stays on it).
Target
Request 44 px; places 56 px rows.

denied (under a page head: h2)

No access

Material planning isn’t part of your access.

Signed in as Márta Kovács, Operator. Planners and supervisors have this page.

Who can give you access

Eszter SzabóSite administrator

Right needed: see material planning

Open Floor
Technical details
Reason
Right missing
Requires
planning.material.view
Address
/planning/material

lost (h2 here; h1 when it is the page)

There’s no page at this address.

The link may be old or mistyped. Search for the reel, part or bay instead.

/inventory/reels/R-2026-00420

Closest match: R-2026-0042 · PN-0603-10K at SR-01-T3-L14

Wizard step

One step of a guided flow (setup, provisioning, import): an eyebrow ("Step 2 of 6"), the title, a lead, the body and the step actions (primary next, ghost back). The title takes focus when the step changes.

API
wizardStep({ eyebrow, title, lead, body, actions, level, wide, id })
Keyboard
Focus moves to the step title on each step; Tab continues into the body and the actions.
Name
The section is named by its title (aria-labelledby).
Live region
None; focus moves (the title is read).
Target
Step actions 44 px (56 on station scale).

with a body and actions

Step 2 of 6

Name this site

People see this name on every screen and label.

You can change it later in Settings.

wide · a list body

Step 4 of 6

Connect the first rack

  • CTRL-S01
    Found on 10.2.0.21
    Answering
  • CTRL-S02
    Found on 10.2.0.22
    No shape yet

Step title

The station step heading of a bench pane (brand type, fluid size). It takes focus after a swap (tabindex=-1; give data-focus). PageHeader: .sub(text, { phone: 'hide' }) drops the sub under 560 px; .focus('load') focuses the h1 on a page a write navigated to. workbench({ minHeight: false }) lets the main pane take its content's height; outcome() splits by its own width (container query).

Scan the reel label

Put it in SR-01-T3-L14

Domain blocks

The concept's signature blocks. Meaning is an attribute: bays carry data-kind, lit things data-mode and an LED colour, steps data-state. Dashed means Unknown only; the amber hatch means a difference to decide only; a light hatch in the series' own colour means not final yet.

Rack glyph

A rack drawn as tiers × bays wherever a location is named. Every former per-page --cell override, regex rewrite and JS fit is a prop: size presets, one container-query fit formula, elevation, ruler, labels, per-bay attributes, marked vs lit, caution and settle. Towers lie in a strip along the top edge, at their side (a lamp capsule, never a column above the rack), and scale with the cells; at thumb a tower folds to one dot, the lit lamp.

Call
rackGlyph(spec, { size, fit: { min, max, slots }, dim, variant, elevation, ruler, labels, cellAttrs, mark, caution, settle, pick, multi, label, decorative, quietTowers, blinkStyle, rack, id })
Keyboard
A drawing is one image. With pick it is an ARIA grid: one tab stop, arrows move, Home/End go to the row ends, Enter or Space picks (rack:pick).
Name
role=img with the page's words; tower states, lit bays and the marked area are added unless the label already says them. Pick cells are named "SR-01-T3-L14, occupied"; tier labels are row headers. decorative hides it when the row text says it.
Live region
None. What changes is said by the page (sensor line, toast).
Target
Pick grids are for pointer precision at lg/elevation; every bay stays reachable by keyboard and by the "Go to bay" field.
thumb · 4 px cells
xs · 7 px cells
sm · 11 px cells
md · 18 px cells
T2T1
lg · 30 px cells, bay numbers with labels: 'all'
T2T1
xl · 40 px cells, bay numbers with labels: 'all'
T2T1
Every bay state, lit colours and marks (size lg). Top: occupied, reserved, needs review, fault, Unknown (dashed), not in service, stencil, reel with no record, decided, this bay. Bottom: green steady, blue blink, red blink, amber slow blink; marked (accent, not lit); careful neighbour. Lit and marked bays keep the cell footprint: the fill plus an inset rim (a band of the colour, then a line in its dark shade); annotations (this bay, careful neighbour, selected, focus) sit outside as outlines.
T4T3T2T1T4T3T2T1
Two-sided rack, a tower per face
Double-height tier, 3-lamp towers, both off
T4T3T2T1
Stencil tower offline: Unknown slots, Unknown tower
Overlays drawn by CSS from the cell geometry: zone= with its label, cross=1 locator lines
Locator cross through a lit bay; the reel settles into it (settle)
Ghost: a rack not seen yet
Fit: fills its container (one formula; bounds 4–22 px here)
stretch: one tier across the width, with count badges
dim: occupancy fades so marked bays stand out
Fit on a shared bay scale (fit.slots 32): a 16-bay rack draws half as wide
  • Green · blinking
  • Red · slow blink
  • Amber · steady
  • Tower yellow · blinking
blinkStyle: 'split': a blink drawn still, corner to corner lit / off (bays and tower lamps). It is also what every blink becomes under reduced or paused motion and in print. Its key passes the same style.
T5T4T3T2T1
Elevation: a pick grid with tier row headers and a ruler (first, every 8th, last). cellAttrs marks the selected bay; arrows move, Enter picks.

Rack legend

The key under a rack: the same cells, lamps and marks at 12 px, each with its word. Pass a key, or [key, words] to say it in the page's own words.

Call
rackLegend(['occ', 'res', ['lit:green:blink', 'Put the reel in here'], 'stale', 'tower:ryg:green'])
Keyboard
None (not interactive).
Name
A list named "Key"; swatches are hidden, the words carry the meaning.
Live region
None.
Target
n/a
  • Empty
  • Occupied
  • Reserved
  • Needs review
  • Reel, no record
  • Fault
  • Unknown
  • Not in service
  • Decided
  • Marked
  • This bay
  • Careful: neighbour bay
  • Green · blinking — put the reel in here
  • Blue · blinking — take this reel out
  • Red · blinking
  • Amber
  • Marked area
  • Locator
  • Double-height tier
  • Stencil slot
  • Tower light

Rack notes

The rack glyph as the status picture of a station home. Each bay or tower lamp that means something carries a pin: the initials of the person it belongs to (the action ring when it is yours to act on), or an icon when nobody. The words (who is on it, since when, what to do) live behind the pin in a hint, and the light language behind a "?" under the drawing. Pins sit on their bay by CSS anchor positioning, so the drawing keeps one geometry; a band above the top tier holds the pins there and the towers' pins. Up to six bay pins and one per tower.

Call
rackNotes(spec, { id, notes: [{ at: 'T2-L28' | 'tower:l' | 'tower:r', ini | icon, mine, label, words }], glyph: RackGlyphOpts, caption, legend: { label, body } })
Keyboard
Each pin is a button in reading order (Tab); Enter or Space shows its words, Esc hides them. The "?" opens the legend the same way.
Name
A pin is named by its short label ("T2-L28, lit blue for you") and described by its words; the glyph keeps its own label naming every lit bay and tower.
Live region
None. The words are there when asked.
Target
44 px, 56 px at the station scale (the pin is drawn at 26–36 px inside its target).
T5T4T3T2T1
T2-L28 · lit blue for you
Take out R-2026-0012 · then SMT Line 2 · feeder 14.
T5-L22 · reel missing since 12:52
Bence Tóth is on it.
Left tower yellow · attention: T5-L22 reel missing.Right tower blinking blue · a take-out waits at this end.
SMT Line 2 · position 1 · this station's rack
  • Blue blinking · take this reel out.
  • Red blinking · a reel left without a take-out, or a fault.
  • Yellow tower · attention on this rack.
Station home: yours (MK, action ring), a missing reel someone is on (BT), the towers that explain themselves
T3T2T1
Yours: put in R-2026-0042.Oleh Bondar’s take-out · leave it.
On paper, two people at one rack: whose each lit bay is

Rack figure, card, strip and location

Compositions of the glyph. The figure puts a rack title and its place above it; the card is a to-scale thumbnail that links to the rack; the strip is one tier around the bay that matters, big enough to count; loc is the address lockup in a row.

Call
rackFigure({ title, place, spec, label }) · rackCard({ title, place, tag, status, spec, href, nums, current, tone }) · rackStrip({ rack, bay, width, lit, occ, caution, title, meta, label }) · loc(address, glyph?)
Keyboard
Card: one link (Tab, Enter). Figure, strip and lockup: not interactive.
Name
Card: its words (the glyph inside is decorative). Figure and strip: the glyph's label.
Live region
None.
Target
The whole card is the link target (≥ 44 px).
SR-01 · Reel rack A · SMT Line 2 · position 1
rackFigure

SR-01-T3-L14 · stored 14:20

loc: glyph + address in a row
Tier 3bays 11–17
Tier 2take out from 28, not 29

Stack light

A tower drawn lying down: one capsule, the lamps it has left to right in their top-to-bottom order on the device (red, yellow, green, blue, white). The same glyph sits in a rack glyph's top strip. Blink is 500 ms, slow blink 1 s (device truth); with motion paused or reduced (or blinkStyle: 'split') a blinking lamp is drawn split corner to corner, lit / off. Unknown is the dashed grey capsule, never "off".

Call
stackLight(lamps, on, { mode, size: 'xs'|'sm'|'md', label })
Keyboard
None.
Name
role=img "Tower light: yellow, blinking" (or the page's words); label: false when the row text says it.
Live region
None.
Target
n/a
md
sm
xs
blinkStyle: 'split': blink and slow blink drawn still (the form under reduced or paused motion)
Three lamps (SR-03): only the lamps it has
On a rack: left and right towers in the top strip (sm, xs); at thumb each folds to one dot: the lit lamp, a dim dot when off, dashed when Unknown

Covered-by bar

A quantity decomposed into its sources, in the order they can be used; the need is the tick. One colour encoding on every page: available ok solid · reserved accent · taken out grey · waiting for review accent outline · being put in a light ok hatch (not final yet) · on the way accent stripes · quarantine amber stripes · Unknown grey hatch · short red outline. The first source that closes the gap is outlined and its legend reads "covered" or "covers it".

Call
coverBar('reserved:480:WO-07|unknown:310:SR-02:Troubleshoot>/rack.html', 600, 'PN-QFN32-MCU7 coverage', { dense }) · coverKey(kinds) · coverCell(kind | 'part' | null, { as: 'span' | 'button', size, label, expanded })
Keyboard
The legend levers are links (Tab).
Name
The bar is one role=img with a full sentence ("480 Reserved, 310 Unknown; 600 needed").
Live region
None.
Target
Lever links sit in text.
  • 1 800 Available · SR-01, 3 reels
  • 480 Reserved · WO-260714-07
Covered by the first source
  • 480 Reserved · WO-260714-07
  • 310 Unknown · SR-02 tier 3 · Troubleshoot
  • 600 Being put in · R-2026-0121 · Open in Review
  • 350 On the way · from Store B
Covered by a later source ("covers it"), with levers
  • 200 Available
  • 350 Quarantine · bake needed
  • 120 Waiting for review
  • 230 Short
Short: the sources do not reach the need
  • 18 400 Available
  • 4 000 Reserved
  • 2 310 Taken out
A stock bar (no need)
  • Available
  • Reserved
  • Taken out
  • Waiting for review
  • Being put in
  • On the way
  • Quarantine
  • Unknown
  • Short
The key on its own
  • 480 Taken out · R-2026-0164
  • 120 Closed short · at allocation
Closed short: the end-stop of a demand line (terminal, never a severity)

    + 1 reel · quantity unknown · piece fulfilment unproved

    Piece fulfilment unproved: proven pieces, and the reels of unknown quantity beside the bar (no deficit invented)
    • 200 Available
    • 350 Quarantine · bake needed
    • 350 Short
    dense (inside a table cell)
    coverCell: one order item per square (a button opens what covers it)

    Pick batch, demand summary

    pickBatch(): the bays a station's pull lit together (Neo 0046 §4), one chip per task in walking order, the numbers the rack drawing's pins carry. Each chip: its lamp (device truth: blue blink), the reel and its line, the task's state as square + word, its own pick window as a drain, and while taken its Undo take-out with the undo window's countdown. On first appearance (ignite) the chips light in walking order, 40 ms apart; the lamps keep the hardware's one blink phase; a taken chip settles; an undone take-out relights one chip. Reduced motion: none of it, a cut. demandSummary(): any demand, from its source to its destination, requested / allocated / confirmed / short.

    Call
    pickBatch([{ bay, reel, line, takes, state: 'lit' | 'taken' | 'verify' | 'lapsed' | 'dark' | 'cancelled' | 'attention', light, window: { ms, elapsed }, undo: { seconds, total }, note, relit }], { dest, rack, ignite, id, label }) · demandSummary({ source, from, dest, unit, requested, allocated, confirmed, short, unproved, state, id, label })
    Keyboard
    Undo take-out is a button (Tab); nothing else in a chip is interactive.
    Name
    The batch is an ordered list named with its destination and how many are taken; each chip reads its number, bay, reel, line and state in order.
    Live region
    None: the station says each take-out once through its own live region.
    Target
    Undo take-out is a standard button (56 px on the station scale).
    1. T4-L12

      R-2026-0164 · line 4 · 480 pcs

      Lit

      Pick window 60 s
    2. T1-L25

      R-2026-0145 · line 6 · 900 of 3 000 pcs

      Lit

      Pick window 60 s
    3. T2-L28

      R-2026-0012 · line 1 · 4 000 of 4 820 pcs

      Lit

      Pick window 60 s
    Pulled: three bays lit, walking order, each with its pick window
    1. T4-L12

      R-2026-0164 · line 4 · 480 pcs

      Taken

      21 s

    2. T1-L25

      R-2026-0145 · line 6 · 900 of 3 000 pcs

      Window passed · still lit

      Its pick window passed: it stays lit and keeps watching.

    3. T2-L03

      R-2026-0170 · line 1 · 1 reel

      Taken · scan it

      Scan it to verify.

    4. T1-L20

      R-2026-0103 · line 3 · 360 pcs

      Waits · dark

    One taken (Undo take-out while its window runs), a window passed, a verification scan due, a waiting one dark
    1. T4-L12

      R-2026-0164 · line 4 · 480 pcs

      Needs attention · not lit

      SR-01 is not reporting. If you already took the reel, keep it.

    2. T1-L25

      R-2026-0145 · line 6 · 900 of 3 000 pcs

      Taken

    Needs attention: the rack stopped reporting mid-pick (Neo J02-F17). Nothing is lit, the window is suspended, the task keeps its claim

    Work order · MES · work-order importSMT Line 2

    Requested
    13 900 pcs
    Allocated
    7 780 pcs
    Confirmed
    6 000 pcs
    Short
    120 pcs
    The shared demand summary

    Work orderSMT Line 1

    Requested
    4 000 pcs
    Allocated
    360 pcs
    Confirmed
    3 120 pcs
    Short
    120 pcs

    1 reel issued with an unknown quantity: they prove no pieces, and no deficit is invented for them.

    With reels of unknown quantity (they prove no pieces)

    Seam, seam row, twin

    A movement is real only when the record and the floor meet. The seam shows its steps: square = Janus decides, round = a sensor (or scan) confirms; the last step closes once. The seam row says the same in one row of a list; the twin puts Record and Sensor side by side. Divergence never auto-resolves: it bends amber.

    Call
    seam({ label, steps: [{ name, who, t, lane, state, led }], compact, caption, deadline, lanes }) · seamRow({ record, sensor, tone, keys, sensorId }) · twin(left, right, { open, framed, level })
    Keyboard
    None (not interactive).
    Name
    The seam is an ordered list named by label; each step says its state ("Sensor, now, sensor-confirmed") and the current step has aria-current="step". The twin stamp names agree/disagree.
    Live region
    None in the component: the page announces the step that just closed (announce()).
    Target
    n/a
    1. Scanned, doneMárta Kovács14:20:03
    2. Recognised, doneR-2026-004214:20:03
    3. Bay lit, doneSR-01-T3-L14 green14:20:04
    4. Sensor, now, sensor-confirmedreel seen in the bay
    5. Stored, not yet
    • System step
    • Sensor-confirmed step
    Waiting on the sensor: the now pip is the blinking bay LED; the sensor window drains
    1. Scanned, doneMárta Kovács14:20:03
    2. Recognised, doneR-2026-004214:20:03
    3. Bay lit, doneSR-01-T3-L14 green14:20:04
    4. Sensor, done, sensor-confirmedreel seen in the bay14:20:09
    5. Stored, doneRecorded14:20:09
    Closed: every step done
    1. Scanned, doneMárta Kovács14:20:03
    2. Recognised, doneR-2026-004214:20:03
    3. Bay lit, doneSR-01-T3-L14 green14:20:04
    4. Sensor, needs review, sensor-confirmedreel seen in the bay
    5. Stored, not yet
    • System step
    • Sensor- or scan-confirmed step
    Bent amber into Needs review; a failed step
    1. Scanned, doneMárta Kovács14:20:03
    2. Recognised, doneR-2026-004214:20:03
    3. Bay lit, nowSR-01-T3-L14 green
    4. Sensor, not yet, sensor-confirmed
    5. Stored, not yet

    Step 3 of 5 · Bay lit · SR-01-T3-L14 green

    • System step
    • Sensor-confirmed step
    phone: line — one sentence instead of the steps under 560 px
    1. Scanned, doneMárta Kovács14:20:03
    2. Recognised, doneR-2026-004214:20:03
    3. Bay lit, doneSR-01-T3-L14 green14:20:04
    4. Sensor, needs review, sensor-confirmedreel seen in the bay
    5. Stored, not yet

    Step 4 of 5 · Sensor · reel seen in the bay · needs review

    phone: focus — only the step that matters under 760 px
    1. Scanned, done
    2. Recognised, done
    3. Bay lit, done
    4. Sensor, now, sensor-confirmed
    5. Stored, not yet

    Waiting for the sensor · push the reel fully in

    compact with its caption (handheld, rows)
    1. Scanned, done
    2. Recognised, done
    3. Bay lit, failed
    4. Sensor, not yet, sensor-confirmed
    5. Stored, not yet

    The bay did not light. Try again or report it.

    compact, bad
    1. Scanned, doneR-2026-018714:31:02
    2. Put in by hand, done, sensor-confirmedMárta Kovács · I put it in T4-L0714:31:20
    3. Sensor, done, sensor-confirmedreel seen in T4-L0714:31:21
    4. Stored, doneRecorded · manual14:31:21
    Manual operation (B10; Neo 0033 §6, 0038 §3): no light, so the person's step carries their initials in the pip; the sensor still closes it
    RecordR-2026-0042 in T3-L14SensorA reel in T3-L14
    seamRow · agree
    RecordR-2026-0088 in T5-L22SensorEmpty since 12:52
    seamRow · diverge
    Counted1 800 pcsScanned1 760 pcs
    seamRow · plain, with its own keys
    RecordPN-0603-10KCameraPN-0603-1K0
    seamRow · diverge, camera

    Record

    R-2026-0042 · 10 kΩ 0603 · 5 000 pcs

    Sensor

    A reel in T3-L14 since 14:20

    twin · closed, framed

    Record

    R-2026-0088 · 2 150 pcs

    Sensor

    Empty since 12:52

    twin · open (dashed amber)

    Removal watch

    An unexpected removal (B6; Neo 0026 §6–10, J05 §3.3): a bay went Clear with no task holding its reel. Open, it is red with the Missing delay draining to the minute Janus books it Missing; the reel pushed back into the same bay closes it with no movement. Booked, it rests as one line in the system's own voice (the square pip: Janus decides), Missing · R-… · By: Janus · 13:22, which opens its last bay, the delay and its entry. Under 560 px of card the open watch stacks, bay over words. Motion: the drain runs on the floor's clock; reduced motion keeps the bar, without the sweep. Station scale: the bay address takes the step size.

    Call
    removalWatch({ reel, bay, since, at, delay, drain: { ms, elapsed } | null, booked: { at, ev }, id })
    Keyboard
    None (not interactive): the page offers Found it or opens the review.
    Name
    Its words: the status word, the reel, the bay and the time; the drain is role=img, named in full (“Missing delay: 11 min left, until 14:51”): it counts down to a deadline, it is not a task’s progress.
    Live region
    None: the page announces a booking once (announce()).
    Target
    n/a
    T2-L10 Unexpected removal R-2026-0175 left its bay · 14:21 Missing at 14:51 unless it is back
    Open · the Missing delay draining
    Missing R-2026-0088By: Janus · 13:22
    Last bay
    T5-L22 · dark
    Booked at
    the Missing delay, 30 min
    Reference
    EV-2461
    Booked Missing by Janus at the delay

    Act-by, countdown, drain, clock

    Time as the floor needs it. Act-by: when to act, with the lead-time zone before run-out hatched (not final yet) and a now marker; late turns the time red. The countdown ring drains over the real window and turns amber at a quarter left; the text form sits in a sentence. The drain is the linear bar. The clock is the site clock; app.js keeps every one ticking from one time authority.

    Call
    actBy({ at, now, act, runsOut, late, label }) · countdown({ seconds, total, size, label, onZero }) · drain({ ms, elapsed, tone }) · clock({ size, time, date })
    Keyboard
    None.
    Name
    Act-by bar: role=img with a sentence. Countdown: role=timer named by its purpose ("Sensor window"). Clock: role=timer "Current time".
    Live region
    Timers are not live (they would speak every second); the page announces the outcome at zero.
    Target
    n/a
    Act by 14:24 · runs out ≈ 14:50
    actBy
    Act by 14:02 · runs out ≈ 14:40
    actBy · late
    8seconds
    sm
    30seconds left
    md
    60seconds
    lg
    2seconds
    ≤ 25 % left (amber)
    0not confirmed
    over

    Lit amber · off by itself in 30 s.

    text form
    drain
    drain · tone ok, started 2 s ago
    drain · tone warn
    :00Wed 24 Sep · Day shift
    clock · top
    Wednesday 24 September
    clock · hero

    Scan parts

    The presentational pieces every scan surface shares: the desk scan sheet, the kiosk, the handheld, the phone camera, the inbound and outbound benches. What a code is (resolveScan, canon) is not kit; these only draw the answer.

    Call
    scanPrompt({ title, sub, size, layout, as, level: 1 | 2 | 3 }) · resolveCode(raw, canon) → { kind, code, rack, cell, r, p } (pure; app/scan.ts passes the site canon) · codeEntry({ id, action, error, folded, size, focus }) · bayAddress(bay, { size, led, as, where }) · sensorLine({ state, text, sub }) · entityHead({ id, sub, qty, status }) · reelSummary(reel) · scanSheet({ code, status, facts, says, rack, lit }) · scanLit({ text, seconds, off: { action, hx, fields }, live }) — at zero the countdown submits the Switch off form (#id-off)
    Keyboard
    Code entry: Tab to the field, Enter submits; the fold is a native disclosure (Enter/Space). A button scan prompt is one button.
    Name
    The code field has a label ("Code"); its error is wired by aria-describedby and aria-invalid. The bay address is a heading when it is the screen's title.
    Live region
    sensorLine is one stable role=status; a code error is role=alert; the lit line is role=status.
    Target
    Fields and buttons 44 px (56, primaries 64–72, at station scale).

    Scan a reel

    or the supplier label on the bag
    scanPrompt · md, stack

    Scan the reel to put it in

    scanPrompt · lg
    Scan your badgeHold it under the scanner
    scanPrompt · row, badge
    scanPrompt · as a button (handheld)
    Type or paste a code
    codeEntry · folded
    codeEntry · open, with a server error
    codeEntry · lg (station)
    SR-01-T3-L14

    T3-L14

    T2-L28

    bayAddress · inline, sm, md

    T3-L14

    SR-01 · tier 3, bay 14 from the left
    bayAddress · lg with its where line

    T3-L14

    bayAddress · hero (kiosk)
    Waiting for the sensorPush the reel fully in · the screen moves on by itself
    Seen in T3-L1414:20:09 · the bay light went off
    Not confirmed yetThe sensor did not see a reel within 8 s
    Sensor not reportingTier 3 since 09:41
    R-2026-0042 Stored
    PN-0603-10K · 10 kΩ 1 % 0603 · Yageo
    5 000pcs
    entityHead
    R-2026-0042 Stored
    PN-0603-10K · 10 kΩ 0603
    5 000pcs
    Where
    SR-01-T3-L14
    reelSummary
    R-2026-0042Stored
    Part
    PN-0603-10K · 10 kΩ 0603
    Quantity
    5 000 pcs
    Where
    SR-01-T3-L14
    SR-01 · Reel rack A
    SR-01-T3-L14 lit amber · off by itself in 30 seconds
    scanSheet with the bay lit (scanLit)
    Reel ID
    R-2026-0042 Label
    Part number
    PN-0603-10K Label
    Lot
    LOT-2614-042 Label
    Quantity
    5000 Label
    labelFacts: a published label format's reading, each value with where it came from (MH10.8.2, v1)

    Station parts

    The kiosk, handheld, phone camera, sign-in and setup wizard share one frame, keypad, set of PIN dots, result mark and bottom nav. The frame wears data-scale="station", so type, controls, icons, avatars and LEDs step up by token; no page restates a size.

    Call
    faces([{ id, body, hidden }]) · kit.js crossfade(from, to) · [data-xfade-show="#id"] · kit.js ack(frame, 'ok' | 'warn') (the frame's edge flashes for a read) · kit.js seedKeypad(target, value, fresh) · kit.js identify(rackEl) (tiers light in turn, then two flashes; still under paused motion) · twin(l, r, { open, seamLabel: [agree, disagree] }) · keypad({ target, size, value, unit, steps, fn }) · pinDots(n, { filled, tone, shake }) · resultMark(tone, { size }) · bottomNav({ label, items }) · stationFrame({ head, body, bar, nav, presence })
    Keyboard
    Keypad keys are buttons (Tab, Enter/Space) that type into their target (aria-controls): app.js wires every [data-keypad] (kit.js keypads()). Digits append up to maxlength, Clear empties, Delete removes the last digit, a step adds its number; the field then gets an input event (gates sync) and the value box updates. A page that owns a key cancels the opus:keypad event ({ key, step, target }). A hardware keyboard types into the field directly. Bottom nav: Tab between places.
    Name
    Delete is named "Delete last digit"; the value box is the target's label. PIN dots are hidden: a polite "3 of 6 digits" beside them says the count. The result mark is decorative beside its heading.
    Live region
    The frame carries one role=status for the station's announcements.
    Target
    Keys 64/72/80 px; nav items 64 px (72 at station scale).
    keypad · sm with a value box (handheld count)
    keypad · md, passcode (Letters · 0 · Delete)
    keypad · lg with steps (kiosk)
    pinDots · empty, 3 of 6
    pinDots · wrong (bad, shake)
    resultMark · ok, warn, bad, info; md and lg
    bottomNav · in-place panes (buttons), a count
    bottomNav · links with the round scan place (phone bar)
    Take out · reel 1 of 3

    SR-01-T3-L14

    Station type and crossfade: under data-scale="station" the .display and step-title tokens follow the frame's width (container units on the frame's main area): ≥ 64 px on the 1280 × 800 kiosk (NFR-E3), 72 on a wider wall screen, 32 px on a 480 px handheld. faces() stacks faces in one place; kit.js crossfade(from, to) (or a [data-xfade-show="#id"] button) crosses them: the bay rises out, the destination grows in. Instant under reduced or paused motion.
    SR-01 · Reel rack A
    :00

    Put the reel in

    T3-L14

    SR-01 · tier 3, bay 14
    Waiting for the sensorPush the reel fully in
    stationFrame · inline preview, presence register: head, body, the thumb bar and the nav

    Thumb bar

    The bar in a station's thumb zone, one order on every task screen: Back first, then what the floor is doing now (the sensor line with its live LED, the scanner's state, a retry line), then a More menu button and the one primary last. Any part may be empty. Under 720 px the status takes its own row above the buttons.

    Call
    thumbBar({ back, status, end, attrs }) // in stationFrame({ bar })
    Keyboard
    Its controls in reading order: Back, then the end buttons.
    Name
    The frame names the bar region ("Actions"); each control by its words.
    Live region
    The sensor line inside is not live (the frame's one status region speaks).
    Target
    Station controls: 64–72 px.

    T3-L14

    Waiting for the sensor at T3-L14Push the reel fully in · the screen moves on by itself
    A put-in waiting for the sensor

    Stored.

    Reel seen at T3-L14 · 14:32Recorded
    A result: the status, then the one primary

    Stop signal on a station, last actions

    A station keeps no queue: while it is not connected nothing is booked. stationFrame({ stop }) puts the stop signal where the person acts: the kiosk's veil over its task pane (must-read size), the handheld's 56 px strip over its thumb bar. The thumb bar (and, under the veil, the pane) is [data-needs-live]: inert while not connected. lastActions() lists what this session sent and what became of it: Sending…, Recorded, Not recorded, Not booked (never sent), Outcome unknown (sent, no answer: asked about, never resent; it settles once, data-settled). Kit.js record() keeps each write's intent in the session before it is sent.

    Call
    stationFrame({ stop: { form: 'veil' | 'strip', body, state, detail } }) · lastActions([{ label, at, state, reason, settled }], { id, label, empty }) · kit.js record(), intents(), settleIntents() · intent:change · intent:settled
    Keyboard
    Nothing to press on the signal; the inert thumb bar takes no focus while not connected.
    Name
    The signal says "Not connected — nothing is being booked", then what the device does and "as of"; each last action its words, stamp and time.
    Live region
    None on the list: app.js says stopping and coming back once; a settled Outcome unknown is said once (station-kit.js).
    Target
    The strip is 56 px on the station scale; the veil words are must-read (64 px on the kiosk).

    Scan the reel.

    Put in · SR-01

    Not connected — nothing is being bookedReconnecting… · as of 14:32

    Take out T2-L28

    Not connected — nothing is being bookedOffline · as of 14:32

    Last actions

    1. Take-out of R-2026-0012Outcome unknown14:31 · sent, no answer yet · asked again when Janus is back
    2. Scan · R-2026-0036Not booked14:32 · never sent
    3. Put-in of R-2026-0109 at T4-L31Recorded14:28 → 14:33
    4. Report · label X7Q-2231-BNot recorded14:27 · Janus never got it
    lastActions: each stamp once; an Outcome unknown settles into Recorded or Not recorded

    Later · not in R1The station capture journal (Neo 0033 §7, 0058 §1): kiosk.html?state=journal and handheld.html?state=journal draw it. Nothing paints or sends from these.

    Offline · 2 waiting to send

    Not sent yet

    Kept on this device and sent in order when the connection is back: scans, count marks, reports, and your word that a reel is in. None of them moves a reel on the record: the sensor still has to agree.

    1 waiting to send

    1. Scan · R-2026-0036Kept 14:33 · not sent

    Waits for the network

    • Put in and take outA bay can't light and its sensor can't be read, so nothing moves on the record.

    Follow the light, manual bays

    Commissioning qualifies a rack as a whole by follow-the-light insertion (Neo 0032 §14, J06 steps 10–11): Janus lights one sampled bay at a time, a named person puts a reel there, and the sensor must change at the lit bay and only there. followLight() draws each sampled bay as a two-pip seam, square (Janus lit it, blinking the bay's LED colour) then round (the sensor saw it): when the sensor answers the green line draws and the round pip settles closed, once, as the live row changes. A bay that did not follow leaves the seam broken amber. A board in manual operation (0033 §6, 0035 §4) has no sensor check: its round pip becomes the manual mark, the same solid inner border the rack glyph's spec key manual= puts on its bays (rackLegend key 'manual' says it: Manual · scan-confirmed). Reduced motion: the seam closes at once; a lit pip is drawn split, lit / off.

    Call
    followLight({ label, boards: [{ title, samples: [{ bay, why, state: 'todo' | 'lit' | 'seen' | 'miss' | 'manual', by, at, occupied, seenAt, id }], note, id }], color, collapse: 'done', id }) · rackGlyph('…;manual=B-T3-L01..B-T3-L16') · rackLegend(['manual'])
    Keyboard
    Nothing to press: a record of the rack answering.
    Name
    An ordered list per board, "Bays lit on Board 5 · side B, tier 1"; each row says its state first (Lit, Followed, Did not follow, Manual operation), then the bay and what the sensor said; who put the reel in is the avatar's name.
    Live region
    None inside: the page re-renders it from the live topic (commission:<controller>) without announcing; the progress figure above it carries the count.
    Target
    Rows are 36 px; nothing in them is pressed.
    1. Board 5 · side B, tier 11 of 3 followed

      1. B-T1-L01first bay of the board Followed: Seen in B-T1-L01 · 14:31 MK
      2. B-T1-L08holds a reel Lit: Lit green · pull the reel and push it back in
      3. B-T1-L16last bay of the board Not lit yet: Not lit yet
    2. Board 8 · side B, tier 4Did not follow

      1. B-T4-L01first bay of the board Did not follow: No change at B-T4-L01
      2. B-T4-L16last bay of the board Did not follow: No change at B-T4-L16 · seen in B-T4-L15 instead

      Board 8 reads nothing on any channel. Its bays stay Unknown.

    3. Board 7 · side B, tier 3Manual operation

      1. B-T3-L01 Manual operation: No sensor check · screen-guided, scan-confirmed
      2. B-T3-L16 Manual operation: No sensor check · screen-guided, scan-confirmed
    One board as it happens: a bay followed (the seam closed), the next lit green, one to come; then a board that did not follow, and one in manual operation
    T4T3T2T1T4T3T2T1
    • Manual · scan-confirmed
    • Unknown
    • Occupied
    manual= on a rack glyph: side B, tier 3 in manual operation beside Unknown (dashed) tier 4; its legend key says it

    What shows when

    The rules for what a screen shows first, and the components that make the resting rung the default.

    Primary at rung 0secondary at 1–3tertiary at 4–5

    Tiers and the disclosure ladder

    Every fact is primary (what this person decides or does now), secondary (context that changes how they act) or tertiary (evidence and diagnostics). Primary is at rung 0; secondary at rungs 1–3; tertiary at rungs 4–5. Each rung has its kit component and the one trigger that brings it up; the components below make the resting rung the default, so a page has to ask to show more.

    Call
    DESIGN.md › What shows when
    Keyboard
    Per component.
    Name
    Per component.
    Live region
    None.
    Target
    Every fold is a 36 px summary (56 on the station scale).
    The disclosure ladder
    RungWhereKitSurfaces when
    0The first viewport, at restPageHeader (one sub-line), focusCard, bayAddress, st(), one primary btnAlways
    1Inline, in placelistRow({ current | open, detail }), seamRow, thenLine, calloutThe row is selected, the item diverges, or it is at the step that needs it
    2In place, foldeddisclosure (compact or sheet), listRow's Details, hint(), the done line of section · steps · followLight · removalWatchA tap; a hint also by Settings › Hints
    3Beside the listdialog({ kind: 'drawer' })The row is opened; the list stays
    4The record’s own pagereview-unit, work-order, rack.html#troubleshootThe person needs the whole case or its history
    5Technical detailstechDetails, receipt({ details }), PageHeader.tech()Asked for; never above rung 3 on an operator surface

    Rows at rest

    A row rests at two lines: its title and one line of words, a state and one action. The sub is clamped at 2 lines until the row is current or holds focus. Its case (detail: a seam row, a cover bar, provenance) folds behind a Details line and shows in place only when the row is current (selected) or open (it diverges, or it is at the step that needs it). A row's own controls are its form, always shown; a detail that holds a control is treated as the form, and a detail that is already a fold is shown as it is.

    Call
    listRow({ title, sub, meta, actions, detail, more, open, current, form, … })
    Keyboard
    Tab reaches the row link, the row's controls, then its Details summary; Enter or Space opens it.
    Name
    Details is named by its words ("Details", or the row's `more`: "2 differences").
    Live region
    None.
    Target
    The Details summary is 36 px (44 under a coarse pointer, 56 on the station scale).
    • SR-01-T5-L22
      Count difference 1 · open since 12:52. The count found the bay Clear where the record has a reel: decide with the reel in hand, or send someone to look at the bay before the Missing delay runs out.
      Open
      Record and sensor
      RecordR-2026-0088 · 2 150 pcsSensorClear since 12:52 · no task
    • SR-01-T5-L30
      Count difference 2 · reel, no record
      Open
      Details
      RecordEmptySensorReel since Fri 19 Sep 16:40
    At rest: the case folds behind Details; the long sub clamps at 2 lines
    • SR-01-T5-L22
      Count difference 1 · open since 12:52
      Open
      RecordR-2026-0088 · 2 150 pcsSensorClear since 12:52 · no task
    • PN-0603-10K
      3 reels short for WO-260714-07
      Short
    current: the selected row shows its case in place; open does the same for a row that diverges
    • Appearance
      Bay light colours look the same in both.
    form: an editor row keeps its controls in view

    Done collapses

    Finished work rests as one receipt line that opens it; open work comes first. section({ done }): the title and the line. steps({ done }): two or more done steps become one done step. followLight({ collapse: 'done' }): the boards that followed become one line, first, so the board being lit is the first open one. A booked removalWatch is one line in Janus's own voice.

    Call
    section({ title, done }, body) · steps(items, { label, done }) · followLight({ …, collapse: 'done' }) · removalWatch({ …, booked })
    Keyboard
    Each line is a summary: Tab, then Enter or Space opens it.
    Name
    The line is its own name (the receipt words); the section keeps its heading.
    Live region
    None.
    Target
    36 px; 56 on the station scale.

    Site transition

    Finished Mon 1 Sep · go/no-go 2 recorded by Bence Tóth

    All seven phases, their checklists and decisions.

    section({ done }): a finished site transition at rest
    1. 2 steps done · last 14:10 (done)
      Restore the snapshot14:02 · Eszter Szabó (done)
      Finish the access review14:10 (done)
    2. Reconcile integrations1 of 3 resumed
    3. Close the outage sheet
    steps({ done }): recovery with its first two steps done
    1. 4 boards followed8 bays seen
      1. Board 1 · side A, tier 12 of 2 followed

        1. A-T1-L01 Followed: Seen in A-T1-L01 · 14:02 DH
        2. A-T1-L16 Followed: Seen in A-T1-L16 · 14:04 DH
      2. Board 2 · side A, tier 22 of 2 followed

        1. A-T2-L01 Followed: Seen in A-T2-L01 · 14:02 DH
        2. A-T2-L16 Followed: Seen in A-T2-L16 · 14:04 DH
      3. Board 3 · side A, tier 32 of 2 followed

        1. A-T3-L01 Followed: Seen in A-T3-L01 · 14:02 DH
        2. A-T3-L16 Followed: Seen in A-T3-L16 · 14:04 DH
      4. Board 4 · side A, tier 42 of 2 followed

        1. A-T4-L01 Followed: Seen in A-T4-L01 · 14:02 DH
        2. A-T4-L16 Followed: Seen in A-T4-L16 · 14:04 DH
    2. Board 5 · side B, tier 10 of 2 followed

      1. B-T1-L01first bay of the board Lit: Lit green · put a reel in here
      2. B-T1-L16last bay of the board Not lit yet: Not lit yet
    followLight({ collapse: 'done' }): four boards followed, one line; the lit bay is the first open row
    Missing R-2026-0088By: Janus · 13:22
    Last bay
    T5-L22 · dark
    Booked at
    the Missing delay, 30 min
    Reference
    EV-2461
    T2-L10 Unexpected removal R-2026-0175 left its bay · 14:21 Missing at 14:51 unless it is back
    removalWatch: booked Missing is one line; open stacks under 560 px of card

    Explain

    A sentence that teaches how Janus works or what a control will do is on demand, never inline (rule 7): not a sub-line, a lead, a section meta, a notice body or text beside a button. explain() is an ⓘ button named by its summary that opens the words in a popover anchored to it; on a desk, resting the pointer on it previews the popover. Beside a section title use section({ explain }); for a whole page, PageHeader.explain() (a popover, or with panel the page help panel, a drawer).

    Call
    explain(summary, body, { id, label, first, attrs }) · section({ title, explain }) · PageHeader(title).explain(summary, body, { panel })
    Keyboard
    Tab reaches the button; Enter or Space opens; Escape or a click outside closes and returns focus.
    Name
    The button is named by the summary ("How the pick window works"); the popover is a region named by the same words.
    Live region
    None.
    Target
    24 px on a desk (44 under a coarse pointer, 56 on the station scale).

    Pick window

    How the pick window works

    A reel is taken when the sensor sees the bay empty within the pick window. After it, the bay stays lit until the reel is gone.

    8 s · policy

    Beside a section title

    People

    14 people · 1 needs attention

    About People

    Grants

    A person holds roles through grants: a role on the site, an area or a rack.

    Nobody is deleted

    Deactivate and Reactivate keep the record.

    The page head (K6) with the help panel

    Hint

    Help for a person's first weeks is the explain() control with its summary as a visible label, so a new person sees that help is there; the explanation itself stays in the popover and is never re-inlined. The person's Settings › Hints decides it: labelled (always), labelled until they have opened it once in this browser (the first weeks, the default), or absent (off). The page passes its session's value; the kit decides what renders.

    Call
    hint(summary, body, { hints: 'always' | 'first' | 'off', id, attrs })
    Keyboard
    Tab reaches the button; Enter or Space opens the popover; Escape closes it and returns focus.
    Name
    Its label (the summary).
    Live region
    None.
    Target
    24 px on a desk (44 under a coarse pointer, 56 on the station scale).

    Why 8 s?

    The placement window is 8 s. If it runs out, the put-in keeps watching: a late sensor answer still confirms it.

    Settings › Hints: always

    Why 8 s?

    The placement window is 8 s. If it runs out, the put-in keeps watching: a late sensor answer still confirms it.

    Settings › Hints: first

    (nothing renders)

    Settings › Hints: off

    Entity head

    An entity head carries at most 2 meta items, its state and its place. .meta([state, place]) renders the first two items that are not identifiers; an item that holds an identifier (an Activity entry, a permit, a controller, a command key, an IP or MAC address, a decision point), and every item after the second, goes to the head's Technical details, with .tech(rows). A single part is the free form (a skeleton, a page's own line): rendered as given, and the density gate reads it.

    Call
    PageHeader(title).crumbs(trail).meta([state, place, …]).tech([[key, value], …]).actions(…).render()
    Keyboard
    Technical details is a summary: Tab, then Enter or Space.
    Name
    The h1 names the thing; the meta line is text.
    Live region
    None.
    Target
    36 px.

    SR-02 · High-capacity rack B

    Limited · tier 3 UnknownSMT hall › SMT Line 2 · position 2

    Technical details

    CTRL-B02 · 10.2.0.22Permit PMT-260924-01 · Dávid Horváth until 18:00

    Shape
    6 tiers × 32 bays · 2 boards per tier
    Floor label
    POS-L2-03
    SR-02: state and place on the line; the controller, its address and the permit in Technical details

    Session chips

    A session or role fact (Support session, View only) is a chip in the top bar, never a page banner: it is said on every page without taking the page's first viewport. The chip opens a popover with the words. Under 560 px the chip keeps its icon and its name.

    Call
    sessionChip({ id, icon, label, title, body })
    Keyboard
    The chip is a button: Enter or Space opens the popover, Esc closes it.
    Name
    The chip's word; the popover is a region named by its first line.
    Live region
    None.
    Target
    32 px, 44 under a coarse pointer.

    Dávid Horváth, Danutek technician account

    Always on while R1 is trialled. Every action is logged under this named account; it is not a site user.

    Your roles can see, not change

    Pages show what is recorded.

    Station home

    A station's home in DESIGN.md's order, which a page fills and can't reorder: the rack as the status (rackNotes), the one thing yours now (focusCard, whose action is the only primary), waiting work and history as counts behind folds, then the one start tile. Beside the rack from 900 px of its own width; one column, in the same order, below.

    Call
    stationHome({ rack, focus, folds: [{ summary, body, id, hidden }], start: ActionTileOpts, startAlso, startTitle, level, id })
    Keyboard
    Reading order is the slot order: the rack's pins, the focus action, the folds, the start tile.
    Name
    The start region is named by its heading ("Scan to start"); the rest by their own components.
    Live region
    None: the station re-renders a slot from the live topic.
    Target
    Station scale: 56 px, the primary 64–72.
    T5T4T3T2T1
    T2-L28 · lit blue for you
    Take out R-2026-0012.
    SMT Line 2 · position 1 · this station's rack

    Pull WO-260714-07

    Ready · lights when you press Next

    2 more yours
    • Put back R-2026-0057
      Due back from SMT Line 2
    Recent

    Stored R-2026-0042 at SR-01-T3-L14 · 14:32

    Scan to start

    At the station scale, something yours
    T5T4T3T2T1
    T2-L28 · lit blue for you
    Take out R-2026-0012.
    SMT Line 2 · position 1 · this station's rack

    Nothing waits for you

    New work sent to you shows here.

    Scan a reel to start

    Nothing waits for you: the focus slot says so, the start tile stays
    Technical details
    Gate
    tests/density/
    Rules
    DESIGN.md › What shows when, 1–12
    Voice
    DESIGN.md › Voice and copy, 1–15

    Behaviours

    app.js owns these contracts on every page. Page scripts import helpers from /assets/kit.js and never re-implement them. Every demo is a real form or link that htmx sends; its route answers the way a product write does (a fragment, or 204 and a toast).

    Close on done

    A form in a dialog closes its dialog once the write succeeded. A rejected write answers 4xx with the fields and their errors, and the dialog stays open.

    Markup
    <form data-close-on-done>[data-close]data-close="#id" data-open="#id"
    Keyboard
    Enter submits; Esc closes; focus returns to the button that opened it.
    Name
    The dialog is named by its title (aria-labelledby).
    Live region
    The result shows on the page; the toast confirms it in #toasts (polite).
    Target
    44 px buttons in the foot.

    SR-02 is called Paste line.

    Dialogs the server opens

    A dialog rendered with data-modal="open" is shown modally on load and after every swap. A link to #id opens that dialog; closing drops the fragment and the URL params named in data-clears.

    Markup
    dialog[data-modal="open"][data-clears="kd"]<a href="#dialog-id">
    Keyboard
    Focus moves into the dialog; Esc closes; focus returns to the control that asked for it.
    Name
    Dialog title (h2) via aria-labelledby.
    Live region
    None: the dialog itself is the answer.
    Target
    Openers are 44 px buttons or links in text.
    Open by address

    Focus after a swap

    After htmx swaps content in, focus goes to [data-focus] in the new content (the element, or the id it names), then to [autofocus] when focus was lost. Otherwise the control that caused the swap keeps focus: it is found again by id.

    Markup
    <div data-focus="new-row-id">[data-focus][autofocus]stable ids on controls
    Keyboard
    Tab order continues from the focused element.
    Name
    A row that takes focus gets tabindex="-1" and is read by its content.
    Live region
    None needed: focus says what changed.
    Target
    Unchanged.
    • R-2026-0042
      Put in at 14:32
      SR-01-T3-L14
    • R-2026-0109
      Put in at 14:32
      SR-01-T3-L15
    3 reels counted

    Gates

    A [data-gated] submit is disabled while its form is invalid. Kit constraints add at least n boxes (fieldset[data-min-checked]), a typed phrase (input[data-phrase], paste blocked) and a changed-from-saved gate (data-gated="changed"). Gated forms start fresh on every opening of their dialog (reset and re-gated on close; when that disables the invoker, focus goes to the nearest enabled control in its section). data-gate-scope="#sel" makes a gated button count only the form controls inside that element (an opener waiting for a changed tick while the dialog it opens holds the required acknowledgement).

    Markup
    [data-gated][data-gated="changed"][data-gate-scope="#sel"]fieldset[data-min-checked="1"]input[data-phrase][data-match="#id"]minlength
    Keyboard
    The disabled submit is skipped by Tab; the fields say what is missing.
    Name
    The unmet reason is the submit’s aria-describedby (“Reason: …”).
    Live region
    A typed phrase says “Phrase matches.” once, through the one announcer.
    Target
    44 px submit; 24 px check boxes with their whole label as target.
    Days this shift runs

    At least one day.

    Who is told when a rack goes offline

    Copy

    data-copy is the value itself (a site path becomes a full link; an <a data-copy> with no value copies its link). data-copy-from names the element whose text or value is copied. data-copied is the toast.

    Markup
    [data-copy="/activity.html#EV-2487"]<a href data-copy>[data-copy-from="#id"][data-copied="…"]
    Keyboard
    Enter or Space on the button.
    Name
    The visible words, or an aria-label that contains them (“Copy link to EV-2495”).
    Live region
    The toast says what was copied (#toasts, polite).
    Target
    44 px buttons; 32 px icon buttons in dense rows.
    jct_7Q4mV2xk9LpR3sHn8ZbW1cTe6YuA EV-2495

    Site clock and live durations

    The site clock is the one time authority (Opus.now(), kit.js now()): it starts at 14:32 when the session’s first page opened. [data-clock] shows it; [data-ends] and [data-since] hold the time left or gone, in site time.

    Markup
    [data-clock][data-clock="s"][data-ends="22:00"][data-since="14:24"]data-format="min-s|m:ss"
    Keyboard
    Not interactive.
    Name
    The sentence around it says what the time is for.
    Live region
    None: clocks never announce themselves.
    Target
    n/a
    Now
    · with seconds
    Shift B ends 22:00
    7 h 28 min left
    Feeder 14 is late
    by 8 min
    The offer lapses 14:39:40
    7 min 40 s left

    Countdown

    A countdown counts its seconds down from when it appears (a new one from the server restarts it). At a quarter left it turns amber; at zero data-on-zero runs: “submit” (its form) or “#id” (a form is submitted, anything else is clicked).

    Markup
    [data-countdown="30"][data-on-zero="#form-id"]data-totalopus:zero event
    Keyboard
    The switch-off stays a button; nothing depends on waiting.
    Name
    role="timer", aria-label “n seconds left”.
    Live region
    Off: a timer never reads out each second; the page says what happened at zero.
    Target
    44 px Switch off.
    10seconds

    SR-01-T3-L14 is lit amber. It switches off by itself.

    Busy label

    A submit with a busy label says it is working while its request runs, and a second press does nothing. The label stays until the answer lands.

    Markup
    btn(label, { busyLabel })[data-busy-label="Sending…"]
    Keyboard
    Focus stays on the button.
    Name
    The label becomes the busy words; aria-busy="true" and aria-disabled="true" while it runs.
    Live region
    The toast confirms when it is done.
    Target
    44 px.

    Toasts

    Toasts confirm a result the page can’t show on its own. The server sends them with HX-Trigger (or the shell’s one-time flash after a write that navigates); buttons can raise one with data-toast="text|tone". Undo rides along when the server offers it.

    Markup
    HX-Trigger {"toast":{text,tone,undo}}[data-toast="Label reported.|ok"]toast(text, tone, opts)
    Keyboard
    Dismiss is a button; hover or focus pauses the timer.
    Name
    “Dismiss”; Undo says what it undoes by its place in the toast.
    Live region
    #toasts is polite; a bad toast is role="alert" and stays until dismissed.
    Target
    28 px dismiss inside a 44 px toast row.

    Pick grids and tab strips

    A rack drawn with pick is an ARIA grid with one tab stop: arrows move, Home/End go to the ends of a tier, Enter, Space or a click picks the bay (Ctrl, ⌘ or Shift adds to a multi grid). The picked bay wears aria-selected and keeps the tab stop; the grid dispatches rack:pick { id, cell, selected }. Page scripts listen for rack:pick and never handle the click themselves. A tab strip that scrolls sideways on a narrow screen (viewNav, tabs) keeps its current tab in sight on load and after a swap; only the strip scrolls.

    Markup
    rackGlyph(spec, { pick: true, multi? })rack:pick { id, cell, selected }viewNav / tabs (scrolling strips)
    Keyboard
    Tab reaches the grid once; arrows move between bays; Enter/Space picks. Tab strips: Tab between links.
    Name
    The grid is named by its label; each bay is named "SR-01-T2-L03, occupied".
    Live region
    None: the picked bay is the state (aria-selected); the page says what the pick did.
    Target
    Bays are grid cells (exempt from 24 px); tab links are 44 px tall.

    Announce, fill, since, print, fold, counters

    Small contracts: [data-announce] in swapped content is said once through the one live region (a status line inside a region every write replaces). [data-fill][data-fill-target] puts a value in a field and focuses it. [data-since-s="N"] counts up from N seconds before the render, on the site clock. [data-print] prints (the shell hides its chrome in print). details[data-open-above="560"] (disclosure({ openAbove })) starts folded on narrow screens. [data-count-for] (textarea({ count })) shows n / max. A dialog nothing opened returns focus to its data-return-focus. [data-required-when="name=a,b"] makes a control required, and shows its field, only for those choices.

    Markup
    [data-announce][data-fill="R-2026-0042"][data-fill-target="#uid"][data-since-s="95"][data-format="m:ss"][data-print]details[data-open-above="560"][data-count-for="id"]dialog[data-return-focus][data-required-when="rv=gone,sensor"]
    Keyboard
    Fill and print are buttons.
    Name
    Their words.
    Live region
    announce() only (one polite region).
    Target
    Buttons 44 px.
    Waiting 1:35

    Live regions

    An element with an id and data-live="topic,…" re-renders from its page's own URL (or data-live-src) when one of its topics fires on the live feed (GET /events). Bursts coalesce (300 ms, one fetch per URL); the swap morphs, so focus, scroll and what is typed stay, and it says nothing. A region being typed in or holding an open dialog waits until focus leaves. A page in a Preview state (?state=) is a fixture: it ignores the floor simulation, and the server refuses its writes. While the connection is lost the regions carry [data-stale] (values dim, [data-asof] words get the time of the last update); the stop signal says so (below). Topics: review · alerts · rack:<id> · people · task · decision · scan · floor · reel · activity.

    Markup
    [data-live="review,rack:SR-01"][id]data-live-src="/url"[data-asof]live(topics, fn)live:<topic>live:state
    Keyboard
    Nothing new: the region keeps its controls and focus.
    Name
    The region's own content.
    Live region
    None on the region (it would speak on every change).
    Target
    Unchanged.

    Review queue · 12 open · decide one in another tab and this follows

    Stop signal

    Neo 0026 §18 and architecture/system-structure.md §6.4–6.5: when the connection is lost, nothing is booked, and the screen says so. One state on every page, on <html data-link>: connected (nothing shows), reconnecting (the stream was lost, or it is back and the page is catching up; a blip under 3 s says nothing) and stopped (gone 5 s, a named heartbeat silent that long, or no network: at once). While not connected every htmx POST is held back with the signal's words and every [data-needs-live] region is inert. Opening the connection never lifts the signal: a fresh render does (the page read again, every live region re-rendered), and only then is anything admitted. The words are preloaded with the page (liveState()'s data-say-*), so a device with no network still says them. Three forms: the pill in the shell's top bar (on a phone it hangs full width under the bar), the strip over a handheld's task bar (56 px), the veil over a kiosk's task pane (must-read size). The veil arrives at --m-enter and leaves at once, only after the fresh render: the page is true again the moment it goes. Reduced motion: it appears at once.

    Markup
    liveState({ form: 'pill' | 'strip' | 'veil', body })[data-stop-signal]html[data-link="connected|reconnecting|stopped"][data-needs-live]opus:net { link, online, asOf }live:heartbeat
    Keyboard
    Nothing to press: a region that needs the connection is inert while it is lost.
    Name
    Its words: "Not connected — nothing is being booked", then what the device is doing ("Reconnecting…" or "Offline") and "as of 14:32".
    Live region
    None on the signal: stopping and coming back are each said once through the one announcer.
    Target
    Not a target. The strip is 56 px tall on the station scale.
    Reconnecting…Not connected — nothing is being bookedReconnecting… · as of 14:32Not connected — nothing is being bookedOffline · as of 14:32

    Take out T2-L28

    The bay blinks blue.

    Not connected — nothing is being bookedOffline · as of 14:32

    Scan the reel.

    Put in · SR-01

    Not connected — nothing is being bookedOffline · as of 14:32

    Station writes (no outbox)

    R1 keeps no client-side queue (Neo 0033 §7, 0058 §1): a station sends a write now, or not at all. kit.js record(action, values, label, { key, movement }) is the one station write (stationWrite() is record() with the movement rule): send() reads <html data-link> first, so nothing leaves a station that is not connected (Not booked); the write's intent (its Idempotency-Key and words) is kept in the terminal session before it is sent; an answer lost is Outcome unknown, never resent, asked about through POST /writes/reconcile until Janus says Recorded or Not recorded (its id then spent), and no new movement is sent meanwhile. The capture journal it replaces is parked: kiosk.html?state=journal.

    Markup
    record(action, values, label, { key, movement }) → { outcome: accepted | rejected | not-booked | unknown | blocked | preview, receipt, reason }stationWrite()send()reconcile()intents()intentOf(key)settleIntents()intent:changeintent:settledIdempotency-Key
    Keyboard
    Nothing of its own: the station's controls are the write's.
    Name
    The page names what happened in its own words; lastActions() lists them.
    Live region
    A settled Outcome unknown is said once (station-kit.js).
    Target
    Station controls.

    See the stop signal and last actions under Domain blocks; the kiosk, handheld and phone camera use them.

    kit.js for page scripts

    Page scripts import these from /assets/kit.js (the same module instance as app.js). They never re-declare them, keep their own clock, or build HTML strings the kit renders.

    • $(sel, root?) · $$(sel, root?)
      One element, or an array of them.
    • on(type, sel, (e, el) => …) → off
      A delegated listener on the document (on(type, fn) listens plainly).
    • onSwap((roots) => …) → off
      After new content lands, whatever the swap style (outerHTML included). roots are the new elements.
    • now() → Date
      The site clock: Wed 24 Sep 2026, 14:32 at the session’s first page, running in real time.
    • every(ms, fn) → stop
      fn now and every ms; whole seconds tick with the site clock.
    • fmtInt(n) · pad(n, w = 2) · dur(min)
      12 400 · 07 · “7 h 28 min”, the same words the server writes.
    • toast(text, tone, opts) → dismiss
      The one toast region. opts: { undo, ttl, html }; html: true only for trusted page copy.
    • announce(text)
      The one polite live region for page messages that have no visible place.
    • ack(frame, tone) · seedKeypad(target, value, fresh) · identify(rackEl)
      Station helpers: the frame edge flash for a read, a keypad value set from script (the next digit starts over), and the rack Identify pattern.
    • keypads()
      Wires every kit keypad ([data-keypad]; app.js calls it). A key dispatches a cancelable opus:keypad { key, step, target } first; digits, Clear, Delete and steps then type into the target and fire its input event.
    • legacyHash(map) → redirected
      Old #state links → the ?param URL the server renders ({ weeks: { tab: 'weeks' } }).
    • live(topics, fn) → off · liveState() · liveRetry()
      The live feed: one EventSource per browser shared by its tabs, resumed after the last event, reconnecting with backoff. fn gets { topic, data, sim, id }.
    • record(action, values, label) → { outcome, key, receipt, reason }
      A station record of what a person did: accepted (with the server's receipt) · kept (in the outbox, sent later) · rejected · unknown · preview. A page claims only what the outcome says. outboxList() · outboxAttention() · outboxFlush() read and send what is kept.
    • send(action, values, { key, actor, device }) → { status, outcome, refusal, events }
      The one write outside htmx (record() and stationWrite() use it): a POST whose answer the script must read (Janus-Outcome, Janus-Refusal).

    Rename SR-02

    Type “fail” to see a rejected write keep the dialog open.

    Shown on the rack label and in every list.

    SR-02 · Paste line

    Opened by its address: /kit.html#kd-hash

    Closing takes #kd-hash off the address, so the same link opens it again.

    Rack
    SR-02
    Place
    Paste line · position 1
    Controller
    CTRL-S02

    Mark R-2026-0042 as missing?

    It leaves the stock count until someone finds it.

    Recorded with your name.

    Write at least 4 characters.

    Clear SR-02’s bay map?

    Every bay goes back to Unknown until the rack is read again.

    Typed by hand, so it is a decision and not a reflex.

    Alerts

    Faults and blocked work stay here until fixed. Notices can be cleared.

    SR-02 is limited

    Bence Tóth is on itsince 09:41 · 4 h 51 min14:30 installation map v4 drafted by Dávid Horváth under permit · waiting to be applied

    Tier 3, bays 1–16 are Unknown. Blocks 2 take-outs for WO-260714-09 and hides 310 pcs of PN-QFN32-MCU7. The rest of SR-02 works.

    Handled by the site administrator.Stays until fixed

    Label not recognised

    A Würth Elektronik label matched no code rule.

    Eszter Szabó is told. Put the reel aside.

    Put-in not finished

    Reel R-2026-0109 is waiting for the sensor at SR-01-T4-L31: finish putting it in, or cancel.

    Counter ReelSave is failing

    Owner: line counter vendorsince 13:48

    12 messages waiting. SMT Line 2 counters don't know about reels taken out since 13:48.

    Handled by the site administrator.

    Snapshot taken · 14:30

    Verified · off-host copy 14:30. Snapshots every 15 min while Janus is busy.

    Choose working rack

    Scans and bay lights on this station use the working rack. Opening another rack never switches it.

    Racks

    Help

    What the words mean

    Language

    Applies to this station only. Records keep the words they were written in.

    Prototype

    UI kit

    This page

    Purpose
    Every component, variant and state, with its accessibility contract.