---
version: 0.1.0
name: atomm Generator UI
description: The design system for third-party generator apps embedded in the atomm platform. Hand this file and one of the five self-contained layout templates under https://dev.atomm.com/templates/ to your coding agent.
colors:
  primary: '{colors.neutral-900}'
  neutral-0: '#ffffff'
  neutral-25: '#fcfcfd'
  neutral-50: '#f6f6f7'
  neutral-100: '#eeeff1'
  neutral-200: '#e7e8ea'
  neutral-300: '#d6d8db'
  neutral-400: '#b9bbc0'
  neutral-500: '#85878b'
  neutral-800: '#3d3e42'
  neutral-900: '#292a2d'
  neutral-950: '#171719'
  neutral-alpha-25: rgba(61,62,66,0.06)
  neutral-alpha-50: rgba(61,62,66,0.08)
  neutral-alpha-100: rgba(61,62,66,0.12)
  neutral-white-alpha-500: rgba(255,255,255,0.5)
  atomm-600: '#fa253d'
  atomm-700: '#d9002d'
  atomm-800: '#b20025'
  atomm-alpha-400: rgba(255,0,53,0.4)
  blue-100: '#f1f6fd'
  blue-500: '#3f81ea'
  blue-600: '#3170d5'
  green-100: '#ecfff0'
  green-500: '#16b364'
  orange-100: '#fff3eb'
  orange-500: '#ff6500'
  red-100: '#fee9e7'
  red-500: '#ec221f'
  bg-default: '{colors.neutral-0}'
  bg-subtlest: '{colors.neutral-25}'
  bg-surface: '{colors.neutral-50}'
  bg-control: '{colors.neutral-100}'
  bg-editor: '{colors.neutral-200}'
  bg-hover: '{colors.neutral-alpha-25}'
  bg-active: '{colors.neutral-alpha-50}'
  bg-highlight: '{colors.neutral-alpha-100}'
  bg-overlay: rgba(0,0,0,0.7)
  bg-segmented-active: '{colors.neutral-0}'
  bg-inverse: '{colors.neutral-950}'
  bg-inverse-subtle: '{colors.neutral-white-alpha-500}'
  text-primary: '{colors.neutral-950}'
  text-secondary: '{colors.neutral-800}'
  text-tertiary: '{colors.neutral-500}'
  text-disabled: '{colors.neutral-400}'
  text-inverse: '{colors.neutral-0}'
  stroke-default: '{colors.neutral-300}'
  stroke-divider: '{colors.neutral-alpha-50}'
  stroke-strong: '{colors.neutral-400}'
  stroke-active: '{colors.neutral-800}'
  stroke-surround: '{colors.blue-600}'
  stroke-scrollbar: '{colors.neutral-300}'
  track-off: '{colors.neutral-300}'
  brand-default: '{colors.neutral-900}'
  brand-hover: '{colors.neutral-800}'
  brand-active: '{colors.neutral-950}'
  brand-disabled: '{colors.neutral-200}'
  emphasize-default: '{colors.atomm-600}'
  emphasize-hover: '{colors.atomm-700}'
  emphasize-active: '{colors.atomm-800}'
  emphasize-disabled: '{colors.atomm-alpha-400}'
  blue-default: '{colors.blue-500}'
  blue-subtlest: '{colors.blue-100}'
  green-default: '{colors.green-500}'
  green-subtlest: '{colors.green-100}'
  warning-default: '{colors.orange-500}'
  warning-subtlest: '{colors.orange-100}'
  red-default: '{colors.red-500}'
  red-subtlest: '{colors.red-100}'
typography:
  16-semibold:
    fontFamily: Inter
    fontSize: 16px
    fontWeight: 600
    lineHeight: 22px
  14-medium:
    fontFamily: Inter
    fontSize: 14px
    fontWeight: 500
    lineHeight: 20px
  12-medium:
    fontFamily: Inter
    fontSize: 12px
    fontWeight: 500
    lineHeight: 16px
  12-regular:
    fontFamily: Inter
    fontSize: 12px
    fontWeight: 400
    lineHeight: 16px
  14-regular:
    fontFamily: Inter
    fontSize: 14px
    fontWeight: 400
    lineHeight: 20px
  11-regular:
    fontFamily: Inter
    fontSize: 11px
    fontWeight: 400
    lineHeight: 16px
  10-medium:
    fontFamily: Inter
    fontSize: 10px
    fontWeight: 500
    lineHeight: 14px
  10-regular:
    fontFamily: Inter
    fontSize: 10px
    fontWeight: 400
    lineHeight: 14px
rounded:
  none: 0px
  small: 4px
  medium: 6px
  large: 8px
  xlarge: 12px
  circle: 100px
spacing:
  50: 4px
  75: 6px
  100: 8px
  150: 12px
  200: 16px
  300: 24px
sizes:                    # measured off the layout frames, not tokens in the export
  control-mini: 24px
  control-sm: 28px
  control-md: 32px
  control-lg: 40px
  input-h: 28px
  input-w: 92px
  select-w: 110px
  slider-w: 110px
  slider-track: 3px
  slider-thumb: 12px
  switch-w: 36px
  switch-h: 20px
  switch-knob: 14px
  topbar-h: 64px
  stage-h: 52vh
  rail-tools: 64px
  rail-templates: 320px
  rail-lead: 320px
  rail-params: 320px
  rail-floating: 360px
  rail-collapsed: 40px
  thumb: 140px
  thumb-sm: 90px
  dialog-w: 480px
  min-target: 24px
elevation:
  shadow-100: 0 4px 12px rgba(11,11,13,0.06), 0 0 4px rgba(11,11,13,0.06)
  shadow-200: 0 16px 32px rgba(11,11,13,0.06), 0 2px 8px rgba(11,11,13,0.06)
  shadow-300: 0 40px 80px rgba(11,11,13,0.06), 0 2px 8px rgba(11,11,13,0.06)
focus:
  ring-color: '{colors.stroke-surround}'
  ring-width: 2px
  ring-offset: 2px
motion:
  fast: 150ms
  base: 200ms
  easing: cubic-bezier(0.2, 0, 0.38, 1)
components:
  button-primary:
    backgroundColor: '{colors.brand-default}'
    textColor: '{colors.text-inverse}'
    typography: '{typography.14-medium}'
    rounded: '{rounded.medium}'
    height: 32px
    padding: 0 12px
  button-primary-hover:
    backgroundColor: '{colors.brand-hover}'
  button-primary-active:
    backgroundColor: '{colors.brand-active}'
  button-primary-large:
    backgroundColor: '{colors.brand-default}'
    textColor: '{colors.text-inverse}'
    typography: '{typography.14-medium}'
    rounded: '{rounded.large}'
    height: 40px
    padding: 0 16px
  button-secondary:
    backgroundColor: '{colors.bg-default}'
    borderColor: '{colors.stroke-default}'
    textColor: '{colors.text-secondary}'
    typography: '{typography.14-medium}'
    rounded: '{rounded.medium}'
    height: 32px
    padding: 0 12px
  button-secondary-hover:
    backgroundColor: '{colors.bg-hover}'
    borderColor: '{colors.stroke-strong}'
  button-ghost:
    textColor: '{colors.text-secondary}'
    rounded: '{rounded.medium}'
    height: 32px
  button-ghost-hover:
    backgroundColor: '{colors.bg-hover}'
  button-emphasize:
    backgroundColor: '{colors.emphasize-default}'
    rounded: '{rounded.large}'
    height: 32px
    padding: 0 16px
  button-emphasize-label:
    textColor: '{colors.text-inverse}'
    typography: '{typography.14-medium}'
  button-emphasize-hover:
    backgroundColor: '{colors.emphasize-hover}'
  button-emphasize-active:
    backgroundColor: '{colors.emphasize-active}'
  button-emphasize-disabled:
    backgroundColor: '{colors.emphasize-disabled}'
  button-disabled:
    backgroundColor: '{colors.bg-subtlest}'
  button-disabled-label:
    textColor: '{colors.text-disabled}'
  button-icon:
    backgroundColor: '{colors.bg-default}'
    textColor: '{colors.text-secondary}'
    rounded: '{rounded.large}'
    size: 32px
  button-tips:
    backgroundColor: '{colors.bg-default}'
    textColor: '{colors.text-secondary}'
    typography: '{typography.14-medium}'
    rounded: '{rounded.circle}'
    height: 32px
    padding: 0 10px 0 6px
  material-tile:
    backgroundColor: '{colors.bg-control}'
    rounded: '{rounded.small}'
    size: 57px
  history-tile:
    backgroundColor: '{colors.bg-default}'
    rounded: '{rounded.large}'
    size: 90px
    padding: 10px
  input-field:
    backgroundColor: '{colors.bg-control}'
    textColor: '{colors.text-secondary}'
    typography: '{typography.12-regular}'
    rounded: '{rounded.medium}'
    height: 28px
    width: 92px
    padding: 0 6px
  input-field-hover:
    backgroundColor: '{colors.bg-active}'
  input-field-focus:
    backgroundColor: '{colors.bg-default}'
    borderColor: '{colors.stroke-active}'
  input-field-disabled:
    backgroundColor: '{colors.bg-subtlest}'
  input-placeholder:
    textColor: '{colors.text-tertiary}'
  input-unit:
    textColor: '{colors.text-tertiary}'
    typography: '{typography.10-regular}'
  select-trigger:
    backgroundColor: '{colors.bg-control}'
    textColor: '{colors.text-secondary}'
    typography: '{typography.12-regular}'
    rounded: '{rounded.medium}'
    height: 28px
    width: 110px
    padding: 0 6px
  select-list:
    backgroundColor: '{colors.bg-default}'
    borderColor: '{colors.stroke-default}'
    rounded: '{rounded.large}'
    padding: 6px
  select-option:
    typography: '{typography.12-regular}'
    height: 32px
    padding: 0 10px
    rounded: '{rounded.small}'
  select-option-active:
    backgroundColor: '{colors.bg-highlight}'
  button-plate:
    backgroundColor: '{colors.bg-surface}'
    textColor: '{colors.text-secondary}'
    rounded: '{rounded.small}'
    size: 24px
  color-swatch:
    backgroundColor: '{colors.bg-surface}'
    borderColor: '{colors.stroke-divider}'
    rounded: '{rounded.medium}'
    size: 24px
  switch-track:
    backgroundColor: '{colors.stroke-default}'
    rounded: '{rounded.circle}'
    width: 36px
    height: 20px
  switch-track-on:
    backgroundColor: '{colors.brand-default}'
  switch-knob:
    backgroundColor: '{colors.bg-default}'
    rounded: '{rounded.circle}'
    size: 14px
  slider-track:
    backgroundColor: '{colors.track-off}'
    rounded: '{rounded.circle}'
    height: 3px
    width: 110px
  slider-track-filled:
    backgroundColor: '{colors.brand-default}'
  slider-thumb:
    backgroundColor: '{colors.bg-default}'
    borderColor: '{colors.brand-default}'
    rounded: '{rounded.circle}'
    size: 12px
  segmented-track:
    backgroundColor: '{colors.bg-hover}'
    rounded: '{rounded.medium}'
    height: 32px
    padding: 2px
  segmented-item:
    textColor: '{colors.text-secondary}'
    typography: '{typography.12-medium}'
    rounded: '{rounded.small}'
    padding: 0 8px
  segmented-item-active:
    backgroundColor: '{colors.bg-segmented-active}'
    textColor: '{colors.text-primary}'
  property-card:
    backgroundColor: '{colors.bg-default}'
    borderColor: '{colors.stroke-divider}'
    rounded: '{rounded.large}'
    roundedBottom: '{rounded.none}'
  property-card-header:
    textColor: '{colors.text-primary}'
    typography: '{typography.12-medium}'
    padding: 8px 16px
  field-row:
    padding: 8px 16px
  field-label:
    textColor: '{colors.text-primary}'
    typography: '{typography.12-regular}'
  helper-text:
    textColor: '{colors.text-tertiary}'
    typography: '{typography.12-regular}'
  thumbnail-card:
    backgroundColor: '{colors.bg-default}'
    borderColor: '{colors.stroke-divider}'
    rounded: '{rounded.large}'
    size: 140px
    padding: 14px
  thumbnail-card-selected:
    borderColor: '{colors.brand-default}'
  thumbnail-card-photo:
    padding: 0px
  thumbnail-label:
    textColor: '{colors.text-secondary}'
    typography: '{typography.11-regular}'
  rail-item:
    textColor: '{colors.text-tertiary}'
    typography: '{typography.10-medium}'
    width: 64px
  rail-item-selected:
    backgroundColor: '{colors.brand-default}'
    textColor: '{colors.text-inverse}'
    rounded: '{rounded.large}'
    size: 40px
  float-panel:
    backgroundColor: '{colors.bg-default}'
    borderColor: '{colors.stroke-divider}'
    rounded: '{rounded.xlarge}'
    width: 360px
  panel:
    backgroundColor: '{colors.bg-subtlest}'
    textColor: '{colors.text-primary}'
    width: 320px
  canvas:
    backgroundColor: '{colors.bg-editor}'
  dialog:
    backgroundColor: '{colors.bg-default}'
    rounded: '{rounded.xlarge}'
    width: 480px
  dialog-title:
    textColor: '{colors.text-primary}'
    typography: '{typography.16-semibold}'
  dialog-scrim:
    backgroundColor: '{colors.bg-overlay}'
  dialog-header-inverse:
    backgroundColor: '{colors.bg-inverse}'
    textColor: '{colors.text-inverse}'
    typography: '{typography.16-semibold}'
    padding: 16px 12px 12px 16px
  dialog-thumbnail:
    backgroundColor: '{colors.bg-default}'
    rounded: '{rounded.large}'
    width: 66px
    height: 66px
  tag:
    backgroundColor: '{colors.bg-active}'
    rounded: '{rounded.small}'
    height: 20px
    padding: 0 6px
  tag-label:
    textColor: '{colors.text-secondary}'
    typography: '{typography.10-medium}'
  tag-on-inverse:
    backgroundColor: '{colors.bg-inverse-subtle}'
  form-label:
    textColor: '{colors.text-tertiary}'
    typography: '{typography.12-medium}'
  alert-info:
    backgroundColor: '{colors.blue-subtlest}'
    textColor: '{colors.text-primary}'
    typography: '{typography.12-regular}'
    rounded: '{rounded.medium}'
    padding: 12px
  alert-info-icon:
    textColor: '{colors.blue-default}'
  alert-success:
    backgroundColor: '{colors.green-subtlest}'
    textColor: '{colors.text-primary}'
    typography: '{typography.12-regular}'
    rounded: '{rounded.medium}'
    padding: 12px
  alert-success-icon:
    textColor: '{colors.green-default}'
  alert-warning:
    backgroundColor: '{colors.warning-subtlest}'
    textColor: '{colors.text-primary}'
    typography: '{typography.12-regular}'
    rounded: '{rounded.medium}'
    padding: 12px
  alert-warning-icon:
    textColor: '{colors.warning-default}'
  alert-error:
    backgroundColor: '{colors.red-subtlest}'
    textColor: '{colors.text-primary}'
    typography: '{typography.12-regular}'
    rounded: '{rounded.medium}'
    padding: 12px
  alert-error-icon:
    textColor: '{colors.red-default}'
  link:
    textColor: '{colors.blue-default}'
    typography: '{typography.12-regular}'
---

# atomm Generator UI

## Contract for coding agents

Read this block first. Everything in it is non-negotiable; the rest of the document explains why and fills in detail.

**Start from a template, do not start from a blank page.** The platform publishes this file together with five layout templates, one per layout, at `https://dev.atomm.com/templates/`. Pick the one that matches your generator (see the decision table in Layout); each comes in two forms.

**Read the skeleton when you write markup** — `https://dev.atomm.com/templates/<layout>.skeleton.html`. It is that layout's structure plus this file's full stylesheet, with the script block removed: every token below lives in its `<style>` block as a CSS custom property, next to the component classes, and nothing else is in the file competing for your attention. Copying it in is the shortest path, because that stylesheet is written against those class names — but rebuilding the same layout in your own framework is equally legitimate, as long as what renders comes out the same.

**Take the runnable form only to see the example work** — the same URL without `.skeleton`, one self-contained HTML file with the behaviour inlined. What the controls do in there is one generator's demo, not part of this spec. What has to match is the layout and the sizes it renders, not the tags it uses; wire the controls to whatever your own product does. The exception is `upgradeSelects`: that function is the ARIA contract a styled select owes, and it is written to be ported rather than re-derived.

**A template is a starting point, not a finished product.** Refine it, add what your generator needs, drop what it does not, and borrow a block from another layout when that layout solves your problem better — a history section from layout 3 inside layout 5's panel, say. What may not change is the **skeleton**: region widths, control heights and radii, where the overlays sit, the state set each control shows, and the platform constraints — all of it judged on the rendered page, not read off your source. Stay inside the skeleton and any combination is legitimate; break it and the result stops reading as part of the platform, however good it looks on its own.

**Fetch one template, not five.** What you need is this document plus **one** template — the one closest to your generator, as your base. All five carry the same stylesheet and the same script (70KB of the ~90KB in each file), so fetching them all adds context and no information. If you later need a block that only another layout has, fetch that one template then. The full URLs are in the decision table under Layout.

**If you cannot open the template, stop and ask for it.** Those are ordinary URLs: fetch the one you picked before you write any markup. If you have no network access, or the fetch fails, **ask the user to paste the template file into the conversation and wait for it.** Do not reconstruct it from this document. A component rebuilt from prose is the exact failure this file exists to prevent, and it is not visible in your own output — only in a comparison against the template.

**Skeleton versus skin.** Two kinds of rules live here:

- **Skeleton — must match.** Layout regions and their widths, where the tabs / Fabrication Tips / zoom / export live, control heights, radii, the closed state set, the keyboard and screen-reader contract each control owes, and every Platform Constraint. These constrain **what renders**, not how you write it: the arrangement and the numbers have to come out the same whatever framework, markup or class names you use.
- **Skin — default, replaceable.** Every colour value and the type sizes. The values in this file are the platform default. A generator may ship its own palette or its own accent as long as it stays light, keeps 4.5:1 for text and 3:1 for control boundaries, and keeps every control state visually distinct. Change the value of a token, never its name.
- **Product behaviour — yours.** What the generator actually makes, which parameters it exposes, what Generate produces, how far zoom goes, what the history holds: none of it is specified here. The runnable templates answer these questions one way so the example runs; treat those answers as a demo, not a requirement.

Ten rules that cover most rejections:

1. The layout starts from one of the five templates. Refine it and combine blocks across layouts freely — but keep the skeleton: rails are fixed-width, the canvas takes everything else.
2. The parameters rail is 320px; parameters live in **property cards**; export is pinned to the rail's bottom.
3. View tabs sit top-centre of the canvas, the tips button top-end, the zoom cluster bottom-start.
4. Controls are 24 (icon plates, swatches), 28 (inputs, selects), 32 (buttons, segmented, icon buttons), 40 (the one big call to action). Nothing else.
5. Radii are 4 on tags, icon plates and the active segment, 6 on controls, 8 on thumbnails, the zoom cluster and **property cards**, 12 on dialogs and the floating rail. No pills except the switch track and the Tips button.
6. Inputs are grey-filled with no border; focus draws a 1px `stroke-active` border. Selection is a 2px brand ring, never colour alone.
7. Text on the panel is 12px; only buttons and rail headings are 14px. No raw hex: every colour is a `--color-*` variable.
8. Export goes through `<div data-atomm-export-button>` and the `export` lifecycle hook. No top bar of your own.
9. Icons are inline stroked SVG. Every interactive target is at least 24×24 and shows a pointer cursor. Nothing conveys meaning by colour or motion alone.
10. Nothing exceeds 200ms, nothing bounces, nothing animates on load.

When you are done, run the **Self-check** at the end of this file and fix every line that fails before you submit.

## Overview

A generator is a **workbench, not a storefront**. The reference is the properties panel of a professional creative tool: quiet chrome, dense enough to work in, never decorated. The person using it is in the middle of a job. They will run the same generator twenty times in an afternoon, changing one value each time, and the interface earns its keep through predictability at that repetition rate: controls always in the same place, values always legible, the twentieth transition identical to the first.

Every official generator on the platform shares one skeleton so that a user who has learned one has learned them all: something to pick or generate on the left, the result in the middle, what to adjust on the right, export in the bottom-right corner. Your generator inherits that skeleton. Inside it you may set your own mood — a different neutral, an accent that suits your material — but a user must never have to re-learn where things are.

Your generator runs inside the atomm platform, in the region the platform allocates to it, below a top bar that belongs to the platform. Design like a well-behaved tenant: match the host, never imitate its navigation, never cover it.

What it is not: not a marketing page, not a dashboard, not a consumer app. No hero moment, no gradients, no glass, no glow, no illustrated empty state, no entrance animation.

## Colors

The palette is written in two layers. **Primitives** hold the raw values and are the only place a hex appears. **Semantic tokens** reference them and say what a colour is *for*; components reference the semantic layer only. That is what makes a reskin a change to a dozen lines rather than to every component.

The whole colour layer is **skin**. The values here are the platform default and the templates ship with them; a generator may replace the set. What does not change is the structure: which role each token plays, and the contrast each role has to clear.

### A neutral workbench with one dark accent

The default chrome is a near-neutral grey ramp. Panels are `bg-subtlest` (`#fcfcfd`), cards on the panels are pure white, and the ground the preview stands on is `bg-editor` (`#e7e8ea`) — three steps darker than the panels. The ground is the floor of the room and the panels are furniture standing on it; a preview reads as an object placed on that floor. Controls take a third grey, `bg-control` (`#eeeff1`): an input is a recess in a white surface, in the same way.

`brand-default` is a near-black (`#292a2d`). It carries the primary button, the filled part of a slider, a switch in its on state, the selected tool in the rail and the selection ring on thumbnails. **Hover lightens it and press darkens it** — `brand-hover` is `neutral-800`, `brand-active` is `neutral-950` — so a press always lands darker than the resting state.

`emphasize-default` (`#fa253d`) is the platform's red and the one exception to a neutral chrome: **at most one emphasized action per screen**, on the thing that carries a cost or a consequence. The platform's own generators leave Generate on `brand-default` and spend nothing on it, so no template ships an emphasized button. White on it measures 3.89:1.

### Four text steps

| Token | Value | On white | Used for |
|---|---|---|---|
| `text-primary` | `neutral-950` | **17.90:1** | Field labels, card titles, values, headings |
| `text-secondary` | `neutral-800` | **10.68:1** | Text inside inputs and selects, button labels on white, thumbnail labels |
| `text-tertiary` | `neutral-500` | 3.60:1 | Helper text, placeholders, "View all", inactive rail items |
| `text-disabled` | `neutral-400` | 1.92:1 | Disabled labels, character counters |

The first two clear 4.5:1 on every surface they sit on. **`text-tertiary` does not** — 3.60:1 on white, 3.13:1 on the control fill — It follows that tertiary carries only text a user can afford to miss: helper lines, a placeholder, an inactive label. Never a value, never a message, never a label a decision depends on.

### Strokes

- **`stroke-divider`** (8% ink) is the border of a property card, the line under a card header, the edge of a floating panel. Hairline: it separates, it does not outline.
- **`stroke-default`** (`neutral-300`) is the edge of a docked rail, a secondary button, a thumbnail on hover, the pill-shaped Fabrication Tips button.
- **`stroke-strong`** (`neutral-400`) is the hover step for anything that has a `stroke-default` border.

Inputs have **no border at rest**. The grey fill is the affordance; a border appears only on focus, and then it is `stroke-active` (`neutral-800`).

### Status

The chrome is neutral: `brand-default` carries everything that needs weight, and `emphasize-default` is rationed to one action per screen. Status colours work in pairs: the tint is the background of an alert, the solid is the icon, and the message text stays `text-primary` (15–17:1 on every tint). Coloured text on a coloured tint is the most common accessibility mistake in status messaging — and here it would fail, because `green-default` and `warning-default` measure 2.6–2.7:1 on their own tints. `stroke-surround` is a solid blue at 4.76:1, often the only sign of where the keyboard is, so it clears 3:1 on its own with room to spare.

**Three neutral fills, and they are not interchangeable.** `bg-hover` (6%) is the hover step on a panel; `bg-active` (8%) is a pressed or selected state there; `bg-highlight` (12%) is the row highlight inside a floating list. The panel steps land at 1.10–1.14:1 on the white ground of a popup and simply disappear, which is why a floating row has a step of its own.

### Reskinning

If you replace the palette: stay light (the ground darker than the panels, never dark mode); recompute every text step against every surface it sits on, and keep the two that carry meaning — `text-primary` and `text-secondary` — at 4.5:1 or better; keep `stroke-*` distinguishable from each other and from the fills; keep the three neutral fills visibly apart, and check the 12% row highlight against whatever surface your floating lists sit on. Change values, not names.

## Typography

**Inter** throughout, three weights, line heights in even pixels. A style is named by its metrics — there is no role alias layer — and the table says which role each one carries. Seven of the export's twenty styles appear inside a generator region; the rest belong to the platform's own pages.

| Style | Metrics | Carries |
|---|---|---|
| `16-semibold` | 16 / 600 / 22 | Dialog titles only |
| `14-medium` | 14 / 500 / 20 | Buttons, rail headings ("Templates", "AI Generate") |
| `12-medium` | 12 / 500 / 16 | Property-card titles, segmented items |
| `12-regular` | 12 / 400 / 16 | Field labels, values inside inputs, select text, helper text, alert and dialog body |
| `11-regular` | 11 / 400 / 16 | Thumbnail labels, "View all" |
| `10-medium` | 10 / 500 / 14 | The selected tool-rail label |
| `10-regular` | 10 / 400 / 14 | The unit suffix pinned to a numeric field's end, and a tool-rail label at rest |

The panel is built on **12px**: labels, values, card titles and helper text all share it, and the hierarchy inside a card comes from weight (card title 500, field label 400) and from where a thing sits, not from size. 14px is reserved for the two things that have to read from across the room — a button and a rail heading. Because one style carries the whole field register, a generator that needs bigger text moves the register up a step, from `12-regular` to `14-regular`, and accepts every row growing 4px.

## Layout

Generators combine two questions — **what goes on the canvas** (left) and **what does it look like** (right) — into one of five layouts. Pick by the table; the platform's own generators cover all five. The table links the skeleton of each — drop `.skeleton` from any of those URLs for the runnable version.

| # | Template | Left | Right | Use it when |
|---|---|---|---|---|
| 1 | https://dev.atomm.com/templates/layout-1-params.skeleton.html | none | 320 params | Nothing to pick or generate; the user only adjusts values |
| 2 | https://dev.atomm.com/templates/layout-2-templates.skeleton.html | 320 templates | 320 params | One left-side job: browse and pick a template |
| 3 | https://dev.atomm.com/templates/layout-3-generate.skeleton.html | 320 generate, docked | 320 params | Start from an input (prompt, style, image), then tune |
| 4 | https://dev.atomm.com/templates/layout-4-floating.skeleton.html | 360 generate, floating | none | Pick a style, set the run up in a dialog, nothing to tune afterwards |
| 5 | https://dev.atomm.com/templates/layout-5-multi.skeleton.html | 64 tools + 320 panel | 320 params | Several left-side jobs: generate, templates, import |

Fetch one of those URLs — the one for the layout you picked — and build on the file it returns.

If you cannot decide between 3 and 4, the question is whether anything is adjusted *after* generating. If yes, 3. If the result is final, 4.

### Regions

| Region | Width | What is in it |
|---|---|---|
| Platform top bar | 64 | The platform draws it — never add a second one |
| Tool rail | 64 | Layout 5 only: one icon per left-side panel |
| Left rail | 320, or 360 when floating | What there is to add or to generate: templates, styles, a prompt, an import |
| Canvas | everything the rails leave | The preview, plus four overlays: view tabs, Fabrication Tips, zoom, and export when there is no parameters rail |
| Parameters rail | 320 | The stack of property cards, with export pinned at the bottom |

Rails are **fixed width**; the canvas absorbs window resizing. A parameter rail must not get wider because the window did. Zero gaps and zero radii between docked regions — a docked rail meets the canvas at a 1px `stroke-default` line. The floating rail is the one exception: a white card, `rounded.xlarge`, `shadow-100`, inset 16px from the top, start and bottom edges of the canvas.

**Left rails collapse** to a 40px pill in the canvas's top-start corner (rail title plus the panel icon); the canvas grows. In layout 5 the panel collapses by clicking the already-selected tool.

**The parameters rail** is always 320px and always the same three bands: a header row (unit toggle at the start, Reset at the end), a scrolling stack of property cards, and a footer with the export button pinned so nobody scrolls to find it. Derived results (usage, part count, duration) are annotations on the canvas, or one card at the bottom of the stack. They never get a column of their own.

**The canvas** carries four overlays and nothing else: view tabs at the top centre (2D design / 3D preview / Export, a segmented control and not a tab strip), the Fabrication Tips capsule at the top end, the zoom cluster at the bottom start. The fourth is export, and it appears at the bottom end only when there is no parameters rail. Overlays do not move on hover.

**Zoom is a free value, not a set of steps.** The wheel and a two-finger pinch scale the canvas continuously and anchor on the pointer: whatever sits under it stays under it, which means the pan absorbs the difference. Dragging the canvas pans it. The readout is a menu button whose menu offers a few levels as shortcuts into that range, and fit measures the canvas and re-centres. Which levels the menu lists, and how far the range goes, is yours to decide. Icon-only overlay controls name themselves with a tooltip on the same dark plate as a toast.

**A lead rail can hold a second level.** "View all" on a section, the plate chevron beside a block heading, and the history row at the rail's foot all replace the rail's content with a back row and a three-column grid of 90px tiles; nothing about the canvas changes and no dialog opens. The back row is a 32px button — chevron plus the title — and Back is the only way out.

**A generate rail ends with History.** Under the prompt, the styles and the one Generate button comes a block heading with a plate chevron and a three-column grid of recent results, scrolling with everything above it. The data is the platform's; the tiles are placeholders until it is wired. Tiles follow the thumbnail rule: an inline SVG sits inset 10px, a photo fills the tile.

### Property cards

Parameters are grouped **by object or by function** into property cards: white, `stroke-divider` border, `rounded.large` on all four corners, `shadow-100`, stacked with 12px between them on the `bg-subtlest` panel. Each card has a header row (title at the start, disclosure chevron at the end, the whole row a `<button aria-expanded>`) and field rows below it. The card itself is a `<section>` pointed at that header with `aria-labelledby`. Cards with few parameters stay open; cards with many may start collapsed. Collapsing changes only `hidden` and the chevron's rotation — no height animation.

A **field row** is `label ⋯ control`, 8px vertical and 16px horizontal padding, label in `text-primary`, control end-aligned. Numeric values are inputs, 92px wide; choices are selects, 110px; ranges are a slider with its value shown as a number beside it; on/off is a switch; a colour is a 24px swatch. Explanatory copy goes under the row in `12-regular` / `text-tertiary`, and is rare — anything that fits in a unit suffix does not become a sentence.

### Flow and states

The layout has to make the flow obvious: **pick or input → configure → generate → review → export**. Live-preview generators redraw as a slider moves, so they have no generate step; adding a Generate button to one is a defect.

The preview has three states and all three need designing. **Empty**: examples or a hint, never a blank rectangle. **Computing**: a stage name, a number, and a way to cancel; never an indefinite spinner. **Done**: zoom, pan, compare, regenerate. Errors belong beside the control that caused them, together with the next action — a toast at the top of the screen is where errors go to be missed.

### Sizing and scrolling

Your width is the region the platform allocates, not the viewport, so adapt with container queries. Below 960px the three regions stack — canvas first, then the rails — and the export button stays pinned to the bottom. Rails scroll internally with thin scrollbars; never introduce horizontal scrolling, never nest one scroll container inside another. A panel that fills its space *and* scrolls needs both `flex: 1` (to grow) and `min-height: 0` (to be allowed to shrink below its content); a flex child defaults to `min-height: auto` and refuses to shrink, which is the overflow-instead-of-scroll bug.

## Elevation & Depth

Depth comes from **surface tone and hairlines first, shadow second**. Docked regions have no shadow at all; they are separated by tone (`bg-subtlest` panel, white card, grey canvas) and by 1px lines.

| Token | Value | Used for |
|---|---|---|
| `shadow-100` | `0 4px 12px rgba(11,11,13,.06), 0 0 4px rgba(11,11,13,.06)` | Floating rail, property cards, collapsed pill, thumbnail cards, the active segment, switch knob, slider thumb |
| `shadow-200` | `0 16px 32px rgba(11,11,13,.06), 0 2px 8px rgba(11,11,13,.06)` | Dropdown lists |
| `shadow-300` | `0 40px 80px rgba(11,11,13,.06), 0 2px 8px rgba(11,11,13,.06)` | Dialogs |

Three steps and no fourth. Every step is two layers: an offset shadow gives direction and a zero-offset one gives a white card an edge on a white panel. `shadow-100` is also the smallest step there is — knobs and thumbs take it, and nothing in the system takes anything smaller. No shadow is ever animated, and nothing lifts on hover.

Keyboard focus is its own layer: a 2px `stroke-surround` outline with a 2px offset. The offset is load-bearing — it keeps the ring visible on a near-black button. The input family is the exception: inputs and selects show focus by turning their fill white and drawing a 1px `stroke-active` border, because a ring around a filled box reads as two boundaries.

## Shapes

Four radii, each tied to a kind of thing, and the pairing is **skeleton**:

- `rounded.small` (4px) — tags, the 24px icon plates in section headers, the active item of a segmented control
- `rounded.medium` (6px) — every control: input, select, button, segmented track, swatch
- `rounded.large` (8px) — thumbnail cards, the zoom cluster, icon buttons, the collapsed pill, the 40px tool plate, and property cards
- `rounded.none` (0) — the bottom two corners of an expanded property card's header row (collapsed, the header is the whole card and takes all four)
- `rounded.xlarge` (12px) — dialogs and the floating rail

`rounded.circle` is the export's `100px`, not `9999px` — it only reaches a pill on something narrower than 200px, which every fully-round thing here is (36px switch track, 12px thumb, the 68px Tips capsule). It is for true circles: the switch knob, the slider thumb, status dots. **Two capsules exist in the system and no more**: the switch track, because a rounded-rectangle switch reads as a checkbox, and the Tips button, a 32px capsule that has to read as help rather than as one more action. Every other button is a rounded rectangle. Corners are the only shape device: no notches, no angled cuts, no gradient strokes.

## Components

Every interactive element supports the same closed state set: default, hover, active, focus-visible, disabled, loading, selected, error. There is no ninth state. Heights are **skeleton**: 28 for the input family, 32 for buttons, segmented controls and icon buttons, 40 for the one large call to action, 24 for icon plates and swatches.

### Buttons

`button-primary` is `brand-default` with white text, 32px, `rounded.medium`, and answers most questions — including Export, which the platform renders in exactly this shape. `button-primary-large` (40px, `rounded.large`, full width) is for the one call to action in a lead rail, such as Generate. `button-secondary` is white with a `stroke-default` border and `text-secondary` label; hover darkens the border and tints the fill. `button-ghost` is transparent until hovered. Destructive actions use the secondary shell with `red-default` text and a confirmation — never a solid red fill.

Labels are verbs (Generate, Export, Reset), never OK or Submit. Loading keeps the button's width and swaps the label for a spinner with `aria-busy`. Icon-only buttons are 32px squares on `rounded.large`, white on the canvas. `button-tips` sits in the canvas's top-end corner: a 32px white capsule with a `stroke-default` border, a 20px lightbulb and the word Tips in `14-medium`; it opens the fabrication-tips dialog and nothing else.

### The input family

`input-field` is 28px tall, 92px wide, `bg-control` fill, no border, `rounded.medium`, text in `text-secondary` with `tabular-nums`. Hover deepens the fill to `bg-active`; focus turns the fill white and draws a 1px `stroke-active` border; disabled fades to `bg-subtlest` with `text-disabled`; error keeps the fill and takes a 1px `red-default` border with the message beside the field.

**A unit is pinned to the field's end, not carried inside the value.** The value starts at the field's start edge and the suffix (`input-unit`: `typography.10-regular`, `text-tertiary`) holds the end edge, so a column of fields reads down both edges — the numbers line up at the start, the units at the end — and switching mm ↔ inch rewrites the two independently. They are two elements, never one string: a unit inside the value moves as the number grows and reorders under RTL bidi.

A numeric field carries its starting value in the markup (`value`) and `inputmode="decimal"`. Numeric fields support horizontal drag: press and drag to change the value, Shift for ten-times steps, Alt for fine steps, and a movement under 3px counts as a click. Use `setPointerCapture` so dragging outside the panel keeps the gesture, invert the direction in RTL, and keep Shift + ↑/↓ as the keyboard equivalent. Native spinners are never shown.

`select-trigger` is the same box at 110px with a `text-tertiary` chevron that flips when the list opens; open, the trigger takes the focus state — white fill, 1px `stroke-active` border. The list is a floating surface, so it takes a `stroke-default` border, `rounded.large` and `shadow-200`: 6px padding, at least as wide as the trigger, capped at seven 32px rows before it scrolls. **Two orthogonal signals inside it, and each has to be legible alone**: the fill (`bg-highlight`) says where the pointer or the keyboard is, a `brand-default` checkmark plus 500 weight says what is chosen. The pointer moves the active option, so exactly one row is ever filled — a hovered row lit next to the keyboard's row shows two "current" rows at once. One fill doing both jobs is what leaves a four-item list unable to say which is which.

Styling that popup means writing, by hand, everything a native `<select>` gives away. The `upgradeSelects` function in every template's script block is that implementation — **port it, do not re-derive it** — and it is written as progressive enhancement: the markup ships a real `<select>` inside a `<span class="select-field" data-select>`, the script hides it and builds the trigger and list, and the native element stays the value's source of truth so form submission and `change` listeners keep working. The contract it pays: `role="combobox"` and `aria-expanded` on the trigger, `role="listbox"` / `role="option"` / `aria-selected` on the list, **focus never leaving the trigger** with `aria-activedescendant` on the active option (moving focus into the list is how you strand it on `body` when the list closes); closed, ↓ ↑ Enter Space Home End or any character opens; open, ↓↑ move, Home/End jump, Enter or Space commits, Escape closes unchanged, Tab closes, an outside click closes, type-ahead jumps; focus returns to the trigger on close and the active row scrolls inside the list's own box. Mount the list on `body` with `position: fixed` and reposition it on scroll and resize, or a scrolling rail clips it; flip it above the trigger when the space below runs out; align it to the trigger's end edge in RTL. On narrow touch screens fall back to the native `<select>` anyway: you give up the system picker, and 32px rows are small under a thumb.

The prompt box in a generate rail is the same family grown up: `rounded.large`, 10/12 padding, a character counter in `text-disabled` at the bottom-end corner.

### Switch

A 36×20 track, `stroke-default` off and `brand-default` on, with a 14px white knob under `shadow-100` that travels 16px. The visible track and knob are decoration painted over a real `<input type="checkbox">`; every decorative layer needs `pointer-events: none` and `aria-hidden`. The track is 20px tall but the checkbox inside it is stretched to `--min-target`, so the hit area clears 24 without the track changing size; the field row's `<label for>` is a second, larger target.

### Slider

Never alone: the value always appears as a number at the end of the row, and when precision matters pair it with an input. A 3px track in `track-off`, the filled part in `brand-default`, a 12px thumb — white centre, 2px `brand-default` ring, `shadow-100`. Step and range come from the model's real constraints; a silently clamped value is a bug the user cannot see. Disabled: `stroke-divider` track, `bg-subtlest` thumb with a `stroke-default` ring.

### Segmented control

A 32px track in `bg-hover` with 2px padding on `rounded.medium`; items are `12-medium` in `text-secondary`, and the active item is white on `rounded.small` with `shadow-100` and `text-primary`; a 1px `stroke-divider` line separates adjacent inactive items. Use it for two or three mutually exclusive views (2D / 3D, mm / inch, Option A / B). Above three options use a select. The container takes `role="group"` with a name, each option a `<button aria-pressed>`.

### Thumbnails and the tool rail

A template thumbnail is a 140px white card on `rounded.large` with a `stroke-divider` border and `shadow-100`; an inline SVG sits inset 14px while a photo fills the card edge to edge; and an 11px label in `text-secondary` underneath. Style tiles in a generate rail are the same card at 90px with no shadow; round subjects (coins, badges) sit in the same square card with a circular image. Selection is a **2px `brand-default` ring** drawn over the card, so the content never resizes; hover is `stroke-default`. Grids: two columns of template cards in any 320 rail or panel, three of the small tiles in a row. The two columns come from tracks that split the rail evenly rather than from a fixed 140 — when a scrollbar appears the cards shrink, they never drop one onto a row of its own.

The tool rail (layout 5) is 64px wide: 40px plates on `rounded.large` with 16px icons and a 10px label beneath; the selected tool is `brand-default` with a white glyph and a `text-primary` label, the others `text-tertiary`.

### Dialogs

Native `<dialog>` with `showModal()`, 480px wide, `rounded.xlarge`, `shadow-300`, scrim `bg-overlay` (70% black). On a short viewport the body scrolls; header and footer stay put. Header 16/24 padding: the title in `16-semibold`, a 20px info glyph in `text-tertiary` beside it, the 24px close button at the end, a hairline below; an optional 267px media band in `bg-control`; body 12/24; footer 16/24 with pagination dots at the start and buttons at the end. Most things put in dialogs do not belong there: errors go beside their control, progress goes in the preview, settings go in the parameters rail. The fabrication-tips walkthrough is the canonical dialog.

**The generate dialog** (layout 4) is the same shell wearing a dark header: `bg-inverse` behind `text-inverse` text, lit at one corner, and no hairline under it. It holds a 66px `dialog-thumbnail` of what was picked, that thing's name in `16-semibold`, and `tag`s for the facts the choice already fixed — the aspect ratio, whether the background comes out transparent. A tag on that header turns into a light film, `bg-inverse-subtle`. The body stacks form fields: a `form-label` **above** its control, the one place in the system where a label does not sit beside one — a 480px dialog has room for a full-width drop target and a 320px rail does not. The footer keeps Cancel and Generate at the end. Picking a style tile opens this; the tile never starts the call by itself.

### Platform-fed blocks

Two pieces of UI draw their data from the platform rather than from your generator. The templates ship them working, with placeholders, so they sit in the right place from day one; wire the data when the capability is open to you.

- **Surface** — a property card at the bottom of the parameters stack titled Surface, holding `material-tile`s four to a row: 57px, `rounded.small`, the material image filling the tile, one selected at a time with a 2px `text-primary` ring 2px outside the tile. It changes how the preview is rendered and never what is exported. The tiles and their names come from the platform's material library.
- **History** — the rail-foot row "My history" opens the second level described in Layout: `history-tile`s three to a row (90px, `rounded.large`, `stroke-divider` border, `brand-default` 2px border when selected) and a secondary Load more button under them. Picking a tile restores that result. The grid is a `role="listbox"`, the tiles its options, selection `aria-selected`.

### Alerts, links, helper text

Alerts put the tint behind the message and the solid on the icon, with `text-primary` words, next to whatever they describe. Links take `blue-default` and keep their underline. Helper text under a field is `12-regular` in `text-tertiary` and explains a constraint rather than restating the label. Be stingy: the ideal parameters rail has two or three genuinely constraining notes in it.

## Motion

| Token | Value |
|---|---|
| `motion.fast` | 150ms |
| `motion.base` | 200ms |
| `motion.easing` | `cubic-bezier(0.2, 0, 0.38, 1)` |

Motion confirms that something changed; it never carries meaning and never makes the user wait. Interactive feedback (hover, press, toggle, a border colour) takes `fast`; content transitions (rail collapse, dialog enter and exit) take `base`. Animate `opacity` and `transform` only — the GPU is busy rendering your preview. Nothing exceeds `base`, nothing bounces, nothing animates on load. Generation progress is a number and a stage name, not an animation. `prefers-reduced-motion: reduce` collapses every duration to zero and nothing is lost, because no state is conveyed by motion alone.

## Iconography

Icons are **inlined stroked SVG**, never an icon font: an icon font from a CDN fails in sandboxes and offline, and an icon-only button then becomes an empty box.

- `viewBox="0 0 16 16"` at 16px (20 for the tips lightbulb); `fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round`.
- Colour always comes from the parent's text token. An icon with its own colour is an icon that is wrong in the next state.
- Icon-only controls have three hit-area steps — 24, 32, 40 — with 32 the default; there is nothing below 24. The glyph inside may be smaller: a 24px plate and a 32px button both carry a 16px glyph. `button-plate` is the 24 step — a `bg-surface` square on `rounded.small`, used in a rail header to collapse the panel or to step into a second level.
- A decorative icon beside its own label takes `aria-hidden`; an interactive icon-only button takes an `aria-label` naming the action; an icon carrying state needs screen-reader text.
- Directional icons (chevrons, arrows, undo) need a class that RTL can flip; clocks, checkmarks and logos never flip. Prefer up/down chevrons, which need no flipping.

## Direction

The platform renders your generator in a document whose `dir` follows the user's locale, and several supported locales read right to left. There is no second stylesheet: the layout flips itself.

Use logical properties everywhere a value is one-sided (`margin-inline-start`, `inset-inline-end`, `text-align: start`); flex and grid mirror on their own. **The canvas content is never mirrored** — lock the artboard to `direction: ltr` so the exported file's orientation is independent of the interface — but the overlays on the canvas are interface and mirror with everything else.

What RTL changes, item by item: the slider's `linear-gradient(to right …)` needs `to left`; the drag-to-change direction inverts; a popover positioned by JS aligns to the trigger's end edge; the switch knob's `translateX(16px)` becomes `-16px` because transforms are physical; the progress bar's `transform-origin` switches sides; a number and its unit read as one run, so a readout that concatenates them — the value beside a slider — takes `unicode-bidi: plaintext` or "6 mm" reorders to "mm 6" (a numeric field is safe: its unit is a separate element pinned to the end). Set `dir="rtl"` and walk the whole flow before submitting.

## Accessibility

The floor, not the ceiling:

- **Every interactive target is at least 24×24px**, with no exceptions left in the templates. Where the visual is deliberately smaller — the 20px switch track, the 12px slider thumb, an 11px text link — keep the visual size and grow the hit area with padding plus an equal negative margin, so the target reaches 24 without the row getting taller. They all reference `--min-target`.
- **Verify contrast per pairing.** `text-primary` and `text-secondary` clear 4.5:1 on every surface in the system; `text-tertiary` does not, which is why it never carries a value or a message. Recompute the table below if you reskin, including the worst case of small text on the control fill.
- **Keyboard reaches everything a mouse reaches**, in visible order. Dialogs trap focus while open and return it to the trigger on close. A hand-built listbox owes the contract a native `<select>` gives away.
- **`[hidden]` needs restoring.** Any component that sets `display: flex` or `grid` overrides the browser's `[hidden] { display: none }`; ship `[hidden] { display: none !important }` in the base stylesheet.
- **Meaning never rides on colour, shape or motion alone.** A red border needs a message; a spinner needs text; a selected thumbnail needs its ring, not just a tint.

## Platform Constraints

These are not stylistic choices — they are conditions of running inside atomm.

**Export must go through the platform button.** Place `<div data-atomm-export-button></div>` in the export slot (rail footer, or the canvas's bottom-end corner when there is no rail) and register an `atomm.lifecycle.on('export', …)` hook. The SDK renders the button; theme it only through the documented `--atomm-export-*` variables (the templates set width 100%, height 32px, radius 6px and the brand colours). A submission with no export hook is rejected automatically. During development you may render a fallback button inside the slot; the SDK takes precedence when present.

**Treat the exported file as machine-facing.** 1:1 real dimensions, `mm` units, a viewBox in mm. Cut-line colours are mapped to machine processes by the device and do not follow the interface palette. Reference images used for preview never enter the drawing.

**The top bar belongs to the platform** — brand, account, credits. Do not rebuild it, do not imitate it, do not add a second one. Your own controls (project name, save, history) go in your lead rail's header.

**You render inside the region the platform allocates.** You cannot cover the platform chrome, you do not know the viewport, and the stacking context is yours alone: one overlay layer plus one for toasts is enough, and `z-index` caps at 3.

## Do's and Don'ts

### Layout

- **Do** pick one of the five templates and keep its regions and widths.
- **Do** put export where the template puts it — rail footer, or bottom-end of the canvas.
- **Don't** add a top bar, a second overlay layer, or a column for derived results.
- **Don't** put a Generate button on a live-preview generator.

### Controls

- **Do** group parameters into property cards with a disclosure header and `label ⋯ control` rows.
- **Do** keep the state set closed; a control that seems to need a ninth state needs rethinking.
- **Do** write out the keyboard and screen-reader contract before replacing a native control, then repay it item by item.
- **Don't** give inputs a border at rest, and don't use native spinners.
- **Don't** fill a destructive button with red; use the secondary shell with `red-default` text and a confirmation.
- **Don't** let a decorative layer take pointer events away from the control underneath it.

### Colour and type

- **Do** reference `--color-*` variables everywhere; a raw hex in a component is a bug.
- **Do** keep the canvas one step darker than the panels and white cards on the panels.
- **Do** recompute the text steps against every surface if you reskin.
- **Don't** raise the field size on its own; raise `--fs-field` and `--fs-note` together or not at all.
- **Don't** convey state by colour alone — a selected thumbnail has a ring, an error has a message.

### Engineering

- **Do** give a panel that fills its space *and* scrolls both `flex: 1` and `min-height: 0` — one without the other is the overflow bug.
- **Do** mount popovers on `body` with `position: fixed`.
- **Do** inline icons as stroked SVG and inherit their colour.
- **Don't** rely on a classic scrollbar taking no space; use a thin one.
- **Don't** animate anything but `opacity` and `transform`.

## Self-check

Run through this list against the finished UI. Every line is mechanically checkable; fix every failure before submitting.

- [ ] The layout was built against a template file you actually opened — not from this document alone. If you never fetched one, stop here and ask the user for it.
- [ ] The page is one of the five layouts; every rail width is 64 / 320 / 360 and the parameters rail is 320.
- [ ] View tabs are top-centre of the canvas, the Tips capsule top-end, the zoom cluster bottom-start; export is in the rail footer or the canvas's bottom-end corner and stays put while content scrolls.
- [ ] There is no top bar of your own and no second overlay layer; `z-index` never exceeds 3.
- [ ] Every control height is 24, 28, 32 or 40; inputs and selects are 28, buttons 32, the one large call to action 40.
- [ ] Every radius is 4, 6, 8, 12 or a true circle, except the bottom two corners of an expanded property card's header row, which are square; the switch track and the Tips button are the only capsules.
- [ ] Inputs are grey-filled with no border at rest, white with a 1px `stroke-active` border on focus, 92px wide; selects 110px.
- [ ] Parameters are inside property cards with a disclosure header; rows are `label ⋯ control` with 8/16 padding.
- [ ] No raw colour appears in component CSS; every colour is a `--color-*` variable and every hex in `:root` is in this file's palette (or your declared reskin).
- [ ] Panel text is 12px, buttons and rail headings 14px, thumbnail labels 11px, rail labels 10px (or your declared scale applied consistently).
- [ ] Selected states have a 2px brand ring or a checkmark, not colour alone; every error has a message beside its control.
- [ ] Every interactive target is at least 24×24; every icon-only button has an `aria-label`; every decorative icon and decorative layer is `aria-hidden`.
- [ ] Every control that reacts to a click shows `cursor: pointer`, every text field a caret, everything disabled `not-allowed`; a value readout keeps the default arrow.
- [ ] Icons are inline SVG with `stroke: currentColor`; no icon font is loaded.
- [ ] Sliders show their value as a number; switches are real checkboxes with `pointer-events: none` on their decoration; selects follow the combobox contract or are native.
- [ ] Nothing animates longer than 200ms, nothing animates on load, and `prefers-reduced-motion` zeroes every duration.
- [ ] `<div data-atomm-export-button>` exists and the `export` lifecycle hook is registered; the exported file is 1:1 in mm.
- [ ] The flow works with `dir="rtl"`: rails, overlays, slider fill and switch knob all mirror; the canvas content does not.
- [ ] Below 960px the regions stack, the canvas comes first, export stays pinned, and there is no horizontal scrolling.
