---
name: edge-33
description: Use this skill to generate well-branded interfaces for Bono The Edge. Contains tokens (version stamped in `bono-ds.css` — versioning rules live in the edge-33 repo), component catalog with status, reference templates for each surface (Landing, Dublin, Blog — internally Reading, which also covers every PDF: reports, contracts, any document meant to be read or printed), a slide library for presented decks (39 layouts, 1920×1080, incl. Read & Learn and room-interaction slides), and hard rules. Always use this skill when generating any Bono UI, page, component, screen, PDF or printable document, or presentation / slide deck (including a request for a PowerPoint: build it from the slide library, then export .pptx) — even if the user doesn't explicitly mention the design system. This skill must be consulted before writing any HTML or CSS for Bono interfaces.
---

## File map

```
bono-ds.css                  ← tokens + utilities. One stamped copy per project (see "Canonical CSS source").

── components/ (markup is copied from here — one page per component) ──
components/welcome.html              ← Welcome (doc site entry point)
components/foundation-colors.html    ← Color tokens (brand, surfaces, text, status)
components/foundation-typography.html ← Typographic scale, Fraunces, T-em, T-actor, numerics
components/foundation-spacing.html   ← Spacing, border-radius, shadows
components/foundation-layout.html    ← Shell & layout: single breakpoint 1024px, .app-shell, .layout-12
components/foundation-grid.html      ← Grid pattern (.has-grid), canonical z-index
components/foundation-sections.html  ← .sec-page / .sec-tonal
components/foundation-naming.html    ← Dicționarul de nume: ce clasă poartă fiecare rol, și numele respinse
components/foundation-icons.html     ← Icons: Lucide only, via .ic (24 grid, optical stroke 1.25→2px, sizes 12/14/16/18/20/24; in .btn the step sets size + stroke)
components/button.html               ← Button (.btn, variants and sizes)
components/input.html                ← Input (.input) — controlul gol + OTP
components/input-group.html          ← Input group — tot ce stă ÎN bordură: addon-uri, scurtătura (badge alb, .badge.sm.outline), eticheta din bordură
components/field.html                ← Field: câmpul (.field) și grupul lui (.fieldset), orientare, separator
components/select.html               ← Select
components/typeahead.html            ← Typeahead (.typeahead) — input cu sugestii, aria-activedescendant
components/switch.html               ← Switch toggle
components/toggle.html               ← Toggle (.toggle / .toggle-group / .toggle-group.text) — buton-icon care ȚINE o stare (aria-pressed solo, role=radio în grup); .text = grup cu coduri scurte (D300, TVA), max 12, pe un rând; .form = printre câmpuri, cu mărimea (40 / .lg 44) și conturul lui .input. NU e .switch: ăla e binar pe o opțiune, ăsta e un buton
components/checkbox.html             ← Checkbox (.check-item, .check-item.card)
components/radio.html                ← Radio (.radio-item, .radio-item.card)
components/money-input.html          ← Money input + currency selector — hero (48px) and regular size (in .input-group); the list is the select's
components/calendar.html             ← Calendar (.calendar / .calendar.lg) — o zi sau un interval
components/uploader.html             ← Uploader (.uploader / .is-compact) — drop zone + file rows
components/badge.html                ← Badge
components/alert.html                ← Alert
components/toast.html                ← Toast (.toast) — mesaj tranzitoriu, colț de ecran
components/banner.html               ← Banner + page banner — DEPRECATED v8, use .alert
components/spinner.html              ← Spinner (.spinner) — încărcare în curs
components/skeleton.html             ← Skeleton (.skeleton) — locul unui conținut care vine; .kpi[aria-busy] îl folosește
components/stepper.html              ← Stepper (.stepper, .stepper.mobile)
components/pulse-dot.html            ← Pulse dot
components/modal.html                ← Modal
components/popover.html              ← Popover (.popover / .popover.compact) — suprafața oricărui meniu: dropdown, select list, meniu contextual
components/context-menu.html         ← Context menu — .popover.compact deschis cu click-dreapta
components/command.html              ← Command (.command) — câmp care filtrează o listă de comenzi, inline sau în dialog (.modal.command-dialog, ⌘K); câmpul e .input, rândurile .popover-item; cmdk în react/command.tsx
components/tooltip.html              ← Tooltip (.tooltip) — eticheta unui control fără text
components/tabs.html                 ← Tabs (ARIA tablist)
components/topnav.html               ← Topnav v3 — DEPRECATED, do not use
components/vox-fab.html              ← Vox FAB (.vox-fab)
components/flow-chrome.html          ← Flow chrome (mobile .flow-chrome + desktop .app-shell = .app-top + .app-footer)
components/proto-menu.html           ← Opțiuni prototip (.proto) — PROTOTYPES ONLY: the one purple menu, bottom-left and draggable, for every control that is not the product (Design · Ecran · Stare · Variantă · bife · Simulează · Reset); built by proto/proto-menu.js. Also the index of every prototype control found across the repos
components/app-nav.html              ← App navigation (desktop .sidebar rail, labels become hover/focus tooltips in .icon-only · mobile .tabbar + .mobile-menu)
components/header.html               ← Header (desktop .topbar · mobile .app-mobile-top: root / section / detail)
components/sheet.html                ← Sheet — the one bottom sheet on mobile (.sheet.attached / .floating / .form)
components/list-row.html             ← List row (.list-row in .list-group — the phone's settings / menu / value row)
components/quantum-stack.html        ← Quantum stack (.quantum-stack > .quantum-group > .quantum-pair) — one record read as label–value facts, on .list-group / .list-row.value; an action value is .btn.link.quantum-action; a full-width fact (a timeline, a sentence) is a .quantum-block, its own card with the label on top
components/timeline.html             ← Timeline (ol.timeline > li.timeline-step[data-state]) — a record's steps in the order they happened, read-only: done / current / upcoming / failed in the stepper's colours; in a record, the body of a .quantum-block. A flow you move through is .stepper
components/record-view.html          ← Record view — PATTERN: one record, read-only, in a .modal.record-view (440px) with a .quantum-stack body; the record's number after the kind (.record-view-ref); .record-view-state while it loads
components/file-viewer.html          ← File viewer (.card.tonal.file-viewer beside a form · .modal.file-viewer over a list, 85dvh — both bej-1, the frame white) — a file a PERSON brought (PDF, photo) in the browser's own viewer; the header is the name, or a .select-trigger.sm.bare chooser with several files, + new tab + download; loading / failed / unsupported / empty in the frame, never the previous file; not drawn below 1024px (the row is a link)
components/solo-screen.html          ← Solo screen — PATTERN: screens OUTSIDE the app shell (login, 404, no access, route error, waiting): .solo-screen = full-height bej-0 frame + a centred .card.lg (max 440px), h1.t-h1, at most one primary; every state in templates/solo.html
components/data-list.html            ← Data list (WIP) — the mobile form of .tbl
components/kpi.html                  ← KPI card
components/chart.html                ← Chart (.chart) — grafic în pagină
components/card.html                 ← Card (.card / .tonal / .dark / .feat)
components/action-card.html          ← Action card (.card.cta + .card-title / .card-body / .btn.full) — ends in ONE large action, buttons level across a row; optional icon action in .card-ft
components/insight.html              ← Insight card
components/separator.html            ← Separator (.separator) — linia care desparte, orizontală sau verticală
components/accordion.html            ← Accordion / disclosure (.accordion / .contained / .sm)
components/avatar.html               ← Avatar
components/table.html                ← Table (.tbl) · filtered table: the filter bar INSIDE .tbl-wrap (.tbl-filters), over a .tbl.compact, one 14px edge for title, filters and columns; the filtered frame is bej-1 so the white filters read as fields, the body stays white
components/filter-bar.html           ← Filter bar (.filter-bar) — the one filter model for every list: 8 filter kinds from existing controls, ✕ per filter, a general ✕ at the end, count, zero results
components/doc-list.html             ← Doc list (WIP) — 4 states
components/doc-bar.html              ← Doc bar + PDF switch (WIP)
components/pdf-viewer.html           ← PDF viewer (WIP)
components/html-doc-view.html        ← HTML doc view mobile (WIP)
components/doc-sheet.html            ← Doc sheet bottom modal (WIP)
components/signer-layout.html       ← Signer layout desktop (WIP)
components/_nav.js                   ← Source of the shared sidebar (JS injection)
components/_select.js                ← Doc-site ONLY: makes the .select-anchor demos actually open. NOT part of the DS — in production the behaviour is react/select.tsx (Radix). Underscore = page chrome, like _nav.js and _chrome.css
components/_search.js                ← Doc-site ONLY: the catalog's ⌘K search — the .command dialog over components/_search-index.json. NOT part of the DS — in production the behaviour is react/command.tsx (cmdk)
components/_search-index.json        ← GENERATED by scripts/build-catalog.py from components/*.html — what _search.js ranks (page name first, content second). Do not edit
components/_chrome.css               ← Doc-page styling (ds-block, demo, stub, WIP badge)

── react/ (React source is copied from here — the DEFAULT form where React exists) ──
react/select.tsx                     ← Select (Radix) — trigger .select-trigger, list .popover.popper, row .popover-item, chosen row gets the brand .popover-check. shadcn API, so it replaces components/ui/select.tsx without touching call sites
react/input.tsx                      ← Input + Input group — size and tone are PROPS (size="lg", tone="tonal"), not call-site classes
react/action-card.tsx                ← Action card — Dublin's API (tone, title, titleVariant, contentAlign, action) + secondaryAction / ActionCardIconButton; replaces shared/dublin-ui/action-card.tsx without touching call sites
react/quantum-stack.tsx              ← Quantum stack — QuantumStack / QuantumGroup / QuantumPair / QuantumBlock / QuantumAction, Dublin's API on the .quantum-* classes
react/timeline.tsx                   ← Timeline — Timeline / TimelineStep (state, title, detail), Dublin's API on the .timeline* classes
react/command.tsx                    ← Command (cmdk + Radix Dialog) — Command / CommandDialog / CommandInput / CommandList / CommandEmpty / CommandGroup / CommandItem / CommandShortcut / CommandSeparator on .command + .popover-item; shadcn API, replaces components/ui/command.tsx without touching call sites
react/file-viewer.tsx                ← File viewer — FileViewer (inline) + FileViewerDialog (Radix Dialog) — files, value / onValueChange, error, onRetry — on the .file-viewer* classes; owns the loading state per url so a new file never shows the old one; the chooser is react/select.tsx; fileViewerKind(type) is the PDF + raster-image allowlist
react/README.md                      ← the contract: react/ wears bono-ds.css classes, never utility classes, so HTML and React stay ONE definition

── proto/ (prototype tools — copied into PROTOTYPES only, never into a production app) ──
proto/proto-menu.css                 ← „Opțiuni prototip” styling. Holds the ONLY purple in the system (#4C1D95 / #EDE9FE), as locals on .proto — not a token, hidden in print
proto/proto-menu.js                  ← builds the menu: Proto.states / Proto.check / Proto.simulate / Proto.action / Proto.open; no dependencies; inside a simulator iframe it forwards to the host page's menu

── templates/ (complete organisms, copied and adapted) ────────────────
templates/card.html          ← Card: white / tonal / dark + border rules
templates/header.html        ← moved 2026-09-16 → components/header.html (the file only redirects)
templates/sidebar.html       ← moved 2026-09-16 → components/app-nav.html (the file only redirects)
templates/form.html          ← Record page, a neutral specimen of how the controls relate: .layout-12 (.col-8 form · .col-4 record file), one group = .card.tonal + .fieldset, rows only for one value (.field-row), option groups under .field-title (radio cards · radio list · yes/no row · check group), a lone .check-item (never a .switch in a saved form), inline .uploader.is-compact, repeated cards, errors on each control or in one .alert.error
templates/table-view.html    ← List screen: the filtered table (.tbl-wrap > .tbl-filters + .tbl-scroll > .tbl.compact) + the three empty states
templates/kpi-grid.html      ← KPI grid, 3 columns (in progress)
templates/empty-state.html   ← Empty state pattern (in progress)
templates/solo.html          ← Solo screens: login (+ refused account), 404, no access, route error, waiting — one full-viewport .solo-screen per state, switched from „Opțiuni prototip” → Ecran or ?ecran=

── references/ (the style of each surface) ─────────────────────────────
references/referinta-landing-aligned.html  ← REFERENCE: marketing / landing page (desktop-first)
references/referinta-dublin-aligned.html   ← REFERENCE: SaaS dashboard (Dublin, desktop-first)
references/referinta-blog-aligned.html     ← REFERENCE: editorial article (Guides / Blog) — the Reading surface on screen
references/referinta-reading-print-aligned.html ← REFERENCE: the Reading surface on paper — any PDF, in both densities (normal 13pt · compact 11pt for contracts)
references/referinta-mobile-aligned.html   ← REFERENCE: the two mobile shells — .app-mobile (product screens: veiled header + .tabbar) and .flow-chrome (flows) — on real Dublin screens

── slides/ (presented decks — one <section> per file, copied and re-worded) ──
slides/<id>.html             ← the 39 layouts. Base (L01–L19): cover, agenda, section, key-points, bullets,
                               statement, quote, steps, timeline, comparison, big-number, kpi-row, chart, table,
                               image-text, screenshot, photo, decision, closing. From the team's Read & Learn decks
                               (L20–L39): card-grid, pair, mapping, reveal (click build), text-panel, spectrum,
                               status, book, series, passage, dialogue, video, frame, room-question, room-results,
                               join (QR), discussion, do-now, resources, talk-closing. + anatomy.html (guide slide).
                               Slides-artifact format: inline styles only, no classes. Rules: docs/slides.md
slides/deck.json             ← index of the reference deck "Bono · Slide Library" (order, sections, faces, DS record)
slides/player.html           ← the HTML deck player for decks built in Claude Code / deployed on Vercel: paste the same
                               <section>s; 1920×1080 scaling, keys, click builds, video, presenter window (P), deck:change event

── doc-originale/ frozen (original documentation — do NOT use as reference) ─
doc-originale/bono-ds-landing.html  ← ❄️ FROZEN · historical drift, visual only
doc-originale/bono-ds-dublin.html   ← ❄️ FROZEN · historical drift, visual only
doc-originale/bono-ds-blog.html     ← ❄️ FROZEN · historical drift, visual only
doc-originale/bono-ds-system.html   ← ❄️ FROZEN · the set's entry point (its internal nav links the other 3)
doc-originale/bono-ds-original.css  ← ❄️ FROZEN · the CSS the 4 pages render against (the handoff, 4f70c68, 2026-05-15)

── docs/components/ (usage rules — one doc per component) ──
docs/components/<id>.md      ← the id = the name of the components/<id>.json contract.
                               The full mapping is GENERATED in the "Docs per
                               component" table below — it is not maintained by hand.

── docs/ (cross-cutting GUIDES — not per-component documentation) ──────
docs/foundation.md           ← GUIDE · tokens, typography, spacing (scale values & rules)
docs/forms.md                ← GUIDE · form rules: validation, neutral default, required/optional
docs/feedback.md             ← GUIDE · the five-tier notification hierarchy (Silence · Receipt ·
                               Evidence · Obligation · Decision) served by .alert and .toast.
                               THE authority on which one to use
docs/navigation.md           ← GUIDE · navigation hierarchy (topnav vs tabs vs sidebar vs breadcrumbs) + back
docs/signer.md               ← GUIDE · composition of the signing flow + shared tokens and rules
docs/layout.md               ← GUIDE · layout & responsiveness: app-shell, widths, hug, CTA, template screens
docs/templates.md            ← GUIDE · when and how to use templates/
docs/slides.md               ← GUIDE · presented decks: room-sized type scale (36px floor, key message ≥ 60px), grid, colours by role,
                               the Fraunces accent in the Slides format, click builds, the 39 layouts and when to use each,
                               the shape of a Read & Learn session (room + Meet), evidence behind L20–L39
docs/accessibility.md        ← GUIDE · focus states, ARIA, WCAG contrast
docs/reading.md              ← GUIDE · Reading: blog + every PDF — print scale in two densities, page, clauses,
                               signatures, tables, how to generate the PDF

── assets/ (logos — relative path from HTML: ../assets/...) ─────────
assets/icons/<name>.svg      ← Lucide glyphs, copied verbatim — the ONLY source for inline icons
assets/logo-full.svg         ← full logo, pink — PRIMARY
assets/logo-full-black.svg   ← on light non-bej backgrounds
assets/logo-full-white.svg   ← on dark backgrounds / dark mode
assets/logo-mark.svg         ← mark alone, pink — favicon, avatar, app icon
assets/logo-mark-black.svg
assets/logo-mark-white.svg
assets/wordmark.svg          ← text only, pink — discreet footer, co-branding
assets/wordmark-black.svg
assets/wordmark-white.svg
```

> **Outside the DS — ignore when generating:** `ds-slack-bot/` (internal bot for Slack notifications), `.github/workflows/` (CI), `vercel.json`. They are not part of the design system; do not reference them when building pages.

> **Where these commands run.** `scripts/`, `tests/`, `doc-originale/`, `ds-slack-bot/`
> and `.github/` live in the `bono-ro/edge-33` repo, **not** in this skill package — the
> sync vendors `skills/*` only. Every `python3 scripts/…` and `node scripts/…` line below
> is run from a clone of that repo, by whoever maintains the DS; none of them is needed to
> generate a page. What ships with this skill is what the file map above lists — the
> versioning rules and the release procedure stay in the repo, in `VERSIONING.md`.

> **Combination rule (in order):**
> 1. **`templates/`** — if a template exists for what you are building (card, header, sidebar, form, table-view, kpi-grid, empty-state) — **start there**. Copy the template and adapt.
> 2. **`references/`** — for the rhythm, spacing and feel of the surface (Landing / Dublin / Blog).
> 3. **`components/`** — for the markup of individual pieces that have no template.

---

## Canonical CSS source — frozen copy, stamped at copy time

`bono-ds.css` has **A SINGLE source of truth**: the `bono-ro/edge-33` repo (root, branch `main`). The copy of `bono-ds.css` inside this skill is a **synchronized mirror** of the repo. The file is edited ONLY at the source — never in the copies living next to artifacts.

**Which copy you take depends on what you are building** (the rules live in the repo's `VERSIONING.md`):

| Building | Copy from | Stamp reads |
|---|---|---|
| a prototype, demo or artifact | this skill's `bono-ds.css` — it mirrors `main` | `v8.1-dev · …` — unreleased work, and it says so |
| anything that ships to users | the **assets of the latest GitHub Release** | `v8.0 · …` — reviewed, numbered, immutable |

A `-dev` suffix in the stamp means exactly one thing: this came from `main` between releases, so it was never walked through a release review. Fine for a prototype, not for production.

**In any HTML artifact (prototype, page, demo) do NOT hotlink the live deployment.** `main` changes constantly, and a `<link>` to `https://edge-33.vercel.app/bono-ds.css` changes consumers' UI without them asking for it. Instead: **a single copy per project, stamped at the moment of copying, that all artifacts link to relatively.**

Copy procedure (at the first artifact of a project):

1. Copy `bono-ds.css` from the skill into the **project root** (or the artifacts' shared folder), keeping the same name (`bono-ds.css`). A project has **a single copy** — do not put one in every folder.
2. **Stamp the copy** — prepend a provenance header as the FIRST line of the copied file (comments before `@import` are valid CSS). The stamp keywords stay in Romanian — they match the canonical banner convention in `bono-ds.css`:

   ```css
   /*! COPIE ÎNGHEȚATĂ bono-ds — nu edita aici; modificările DS se fac în bono-ro/edge-33
      copiat:  2026-07-29T14:05Z
      sursă:   bono-ro/edge-33 @ a1b2c3d
      canonic: https://edge-33.vercel.app/bono-ds.css */
   ```

   - `copiat:` = date/time of copying (UTC, `date -u +%Y-%m-%dT%H:%MZ`).
   - `sursă:` = the skill's short SHA: `git -C <skill-root> rev-parse --short HEAD`. If the skill is not inside a git repo (e.g. installed as a plugin), use the `--ds-version` value from the file instead (e.g. `sursă: bono-ro/edge-33 · v8.1 · 2026-09-10`). Keep the `-dev` suffix if the file carries one — dropping it turns unreleased work into a copy that claims to be a release.
3. Link relatively in HTML, from each artifact to the project's single copy:

   ```html
   <link rel="stylesheet" href="bono-ds.css">        <!-- artifact in the root -->
   <link rel="stylesheet" href="../bono-ds.css">     <!-- artifact in a subfolder -->
   ```

### Re-stamping is a migration, not a file copy

A stamped copy freezes **values** under **names that stay valid**. So a token whose
hex changes at the source does not touch a single line of consumer code — it lands
silently at the next re-stamp. This has happened twice, and once it broke error
contrast in two consumer apps at the same time.

The reference case: `5288edd`, 2026-07-16 opened up `--c-error` `#B91C1C → #DC2626`
and `--c-error-bg` `#FCEAEA → #FDF1F1`, and introduced `--c-error-text`. Until that
commit `--c-error` **was** the family's text token — there was no other — and it read
5.58:1 on the tint. `color: var(--c-error)` was the correct code. The commit migrated
the DS's own call sites in the same step (`.field-help.error`, `.badge.error`,
`.alert.error` → `--c-error-text`) but stamped copies got the new hex without the
migration: the same seven surfaces that all passed now all fail (5.29–5.90 → 3.95–4.40).
`--ds-version` did not exist yet, so nobody could even tell which stamp they held.

**Therefore, before replacing a stamped copy, diff it:**

```bash
python3 scripts/check-stamp-drift.py path/to/your/bono-ds.css
```

It reports tokens that kept their name and changed value, which contrast pairs cross
the AA line because of it, tokens missing from your copy entirely (a `var()` that will
not resolve), and the declared migration note for each. Exits non-zero when a drift
pushes a pair below AA. To see it on your own screens rather than in the abstract:
`node scripts/check-contrast.mjs --stamp=path/to/your/bono-ds.css <your-screens.html>`
overlays your copy's `:root` and measures what renders.

At the source, `scripts/check-token-values.py` (in CI) makes the silent version
impossible: a token whose value changes without a new `value_history` entry fails the
build, and `breaking: true` requires a `migration` note. `tokens.json` carries that
history — value, date, commit, previous value, what to do — so this diff is always
answerable. See `tests/error-tokens/README.md`.

**How to identify the version of an existing copy** — three mechanisms, in order of specificity:
- the stamp header on the first line (copy date + source SHA);
- the canonical banner `/*! bono-ds v8.1 · sursă: bono-ro/edge-33 · modificat: YYYY-MM-DD */` — travels with any copy. A trailing `-dev` (`v8.2-dev`) means the copy was taken from `main`, not from a release;
- at runtime, in any page that loads the file: `getComputedStyle(document.documentElement).getPropertyValue('--ds-version')`.

**Whether a refresh needs a migration is answerable from the number alone.** The version is `major.minor`, and the major moves only when a release breaks something:

- **same major** (v8.0 → v8.4) → a plain re-copy, verified with `check-stamp-drift.py`;
- **higher major** (v8.4 → v9.0) → **migration**: read the release notes and move the call sites in the same step as the file.

`--ds-breaking-since` in the canonical file names the most recent breaking release (always an `x.0`) and is there for copies that predate the scheme — every stamp from v1 to v7 needs the v8.0 migration.

Rules:

- **One copy per project — a project is always on ONE single DS version.** If the project already has `bono-ds.css`, reuse it for any new artifact — do not create a second copy in another folder. A refresh is a deliberate decision: re-copy + re-stamp the single copy and note explicitly (in the commit / in the reply) that **all** of the project's artifacts moved to the new version — verify them visually after the refresh.
- Do **NOT** paste the CSS content inline into HTML (`<style>`) — the copy lives as a separate file, linked relatively. Inline becomes a silent fork at the first edit.
- Do **NOT** edit the copy and **never** redefine a DS class locally (`.btn`, `.card`, `.badge`, ...). Everything artifact-specific = custom classes **ADDED** on top of the canonical file, scoped (`.signing-footer .btn { ... }`), or propose a new DS component. The copy stays byte-identical to the source except for the stamp — that makes drift auditing trivial (diff minus the first line).
- Do **NOT** use `cdn.jsdelivr.net/gh/bono-ro/edge-33@...` — the repo is private, jsDelivr cannot proxy it, the link is broken.
- Do **NOT** use `raw.githubusercontent.com` — it serves `text/plain` + `nosniff`, and the browser **refuses** the stylesheet (the page ends up unstyled).
- `https://edge-33.vercel.app/bono-ds.css` remains the live publication of `main` — good for inspection and diffing ("what is on main right now?"), NOT as a `<link>` in deliverables.
- **Accepted trade-off:** artifacts no longer receive DS updates automatically. That is intentional — a delivered artifact does not change its UI on its own; an update is a deliberate refresh, visible in the stamp.

---

## How to generate a new interface

### 1. Identify the surface AND the platform

Before anything else, determine the **surface** (Landing / Dublin / Blog — internally **Reading**) AND the **platform** (desktop or mobile). **The platform is a mandatory gate — without it identified with ≥90% certainty, you write NOT ONE line of code.** If it is not clear from context, **ask the question**:

> "What kind of page are we generating? Landing / marketing, a Dublin dashboard, or a blog article? And for which platform — desktop or mobile?"

Clear signals (do not ask again) — they may arrive in English or Romanian:

- "presentation page" / "pagină de prezentare", "landing", "public site", "marketing" → **Landing**
- "presentation" / "prezentare", "deck", "slides" / "slide-uri", "keynote" → **Slides** — a fixed 1920×1080 canvas, so there is no platform question: read `docs/slides.md`, then copy layouts from `slides/`. A deck meant to be READ alone from a link, with no presenter → **Blog**, not Slides
- "dashboard", "client", "KPI", "Dublin", "invoices" / "facturi", "account" / "cont" → **Dublin**
- "article" / "articol", "guide" / "ghid", "blog", "editorial" → **Blog** (= Reading on screen)
- "PDF", "document", "contract", "raport", "ofertă", "proces-verbal", "de tipărit" / "print" → **Reading · print** (`.reading`, A4). **The platform gate does not apply** — paper is neither desktop nor mobile; do not ask. Density: **compact** (`.reading.compact`, body 11pt) for contracts and legal documents, **normal** (body 13pt) for everything else. Read `docs/reading.md` before writing the document
- "mobile" / "mobil", "phone" / "telefon", "tablet" / "tabletă", "380px screen", screenshot/Figma of a viewport under 1024px, "wizard", "onboarding", "steps" / "pași" on a small screen → **mobile → `.flow-chrome` shell mandatory**
- "desktop", "browser", viewport ≥1024px, sidebar, multi-column dashboard, dense table → **desktop → `.app-shell` + `.layout-12`, `.app-shell` = `.app-top` + `.app-footer` for flows, NEVER `.flow-chrome`**

**The DS has ONE breakpoint: `--bp-desktop: 1024px`.** Below it, everything is mobile (tablets included); at and above it, everything is desktop. There is no intermediate breakpoint — `@media (min-width: 1024px)` and `@media (max-width: 1023.98px)` are the only two forms. Full contract: `components/foundation-layout.json` + `docs/components/foundation-layout.md`.

**The platform → shell coupling is bidirectional and exclusive:**

| Platform | Layout | Shells | FORBIDDEN |
|---|---|---|---|
| Mobile (<1024px) | single column, no sidebar | product screens: `.app-mobile` (+ `.app-mobile-top`, `.tabbar`, `.mobile-menu`) · flows: `.flow-chrome` | `.sidebar`, `.topbar`, `.app-top` / `.app-footer`, `.vox-fab` |
| Desktop (≥1024px) | `.app-shell` + `.layout-12` (12 columns, max 1440px), sidebar optional | product screens: `.app-shell` (+ `.sidebar` rail, `.topbar`) · flows: `.app-shell` = `.app-top` + `.app-footer` | `.app-mobile`, `.tabbar`, `.mobile-menu`, `.sheet`, `.flow-chrome` |

**On mobile there are TWO shells, chosen by what the screen is — never both on one screen.** A product screen (home, list, settings, detail) is `.app-mobile`; a flow (wizard, signing, onboarding, a form that ends in one action) is `.flow-chrome`. No `.tabbar` inside a flow, no `.flow-bar` on a product screen. Contracts: `app-nav.json`, `header.json`, `sheet.json`, `list-row.json`.

If the request is ambiguous on platform ("make a settings page"), **always ask before generating**. Do not assume desktop or mobile from past experience — ask.

### 2. Open the visual reference for the surface + platform

| Surface / platform | Visual reference | Main characteristics |
|---|---|---|
| Landing (desktop) | `references/referinta-landing-aligned.html` | bej-0, dot grid, Fraunces `<em>` in hero, a single `.btn.primary.lg` |
| Dublin (desktop) | `references/referinta-dublin-aligned.html` | bej-1 sidebar, bej-0 content, white KPI cards, dense tables |
| Blog (desktop) | `references/referinta-blog-aligned.html` | bej-0, `.t20` / `.t-edit` on body, column max 680px |
| Reading · print (any PDF) | `references/referinta-reading-print-aligned.html` + `docs/reading.md` | `.reading` (normal, body 13pt) or `.reading.compact` (contracts, body 11pt); A4, white page, 16mm side margins, „pagina N din M”; no Fraunces accent; tables never scroll |
| Slides (presented deck, 1920×1080) | `slides/` + `docs/slides.md` | sized for a 140 cm screen and Google Meet: text ≥ 36px, key message ≥ 60px, title 88 + one Fraunces accent word (+2px), the header never moves, footer = deck · page number; every slide is one of 39 layouts; the layouts are content-free (bracketed slots at max length: replace every one with the presenter's material or delete it, never ship template text); every question to the room says how Meet answers. HTML decks (Claude Code / Vercel) start from `slides/player.html` |
| **Mobile (any Dublin screen)** | **`references/referinta-mobile-aligned.html`** | **two shells — `.app-mobile` for product screens (veiled header `.app-mobile-top` + `.tabbar`: Acasă · De făcut · [camera, raised] · Banii tăi · Meniu; a detail screen is `.no-bar` + back) and `.flow-chrome` for flows (back top-left, primary lg CTA at the bottom); veil + blur over scrollable content; menus rise as `.sheet.attached`, rows are `.list-row`** |
| Any surface — shell & responsiveness | `components/foundation-layout.html` | single breakpoint 1024px, `.app-shell` with optional sidebar, `.layout-12` 12-column grid |

Understand the rhythm, spacing and layout patterns before continuing.

**On mobile, the page structure is fixed: a product screen (home, list, settings, detail) ALWAYS starts from `.app-mobile`** (`.app-mobile-scroll` + `.app-mobile-top` + `.tabbar`; a detail screen is `.app-mobile.no-bar` with the `.detail` header), **a flow ALWAYS starts from `.flow-chrome`.** Content goes into `.app-mobile-scroll` / `.flow-chrome-scroll`. Do not build mobile pages with free-form layout, without a shell, and never mix the two shells on one screen — see "Hard rules → DON'T".

### 3. Build

0. **Lookup order: `components/<name>.json` (contract — read first) → `docs/components/<name>.md` (usage rules) — including `sizes`, which says which size is the default and which containers force another one → `components/<name>.html` (human reference) → `templates/` (compositions) → `references/` (surfaces). If `<name>.json` is missing, it is a gap in the DS — flag it, do not invent.**
1. **Reuse the project's single `bono-ds.css` copy** — or, if this is the first artifact, copy it from the skill into the project root + stamp it with the provenance header (see "Canonical CSS source"). Link relatively: `<link rel="stylesheet" href="bono-ds.css">` (or `../bono-ds.css` from a subfolder) — never hotlink the live deployment, never paste the content inline into HTML
2. **Search `templates/`** — if you are building a card, header, sidebar, form, table or empty state, copy the corresponding template
3. **Copy the markup of individual pieces** from `components/` for whatever has no template
4. **Assemble the layout** following the surface's visual reference
5. **Write copy in Romanian** — `tu` form, direct, no exclamation marks

### 4. Atomic audit — before writing any markup

For every visible element, check: **does it exist in the DS?** If yes — you copy the markup, you do not reinvent it.

<!-- LOOKUP:start — GENERATED from components/*.json by scripts/build-catalog.py — do not edit by hand -->
| If you need... | Take markup from... |
|---|---|
| Secondary content that may stay hidden until asked for. A... | <code>components/accordion.html</code> → <code>.accordion</code> |
| Card that ends in ONE large action: a dashboard entry poi... | <code>components/action-card.html</code> → <code>.card.cta</code> |
| Feedback PERSISTENT — treptele 2 și 3 ale ierarhiei de no... | <code>components/alert.html</code> → <code>.alert</code> |
| App navigation — DESKTOP ≥1024: the .sidebar rail (top-le... | <code>components/app-nav.html</code> → <code>.sidebar</code> |
| Visual representation of a person or agent (initials or i... | <code>components/avatar.html</code> → <code>.avatar</code> |
| Short label for status, attention, numerics (tab count),... | <code>components/badge.html</code> → <code>.badge</code> |
| DEPRECAT (v8) — nu folosi în cod nou. Înlocuit de .alert:... | <code>components/banner.html</code> → <code>.banner</code> |
| User-triggered action (submit, open modal, navigate, conf... | <code>components/button.html</code> → <code>.btn</code> |
| Selector de dată, două trepte. .calendar (implicit): ante... | <code>components/calendar.html</code> → <code>.calendar</code> |
| Visual container for grouped content (description, action... | <code>components/card.html</code> → <code>.card</code> |
| Paletă categorică pentru dataviz — identitate, nu magnitu... | <code>components/chart.html</code> → <code>.legend</code> |
| Independent selection of 0..N options from a list, or a s... | <code>components/checkbox.html</code> → <code>.check-item</code> |
| Un câmp care FILTREAZĂ o listă de comenzi sau destinații... | <code>components/command.html</code> → <code>.command</code> |
| Meniul care apare din click-dreapta sau dintr-un „…” pe u... | <code>components/context-menu.html</code> → <code>.popover.compact</code> |
| WIP — what a .tbl becomes below 1024px: one .data-card pe... | <code>components/data-list.html</code> → <code>.data-list</code> |
| Secondary header for an open document THE APP DRAWS (Sign... | <code>components/doc-bar.html</code> → <code>.doc-bar</code> |
| Document list with state (unread / read / current / signe... | <code>components/doc-list.html</code> → <code>.doc-list</code> |
| DEPRECATED (2026-09-16) — do not use in new work. Was: mo... | <code>components/doc-sheet.html</code> → <code>.sheet.attached</code> |
| UN câmp și grupul lui. .field = label + control (+ ajutor... | <code>components/field.html</code> → <code>.field</code> |
| Shows a file a PERSON brought — a PDF invoice, a phone ph... | <code>components/file-viewer.html</code> → <code>.card.tonal.file-viewer</code> |
| Rândul de filtre de deasupra oricărei liste filtrabile (f... | <code>components/filter-bar.html</code> → <code>.filter-bar</code> |
| Shell for SEQUENTIAL flows (wizard, onboarding, declarati... | <code>components/flow-chrome.html</code> → <code>.flow-chrome</code> |
| Reference for the DS color tokens (brand, surfaces, text,... | <code>components/foundation-colors.html</code> → <code>.sec-page</code> |
| Background dot-grid pattern applied to a section (Landing... | <code>components/foundation-grid.html</code> → <code>.has-grid</code> |
| Orice icon inline din interfață. UN singur set: Lucide (l... | <code>components/foundation-icons.html</code> → <code>.ic</code> |
| App shell and responsiveness: the DS's single breakpoint... | <code>components/foundation-layout.html</code> → <code>.app-shell</code> |
| Reference for the design→code naming dictionary. NOT a co... | <code>components/foundation-naming.html</code> → <code>.alert.info</code> |
| Section containers with the corresponding background (bej... | <code>components/foundation-sections.html</code> → <code>.sec-page</code> |
| Reference for spacing, border-radius and shadows. NOT a c... | <code>components/foundation-spacing.html</code> → <code>.sec-page</code> |
| Reference for the typographic scale (Inter + Fraunces + J... | <code>components/foundation-typography.html</code> → <code>.t60</code> |
| Page header — where am I, plus the page's one action. DES... | <code>components/header.html</code> → <code>.topbar</code> |
| Readable HTML version of a document, optimized for mobile... | <code>components/html-doc-view.html</code> → <code>.html-doc</code> |
| Câmp de text simplu (nume, email, sumă liberă, căutare) —... | <code>components/input.html</code> → <code>.input</code> |
| Tot ce stă ÎN INTERIORUL bordurii unui câmp: iconiță, tex... | <code>components/input-group.html</code> → <code>.input-group</code> |
| Card with a qualitative observation (trend, AI suggestion... | <code>components/insight.html</code> → <code>.insight</code> |
| Card with a single main numeric metric (amount, count, pe... | <code>components/kpi.html</code> → <code>.kpi</code> |
| List row — the workhorse of a phone screen (<1024px): set... | <code>components/list-row.html</code> → <code>.list-section</code> |
| Decizie sau formular scurt care ÎNTRERUPE flow-ul: are sc... | <code>components/modal.html</code> → <code>.modal</code> |
| An amount with an attached currency selector, at two size... | <code>components/money-input.html</code> → <code>.amount-field</code> |
| PDF view of an official document (Times font, legal layou... | <code>components/pdf-viewer.html</code> → <code>.pdf-page</code> |
| Overlay ancorat de un declanșator: dropdown menu, popover... | <code>components/popover.html</code> → <code>.popover</code> |
| PROTOTYPES ONLY — the one place for every control that is... | <code>components/proto-menu.html</code> → <code>.proto</code> |
| Small visual indicator for live activity (new notificatio... | <code>components/pulse-dot.html</code> → <code>.pulse-dot</code> |
| A record read as label–value facts (quanta): the details... | <code>components/quantum-stack.html</code> → <code>.quantum-stack</code> |
| Mutually exclusive selection of a single option from a gr... | <code>components/radio.html</code> → <code>.radio-item</code> |
| PATTERN. One record, read-only, in a modal: the kind of r... | <code>components/record-view.html</code> → <code>.modal.record-view</code> |
| Alegere dintr-o listă închisă de opțiuni discrete (catego... | <code>components/select.html</code> → <code>.select</code> |
| Linie de despărțire între secțiuni de conținut. NU pentru... | <code>components/separator.html</code> → <code>.separator</code> |
| Bottom sheet — the ONE surface that rises from the bottom... | <code>components/sheet.html</code> → <code>.sheet</code> |
| Desktop layout for the signing flow: document-list sideba... | <code>components/signer-layout.html</code> → <code>.signer-layout</code> |
| Locul unui conținut care încă nu a sosit, când îi știi fo... | <code>components/skeleton.html</code> → <code>.skeleton</code> |
| PATTERN. A screen that lives OUTSIDE the app shell — no s... | <code>components/solo-screen.html</code> → <code>.solo-screen</code> |
| Indicator de așteptare. Peste 24px, loader-ul Bono (asset... | <code>components/spinner.html</code> → <code>.spinner</code> |
| Progress indicator for a linear multi-step process (wizar... | <code>components/stepper.html</code> → <code>.stepper</code> |
| Binary toggle on a single option (notifications on/off, d... | <code>components/switch.html</code> → <code>.switch</code> |
| Tabular data with multiple rows and columns (transactions... | <code>components/table.html</code> → <code>.tbl</code> |
| Horizontal filters over the same data set (All / Income /... | <code>components/tabs.html</code> → <code>.tabs</code> |
| The steps a record went through, in the order they happen... | <code>components/timeline.html</code> → <code>.timeline</code> |
| Treapta 1 a ierarhiei de notificări: CHITANȚA. Ceva s-a î... | <code>components/toast.html</code> → <code>.toast</code> |
| Buton care ține o stare. Solo pentru on/off independent (... | <code>components/toggle.html</code> → <code>.toggle</code> |
| Ajutor scurt, o propoziție, la hover ȘI focus (pe telefon... | <code>components/tooltip.html</code> → <code>.tooltip</code> |
| DEPRECATED — do not use in new work; replacement TBD. Was... | <code>components/topnav.html</code> → <code>.v3-topnav</code> |
| Câmp cu sugestii ancorate. typeahead = text liber care fi... | <code>components/typeahead.html</code> → <code>.typeahead</code> |
| The user uploads documents (receipt, invoice, bank statem... | <code>components/uploader.html</code> → <code>.uploader</code> |
| Floating action button for the Vox agent — always bottom-... | <code>components/vox-fab.html</code> → <code>.vox-fab</code> |
| Entry page of the edge-33 doc site. NOT a reusable compon... | <code>components/welcome.html</code> → <code>.sec-page</code> |
<!-- LOOKUP:end -->

**Absolute rule: if the element is in the table → copy the markup, do not reinvent it.**

### 5. Pre-deploy checklist

Run before any delivery and display the result as a single check line:

```
✅ Platform identified explicitly (mobile or desktop) — declared in the reply, not assumed
✅ Mobile → one of the two shells (.app-mobile for a product screen, .flow-chrome for a flow) · Desktop → .flow-chrome, .app-mobile, .tabbar, .sheet ABSENT (zero occurrences)
✅ Single breakpoint — only @media (min-width: 1024px) / (max-width: 1023.98px); zero 768px / 640px / "tablet" breakpoints
✅ Desktop → .app-shell + .layout-12 (12 columns, spans via .col-*), sidebar optional as a rail outside the grid
✅ bono-ds.css = the project's single copy, with a provenance stamp on line 1 (copiat + sursă), relative link — no second copy in another folder, no hotlink to edge-33.vercel.app, no inline content in HTML
✅ All core elements come from components/ (no reinvention)
✅ Zero hardcoded colors outside the CSS tokens
✅ Prototype → every prototype control (Design, Stare, variants, Simulează, Reset) is a Proto.* call in „Opțiuni prototip” — none in the screen, none on .btn / .tabs / .switch
✅ Zero invented (non-DS) CSS classes
✅ Focus states per the hard rules (fog outline / input box-shadow)
✅ Copy in Romanian, no exclamation marks
✅ Inputs/controls in neutral default (zero brand on functional states)
✅ Trust/safety icons in green/neutral, "read" status on --c-read
✅ Component density per contract — a component declaring `sizes` uses the size its `required_in` demands inside those containers (badge: `.badge.sm` in `.tabs` / `.tbl` / `.doc-list` / `.accordion-title` / `.sidebar`), the declared `default` everywhere else, one density per container
✅ Only DS typography classes, 14px baseline respected
✅ Heading tags paired with their typography class (h1→t-h1/t48, h2→t-h2/t40, modal h2→t28) — lint clean
```

**Programmatic platform check** (run the grep after writing the artifact):
- If the artifact is **desktop**: `grep -c "flow-chrome\|flow-bar\|flow-header\|flow-footer\|flow-sheet\|app-mobile\|tabbar\|mobile-menu\|sheet-scrim" <file>` must return **0**. Any occurrence = ❌.
- If the artifact is **mobile**: `grep -c "app-mobile\|flow-chrome" <file>` must return **≥1** (the outer shell). Plus `grep -c "app-footer\|class=\"sidebar\|topbar\|vox-fab" <file>` must be **0** (desktop chrome, forbidden). And the two shells do not mix: a product screen (`app-mobile`) has zero `flow-bar`, a flow (`flow-chrome`) has zero `tabbar`.

**Programmatic breakpoint check** (one breakpoint, no exceptions):
- `grep -oE "@media[^{]*width: *[0-9.]+px" <file> | grep -v "1024px\|1023.98px"` must be **empty**. Any other width = an invented breakpoint = ❌.

If any item is ❌ — fix it before delivering.

---

## Component status

`wip` and `preview` are visible but not to be relied on. `deprecated` carries
`deprecated_since` in its contract — the release that announced it — and is removed
**no earlier than N+2** (announced in v8 → removable in v10 at the earliest). The
Wednesday release agenda lists every deprecation whose N+2 has passed, so removal is
decided from a list rather than remembered — the rule lives in the repo's `VERSIONING.md`.

<!-- STATUS:start — GENERATED from components/*.json by scripts/build-catalog.py -->
| Component | File | Status |
|---|---|---|
| accordion | accordion.html | Stable |
| action-card | action-card.html | Stable |
| alert | alert.html | Stable |
| app-nav | app-nav.html | Stable |
| avatar | avatar.html | Stable |
| badge | badge.html | Stable |
| banner | banner.html | Deprecated |
| button | button.html | Stable |
| calendar | calendar.html | Stable |
| card | card.html | Stable |
| chart | chart.html | Wip |
| checkbox | checkbox.html | Stable |
| command | command.html | Stable |
| context-menu | context-menu.html | Stable |
| data-list | data-list.html | Wip |
| doc-bar | doc-bar.html | Wip |
| doc-list | doc-list.html | Wip |
| doc-sheet | doc-sheet.html | Deprecated |
| field | field.html | Stable |
| file-viewer | file-viewer.html | Stable |
| filter-bar | filter-bar.html | Stable |
| flow-chrome | flow-chrome.html | Stable |
| foundation-colors | foundation-colors.html | Stable |
| foundation-grid | foundation-grid.html | Stable |
| foundation-icons | foundation-icons.html | Stable |
| foundation-layout | foundation-layout.html | Stable |
| foundation-naming | foundation-naming.html | Stable |
| foundation-sections | foundation-sections.html | Stable |
| foundation-spacing | foundation-spacing.html | Stable |
| foundation-typography | foundation-typography.html | Stable |
| header | header.html | Stable |
| html-doc-view | html-doc-view.html | Wip |
| input | input.html | Stable |
| input-group | input-group.html | Stable |
| insight | insight.html | Stable |
| kpi | kpi.html | Stable |
| list-row | list-row.html | Stable |
| modal | modal.html | Stable |
| money-input | money-input.html | Stable |
| pdf-viewer | pdf-viewer.html | Wip |
| popover | popover.html | Stable |
| proto-menu | proto-menu.html | Stable |
| pulse-dot | pulse-dot.html | Stable |
| quantum-stack | quantum-stack.html | Stable |
| radio | radio.html | Stable |
| record-view | record-view.html | Stable |
| select | select.html | Stable |
| separator | separator.html | Stable |
| sheet | sheet.html | Stable |
| signer-layout | signer-layout.html | Wip |
| skeleton | skeleton.html | Stable |
| solo-screen | solo-screen.html | Stable |
| spinner | spinner.html | Stable |
| stepper | stepper.html | Stable |
| switch | switch.html | Stable |
| table | table.html | Stable |
| tabs | tabs.html | Stable |
| timeline | timeline.html | Stable |
| toast | toast.html | Stable |
| toggle | toggle.html | Stable |
| tooltip | tooltip.html | Stable |
| topnav | topnav.html | Deprecated |
| typeahead | typeahead.html | Stable |
| uploader | uploader.html | Stable |
| vox-fab | vox-fab.html | Stable |
| welcome | welcome.html | Stable |
<!-- STATUS:end -->

---

## Docs per component

Per-component usage rules live in `docs/components/<id>.md` — the `docs` field in the JSON contract is the source of truth (mandatory for `stable` components). The table is generated; do not edit it by hand.

<!-- DOCS:start — GENERATED from components/*.json by scripts/build-catalog.py — do not edit by hand -->
| Component | Usage doc |
|---|---|
| accordion | <code>docs/components/accordion.md</code> |
| action-card | <code>docs/components/action-card.md</code> |
| alert | <code>docs/components/alert.md</code> |
| app-nav | <code>docs/components/app-nav.md</code> |
| avatar | <code>docs/components/avatar.md</code> |
| badge | <code>docs/components/badge.md</code> |
| banner | <code>docs/components/banner.md</code> |
| button | <code>docs/components/button.md</code> |
| calendar | <code>docs/components/calendar.md</code> |
| card | <code>docs/components/card.md</code> |
| chart | <code>docs/components/chart.md</code> |
| checkbox | <code>docs/components/checkbox.md</code> |
| command | <code>docs/components/command.md</code> |
| context-menu | <code>docs/components/context-menu.md</code> |
| data-list | <code>docs/components/data-list.md</code> |
| doc-bar | <code>docs/components/doc-bar.md</code> |
| doc-list | <code>docs/components/doc-list.md</code> |
| doc-sheet | <code>docs/components/doc-sheet.md</code> |
| field | <code>docs/components/field.md</code> |
| file-viewer | <code>docs/components/file-viewer.md</code> |
| filter-bar | <code>docs/components/filter-bar.md</code> |
| flow-chrome | <code>docs/components/flow-chrome.md</code> |
| foundation-colors | <code>docs/components/foundation-colors.md</code> |
| foundation-grid | <code>docs/components/foundation-grid.md</code> |
| foundation-icons | <code>docs/components/foundation-icons.md</code> |
| foundation-layout | <code>docs/components/foundation-layout.md</code> |
| foundation-naming | <code>docs/components/foundation-naming.md</code> |
| foundation-sections | <code>docs/components/foundation-sections.md</code> |
| foundation-spacing | <code>docs/components/foundation-spacing.md</code> |
| foundation-typography | <code>docs/components/foundation-typography.md</code> |
| header | <code>docs/components/header.md</code> |
| html-doc-view | <code>docs/components/html-doc-view.md</code> |
| input | <code>docs/components/input.md</code> |
| input-group | <code>docs/components/input-group.md</code> |
| insight | <code>docs/components/insight.md</code> |
| kpi | <code>docs/components/kpi.md</code> |
| list-row | <code>docs/components/list-row.md</code> |
| modal | <code>docs/components/modal.md</code> |
| money-input | <code>docs/components/money-input.md</code> |
| pdf-viewer | <code>docs/components/pdf-viewer.md</code> |
| popover | <code>docs/components/popover.md</code> |
| proto-menu | <code>docs/components/proto-menu.md</code> |
| pulse-dot | <code>docs/components/pulse-dot.md</code> |
| quantum-stack | <code>docs/components/quantum-stack.md</code> |
| radio | <code>docs/components/radio.md</code> |
| record-view | <code>docs/components/record-view.md</code> |
| select | <code>docs/components/select.md</code> |
| separator | <code>docs/components/separator.md</code> |
| sheet | <code>docs/components/sheet.md</code> |
| signer-layout | <code>docs/components/signer-layout.md</code> |
| skeleton | <code>docs/components/skeleton.md</code> |
| solo-screen | <code>docs/components/solo-screen.md</code> |
| spinner | <code>docs/components/spinner.md</code> |
| stepper | <code>docs/components/stepper.md</code> |
| switch | <code>docs/components/switch.md</code> |
| table | <code>docs/components/table.md</code> |
| tabs | <code>docs/components/tabs.md</code> |
| timeline | <code>docs/components/timeline.md</code> |
| toast | <code>docs/components/toast.md</code> |
| toggle | <code>docs/components/toggle.md</code> |
| tooltip | <code>docs/components/tooltip.md</code> |
| topnav | <code>docs/components/topnav.md</code> |
| typeahead | <code>docs/components/typeahead.md</code> |
| uploader | <code>docs/components/uploader.md</code> |
| vox-fab | <code>docs/components/vox-fab.md</code> |
| welcome | <code>docs/components/welcome.md</code> |
<!-- DOCS:end -->

---

## Logo — usage rules

| Variant | File | When |
|---|---|---|
| Full logo, pink | `assets/logo-full.svg` | **Default** — nav, hero, og:image. Always the first choice. |
| Full logo, black | `assets/logo-full-black.svg` | Light non-bej backgrounds (pure white, print) |
| Full logo, white | `assets/logo-full-white.svg` | Dark backgrounds / dark mode / over photos |
| Mark, pink | `assets/logo-mark.svg` | Favicon, avatar, app icon, very small spaces |
| Wordmark | `assets/wordmark.svg` | Discreet footer, co-branding, text-only contexts |

**Hard rules:**
- Use `logo-full.svg` (pink) whenever space allows — it is the primary logo
- Do not scale the mark below 24px height
- Do not apply effects (shadow, outline, opacity) to the logo
- Do not recreate the logo in CSS/text — use the SVG file

---

## Anti-reinvention gate

Before writing CSS for any element in the catalog (button, tab/segment, switch, input, select, checkbox, radio, badge, alert, insight, modal, kpi, card, avatar, table, topnav, app-nav (sidebar · tabbar · mobile-menu), header (topbar · app-mobile-top), sheet, list-row, data-list, vox-fab, app-footer, flow-chrome, stepper, uploader / drop zone, doc-*, prototype controls (Desktop/Mobil, state, variant, reset, „Simulator” → proto-menu), logo…):

1. Resolve the name → `components/<name>.json`. The component name IS the file name.
2. It exists → read the contract, copy `markup`, respect `anti_patterns`. Do NOT restyle, do NOT reinvent, do NOT redefine the class in a local `<style>`.
3. It does not exist (404 / missing) → it is a GAP in the DS. Stop and flag the gap. NEVER style the component from scratch.
4. Declare the check: "checked `components/tabs.json` → exists → copied" or "checked → does not exist → gap flagged". Silent skipping = forbidden.

Logo: it does not live in the catalog as CSS. Source = `assets/logo-*.svg`. Never text/CSS. Full logo in nav/hero, mark ≥24px in rails.

The reason: an agent builds from the example in front of it; when the canonical is not in context, it slides onto training defaults (black tab, text logo). The gate forces the lookup at exactly the point of slippage.

---

## Hard rules

**DO**
- **EVERY prototype control lives in „Opțiuni prototip” (`.proto`, `components/proto-menu.json`).** Design (Desktop / Mobil), the screen in a simulator, the data state, design variants, on/off switches, events someone else causes in production („the lawyer accepts”, „the e-mail arrived”) and Reset are `Proto.states` / `Proto.check` / `Proto.simulate` / `Proto.action` calls from `proto/proto-menu.js` — one purple pill, bottom-left, one per page. NEVER in the product UI (no „Simulator” card under the screen's buttons, no „Resetează demo” `.btn`, no state tabs in a header, no „Simulare” tab in the navigation, no „Demo · Configurare” screen), NEVER drawn with product DS components (`.btn`, `.tabs`, `.switch`, `.select`, `.card`), NEVER a second floating control (stacked pills, a white demo panel, a tour bar). Standard labels: Design · Ecran · Stare · Variantă · <ce> · Reset (last). In a phone simulator the menu belongs to the host page, not the framed screen. The two `proto/` files are copied into prototypes only — the handoff deletes them and the `Proto.*` calls and nothing else on screen changes. `scripts/check-proto-controls.py` (repo) fails a prototype that breaks this. See `docs/components/proto-menu.md`.
- **On mobile, EVERY screen starts from `.flow-chrome`** — back top-left, a single primary lg CTA in the bottom footer. Settings, client details, form, wizard, confirmation screen — all of them. Header center: a plain title (normal screen) or `.flow-step-title` with a chevron (only multi-step wizards with a steps sheet). See `docs/components/flow-chrome.md`.
- **EVERY modal-type overlay carries the canonical scrim.** `.modal-backdrop`, `.sheet-scrim` (and its deprecated aliases `.doc-sheet-backdrop`, `.flow-sheet-scrim`) — and any future one — read `background: var(--c-backdrop)` (ink @ 25%) + `backdrop-filter: blur(var(--scrim-blur))` (6px) directly. This is also the DS's only sanctioned use of `backdrop-filter` — no glassmorphism/backdrop-blur anywhere else. Never a literal `rgba(...)`/`blur(Npx)` on the overlay itself, never a different dim%/blur, never a parallel class (`.du-modal-backdrop`) that reimplements the pair locally — if a scrim looks off, fix the two tokens, not the consumer.
- **EVERY inline icon is a Lucide glyph carrying `.ic`.** Copy the SVG verbatim from `assets/icons/<name>.svg` (missing one → copy it from lucide.dev into that folder first, verbatim, and run `scripts/check-icons.py --write`). `viewBox="0 0 24 24"`, `stroke="currentColor"`, `stroke-width="2"` stay as Lucide ships them; the size class renders the stroke in screen pixels on the **optical scale** (12→1.25 · 14/16→1.5 · 18/20→1.75 · 24→2; 16px = Lucide's Size 16 · Stroke 1.5 · Absolute) and the size is a class (`.ic.xs` 12 · `.ic.sm` 14 · `.ic` 16 · `.ic.md` 18 · `.ic.lg` 20 · `.ic.xl` 24) or the parent's rule — inside `.btn` and `.toggle` the icon carries ONLY `.ic` and the step sets the size (next to text 12/14/16/20, `.icon-only` 12/16/18/20 on `.xs`/`.sm`/default/`.lg`) — never `width`/`height` attributes, never a size modifier inside a button, never a `stroke-width` tuned in markup, never a hand-drawn path, never a second icon library. Feedback glyphs: neutral `info`, success `check`, warning `triangle-alert`, error `circle-x`, brand `circle-arrow-right`, close `x`, destructive `trash-2`. `scripts/check-icons.py` blocks regressions. See `components/foundation-icons.html`.
- `--c-accent` for visual interactive states (checked, active, selected) — **not** for the focus ring
- Focus ring: `outline: 2px solid var(--c-fog)` on buttons/switch/tabs, `border-color + fog box-shadow` on input/select. On dark surfaces — `outline-color: #fff`
- Checkbox/Radio: native markup, `<label>` wraps the `<input>` — no JS for the checked state
- **Checkbox / radio / switch share ONE type scale: label `15/22 --c-ink`, description `13/18 --c-fog`.** `.check-item` and `.radio-item` declare both steps themselves (since 2026-09-21), so the text never inherits the host page's base size — write the label as `<span class="check-label">` / `<span class="radio-label">` with `.sublabel` INSIDE it, and the switch's as `.label` + `.sublabel` in a `.field.horizontal`. NEVER re-state the size at the call site (`font-size: 14px`, `text-sm`, `.t14`): a host base of 14 or 16 is exactly what the wrapper's declaration exists to stop — one control used to render at three different sizes because the contract never named one. See `docs/components/checkbox.md`.
- `.check-item.card` / `.radio-item.card` — the whole card is clickable, `--c-accent` border + `--c-accent-ultra` background when selected; used in wizards and selections with context
- Radio always appears in a group of at least 2, all with the same `name`. A single option → switch or checkbox
- Indeterminate checkbox: set exclusively via JS `input.indeterminate = true`
- **Fraunces comes EXCLUSIVELY through 2 classes from `bono-ds.css`** — never inline `font-family: 'Fraunces'`:
  - `<em class="t-em">word</em>` — accent on headings from the **T60 / T48 / T40** scale (Landing) and **T28** (Dublin modal title, the action word). Italic **700**, pink, **at most 3 words** per heading. The chosen word = the emotional or temporal charge, or the action (verb) of the title — never digits, never the whole title.
  - `<span class="t-actor">Vox</span>` — names of AI agents (italic **500**, pink). Size is relative: `calc(1em + 1px)` over the host line, so it matches 11px meta and 17px body alike — do NOT add `font-size` on top of `.t-actor`.
  - **Always use the explicit `.t-em` class** — do not rely on a bare `<em>` even if the heading applies Fraunces via a descendant selector. The explicit class is grep-able, survives moving the text outside the heading, and is a single source for the optical bump.
  - The Fraunces vs Inter optical compensation is automatic via the `--t-em-bump: 2px` token (+2px fixed, at any size) — do NOT add `font-size` on top of `.t-em`.
  - Any other weight or context = a violation. Below T28 = no Fraunces in headings (use Inter italic or `<strong>`).
- `.t-eyebrow` with `.dot` for section labels
- `--sh-pink` shadow **only on hover** on `.btn.primary` — zero shadow in default
- Mono numerics (`.tn40`, `.tn28`) for amounts and KPIs
- Inputs: white on a bej-1 card (Dublin context), tonal on a white card (wizard), lg in modals
- **Controls in NEUTRAL default** — `--c-input-border` border (input/select/small controls — ink @ 30%, contrast on bej) or `--c-fog`; containers (card/KPI) stay on `--c-rule-card` (ink @ 12%). Brand (`--c-accent`) only as a rare accent, never on functional states or defaults. Use the DS controls; if you invent a new one, follow the same rule
- Every component references the tokens from `bono-ds.css` — no parallel class trees per component
- **Icon default** = light semantic-token background (`*-bg`) + dark semantic-token glyph
- **Safety/trust icons** (lock, shield, password, eye, signature) → `--c-success` or neutral (`--c-ink` / `--c-fog`), **never brand** (pink is too close to red for trust signals)
- "Read / seen" status → `--c-read` / `--c-read-bg`, never another color
- Badge chosen by **significance**. Brand, attention and the semantic families answer **different questions**, so they occupy different slots and can share a row. Brand (`--c-accent`, `.badge.pink`) answers „who is this from, what does Bono do for you” — a benefit („Nou”, „Beta”, „Pro”) or a surface Bono assembled proactively. **A proactive surface KEEPS its brand** — a „De făcut” list, a suggestion from Vox, a „we handled this” is the brand promise, and it wears brand in its FRAME: the section label (`.t-eyebrow` + `.dot`), the actor attribution (`.t-actor`), `.alert.brand`. Stripping brand out of it to play safe is the opposite error. **On the object, pink means ATTENTION, never HEALTH** — exactly three statuses take `.badge.pink`: „De făcut” / „To Do” (waiting on the viewer), „Necitit” / „Nou” (not seen yet — the inverse of `.badge.read`), „Curent” (the current item in a history or sequence). Health stays semantic and pink never stands in for it: paid/approved → `.success`, a deadline („Scadent azi”) → `.warn` even when it is also a to-do, rejected/blocked → `.error`, read → `.read`, live → `.live`; a count takes `.neutral` (`.outline` on bej or tinted surfaces), a bucket marker („la ordinea zilei”, „Azi”) takes `.neutral` because it sorts rather than asks. Guardrails for the attention pink (pink sits close to red): never beside `.badge.error` on the same row, at most one pink status per row, never inside `.alert.error` / `.alert.warning`, tonal only (never `.filled`). Inside a feedback surface the badge follows the SURFACE's state: `.alert.neutral` → `.badge.sm.outline` (its surface is `--c-bej-1`, exactly `.badge.neutral`'s fill). Outside the component the same split holds — a pink dot marking a SECTION is the frame and a pink dot marking an UNREAD row is attention, both correct; a pink dot grading an item's HEALTH is the violation. Lint rule #20 catches it in the references; `badge.json` / `alert.json` carry it for the audit
- A card's surface color is chosen by **content type** (see `docs/components/card.md`)
- Typography: **DS classes only** (`.t*`). Baseline = **14px** (`.t14`); smaller classes (`.t12`) only for non-crucial information. **The floor is 11px** (`.t11` / `.t-label`) and it is for uppercase labels and tabular numerals only — column heads in a data card, KPI labels, group labels in a menu — never prose. Nothing under 11 exists in the UI except `.pdf-*`
- Long documents meant for reading (user-facing content) → **a single type size**
- **Every PDF is the Reading surface** — `.reading` (or `.reading.compact` for contracts), never the app scale. Sizes on paper come from the DS print scale in `docs/reading.md`; no `font-size` written in the document
- The active document's title stays `--c-ink` in **all states** in which it can be opened
- **Heading tag ↔ typography class — a mandatory pair.** `<h1>` → `.t-h1` (`.t48`); page `<h2>` → `.t-h2` (`.t40`); modal title → `.t28` on `<h2>` (not `<h1>`); section/card title → `.t18`. Never `<h1 class="t28">` or any other tag↔size mismatch — semantics and visual hierarchy must not diverge. Solo screens (`.solo-screen`) follow the same pair — `h1.t-h1`, no smaller solo heading (decided 2026-10-28, #127)

**DON'T**
- No new colors outside the tokens — ONE declared exception: the prototype purple (`#4C1D95`, text `#EDE9FE`), which exists precisely because it is not in the Bono palette (it says „not the product”). It lives only in `proto/proto-menu.css`, is never a `:root` token, never colours anything in a product and never appears in print
- No gradient on text, no shadow in the default state
- No emoji in the UI
- No old aliases (`--c-bg`, `--c-surface`, `--c-night`, `--c-bg-card`)
- No `.avatar.gradient` — removed in v6
- No `<link>` to `https://edge-33.vercel.app/bono-ds.css` in artifacts — the deployment follows `main` and changes consumers' UI on every push. Use the project's single stamped copy (see "Canonical CSS source")
- No `bono-ds.css` content pasted inline into HTML — the copy lives as a separate, unmodified file, linked relatively
- No local redefinition of a DS class (`.btn`, `.card`, `.badge`, ...) — extend with a scoped selector or propose a new DS component
- **NEVER inline `font-family: 'Fraunces'`** in HTML `style=""` or local CSS — use `.t-em` or `.t-actor` from `bono-ds.css`. Pre-push lint: `grep -rn "font-family:.*Fraunces" --include="*.html" | grep -v "t-em\|t-actor"` must be empty.
- **No bare `<em>` (without `.t-em`) in headings** — even if the descendant selector `.t60 em, .t-display em, ...` applies the same style, the rule is: the Fraunces word **always** carries `class="t-em"`. The reason: grep-ability, moving the text outside the heading (into a breadcrumb, OG title, summary), and ensuring the optical bump is applied from a single source.
- **No `font-size` on top of `.t-em`** — the Fraunces vs Inter optical bump (+2px fixed, at any size) is handled by the `--t-em-bump` token applied in `.t-em` and in the `.t60 em, .t-display em, ...` block via `calc(1em + var(--t-em-bump))`. If you override `font-size` on `.t-em` (inline or via a custom class), you lose the optical correction or apply it inconsistently.
- **NEVER `<h1>` with a smaller size class** (e.g. `<h1 class="t28">`, a modal size on a page `<h1>`) — the semantic tag and the size must be a pair. Pre-push lint: `grep -rnE "<h1 " --include="*.html" . | grep -vE "t-h1|t48|t-display|t60"` must be empty (accepted exception: landing hero on `.t-display`/`.t60`).
- **`.flow-chrome` and `.app-mobile` are EXCLUSIVELY mobile — never on desktop.** The mobile shells (`.flow-chrome` + all `.flow-*` classes for flows; `.app-mobile` + `.app-mobile-top` + `.tabbar` + `.mobile-menu` for product screens; `.sheet` for whatever rises from the bottom) live only below the single breakpoint (`--bp-desktop: 1024px`), tablets included. From 1024px up, the counterparts are `.app-shell` (+ the `.sidebar` rail and `.topbar`) for product screens and `.app-shell` = `.app-top` (flow header: left-aligned stepper, close top-right, 3px progress line — no back, no logo) + `.app-footer` (back + CTA at default md size) for flows. Do not mix: no `.flow-chrome` / `.tabbar` / `.sheet` in a desktop Dublin page, no `.app-footer` / `.sidebar` / `.topbar` / `.vox-fab` in a mobile page. The rule is bidirectional and without exception. If you do not know the platform → ask, do not guess.
- **Two mobile shells, chosen by what the screen is — never both on one screen.** Product screen (home, list, settings, detail) → `.app-mobile`: `.app-mobile-scroll` + `.app-mobile-top` (root: logo only — no hamburger, the menu is the Meniu tab; section: title · ONE action; detail: back · centered title · `.spacer`, on a host `.app-mobile.no-bar`) + `.tabbar` (≤5 `.tabbar-item`; Dublin: Acasă · De făcut · [camera] · Banii tăi · Meniu — the raised action is `.tabbar-action > .btn.primary.lg.icon-only` with the camera glyph, because uploading an expense is the main job on a phone; Meniu is the last item, a `<button>` that opens `.mobile-menu`; Vox, if a product brings it to the phone, is a `.tabbar-item` with `message-circle` inside the five, the counter is `.badge.sm.filled.pink`) + `.mobile-menu[hidden]` (the rest of the rail, as `.list-row` in `.list-group`). Flow (wizard, signing, onboarding) → `.flow-chrome`. No `.tabbar` inside a flow, no `.flow-bar` on a product screen, no `.vox-fab` below 1024px. The title in a header is chrome (`div.title`), the page's `h1.t-h1` stays in the content. Rows on the phone are `.list-row` in `.list-group` under a `.t12` label (`list-row.json`): `.link` / `.value` / `.control` (a `.switch` that saves instantly — the screen has no CTA) / static / `.danger` (ink 600, never red). A menu opened from a button or row is `.sheet.attached` with `.list-group.sm` (44px rows); a short form (≤2 fields) is `.sheet.form` with `.input.lg` / `.select.lg`; an explanation is `.sheet.floating`; suggestions under a field you type in stay `.popover`; a decision stays `.modal`. `.kpi` stays on mobile (its compact container-query form IS the mobile form). `.flow-sheet*`, `.flow-menu-*`, `.doc-sheet*` are deprecated aliases — not in new code. Contracts: `app-nav.json`, `header.json`, `sheet.json`, `list-row.json`, `data-list.json` (WIP — consumers stay on `.tbl` until it settles; never a horizontally scrolling table on a phone).
- **No invented breakpoints.** The DS has exactly one: `--bp-desktop: 1024px`. The only two media-query forms are `@media (min-width: 1024px)` (desktop) and `@media (max-width: 1023.98px)` (mobile band). A `768px`, `640px` or "tablet" breakpoint in new work is drift — if a layout seems to need one, simplify the layout. For a card / KPI row that must reflow before 1024px, use an intrinsic grid (`repeat(auto-fit, minmax(240px, 1fr))`) or a container query (`container-type: inline-size` on the content column + `@container`), never a new viewport breakpoint. A container query measures the real content width, so it stays correct with or without the sidebar. Such a threshold is local to the screen that needs it — it lives in that screen's CSS and never becomes a second system breakpoint. Values live in tokens; media queries write the number literally because custom properties do not work in `@media`.
- **No sidebar inside the 12 columns.** On desktop the sidebar is optional and is a fixed rail (`--sidebar-w` 240px, `--sidebar-w-icon` 64px) outside the grid; `.layout-12` keeps its 12 columns with or without it. Never `.col-3` on `<nav class="sidebar">`, never an ad-hoc `grid-template-columns` to fake columns — compose with `.col-1`…`.col-12`.
- **No mobile pages without a shell** — a product screen starts from `.app-mobile`, a flow from `.flow-chrome`; never an ad-hoc header + a free-floating button in the page. In a flow, back top-left + bottom CTA, always. Single exception: a purely informational flow screen with no primary action (rare) — then hide `.flow-bar.bottom`, but keep `.flow-bar.top` with the back button. Declared exception: the solo screens (`.solo-screen` — login, 404, no access, route error, waiting) live outside every shell at every width, because there is nothing to navigate yet (`components/solo-screen.json`).
- **No two CTAs side by side in `.flow-footer`** — a single one, primary lg, full width. If you need a secondary action, put it in `.flow-bar-btn` top-right or as a text link above the CTA, not beside it.
- **No styled `<input type="file">` and no parallel dropzone.** Document upload is `.uploader` (`.uploader.is-compact` inside a form). The drop zone is the **empty state** of a fixed-height slot (`--up-h` 218px, compact 64px) and it **disappears after the first upload** — more files come in through „Adaugă documente". The zone's background is transparent (it inherits the surface), its dotted outline is the `.uploader-outline` SVG and never `border: dashed`, and an upload error lives **on the file row** (`.uploader-item.is-error`), never in a banner or `.alert` beside the list. See `docs/components/uploader.md`.
- **Feedback is a five-tier hierarchy served by TWO components** (`docs/feedback.md`). One principle: **who holds the information after the message disappears.** T0 **Silence** — the app interface holds it and the user is already looking at it → **no message at all** (a status column that changes, a balance that updates; a product decision, but ask it before every toast). T1 **Receipt** — the app interface holds it, the user may not be looking → `.toast`. T2 **Evidence** — the message is the only carrier → `.alert`, inline or `.page`. T3 **Obligation** — evidence that also demands work → `.alert.page` + `.alert-actions` with **no dismiss**. T4 **Decision** — nothing proceeds until answered → `.modal`. **Tiers go up, never down**: a failure does not become a receipt because it is small. **The test between T1 and T2 is „does the app interface still carry the information after the message disappears?"** — NOT „is it still true", which fails on success: „Factura a fost trimisă" is true forever, yet the row now reads *Trimisă*, so the app interface carries it and the toast is correct. The asymmetry behind it: **success advances state (visible in the app interface → a receipt, disposable), failure blocks it (invisible defect → evidence, not disposable).** Two corollaries: **a toast may never be the only place a piece of data exists** (if the user might write it down — a filing reference — it is not a toast), and **a toast's one optional action is a bridge to where the app interface holds the information** (undo is the exception that proves it: the only case where the toast holds what the app interface does not yet, hence the only action with a deadline). On `.alert`, **dismiss and actions are independent signals and their combination is the tier**: no dismiss + no actions = the page's ambient context (Carbon's Callout, and it takes no ARIA role); no dismiss + actions = a T3 obligation, where **postponement is an explicit button, never an X** — an X cannot distinguish „seen" from „never show me again", while „Amână 24h" is a promise the product can keep and log. `.alert.page` is max 1 visible, so coalesce several obligations into one alert plus a „Vezi" action; if obligations become routine the answer is a task surface, which is deliberately NOT in the DS yet. Five states on both components, same meaning (`neutral` · `success` · `warning` · `error` · `brand`); severity picks the STATE, the tier picks the COMPONENT, and the two are independent. Background is the family tint, text the family `-text` token, never the fill. Announcement: `error` → `role="alert"`, the rest → `role="status"`, **only on messages that appear after load**; for toast the live region is the container (`.toast-stack` = `aria-live="polite"` + `aria-atomic="false"`). Stakes (a €4,000 transfer deserving a confirmation screen rather than a toast) are a PRODUCT decision and deliberately not encoded here. **DEPRECATED in v8** — do not use in new code: `.banner` → `.alert`, `.page-banner` → `.alert.page`, `.alert.floating` → `.toast`, `.warn` → `.warning`, `.info` → `.neutral`, `.toast-close` → `.toast-dismiss`. They keep working until removal, no earlier than v10 (`VERSIONING.md`); migration table in `docs/components/banner.md`.
- **No chevron on the title without a steps sheet** — `.flow-step-title` with a chevron = "opens the steps list" (multi-step wizards only). For a simple screen, the title = plain text in `.flow-header-center`, no chevron.

---

## Key tokens

```css
--c-accent:      var(--c-pink)      /* interactive states */
--c-pink:        #EE4379
--c-pink-deep:   #C9255B
--c-white:       #FFFFFF
--c-bej-0:       #FBFAF7
--c-bej-1:       #EFEBE4
--c-danger:      #110D10            /* BACKGROUND token — destructive fill only (.btn.danger); never text */
--c-danger-hover:#2E2926            /* hover for the black destructive fill; press returns to --c-danger */
--c-dark-grad:   linear-gradient(135deg, #2F081A 0%, #09041D 100%)
                                    /* dark decorative surfaces (.sec-dark, .card.dark, .kpi.dark,
                                       .tabs.dark, .avatar.dark, .page-banner.dark, .vox-fab) —
                                       hand-tuned ramp: deep magenta → near-black violet;
                                       placeholder, under design */
--c-ink:         #110D10            /* TEXT token — never as background (same hex as --c-danger, separate role).
                                       --c-dark = DEPRECATED alias of --c-danger (frozen pages only) */
--c-fog:         #7A7570
--c-read:        #5C6BC0            /* "read / seen" status */
--c-read-bg:     #ECEFFE
--c-success:     #1F7A3A            /* positive status: fill, border, icon, dot — NOT text (4.53 on
                                       bej-1 is a non-text threshold). Green takes its hue at 138°, not
                                       extra lightness: white-on-fill ≥4.5 is capped by .badge.filled */
--c-success-text:#076E26            /* positive: THE text token — 6.43 / 6.16 / 5.41 / 5.84 */
--c-success-bg:  #EBF7EF            /* positive: tint */
--c-warn:        #B8580A            /* attention: fill, border, icon — NOT text (4.23 tint / 3.98 bej-1) */
--c-warn-text:   #A95109            /* attention: THE text token — 5.43 / 5.20 / 4.57 / 4.86 */
--c-warn-bg:     #FBF1E2            /* attention: tint */
--c-ai:          #A8336E            /* AI: fill, tint AND text — the one family with no -text token,
                                       deliberately: it passes as text everywhere (worst 5.11).
                                       Do not mint --c-ai-text for symmetry */
--c-error:       #DC2626            /* ERROR (red): fill, border, icon — NEVER text under 24px.
                                       Fails AA on its own tint (4.38:1) and on --c-bej-1 (4.06:1);
                                       bold does not move the threshold */
--c-error-text:  #CE2222            /* error: THE text token of the family — passes AA on all four DS
                                       surfaces (5.42 white / 5.19 bej-0 / 4.56 bej-1 / 4.91 tint) */
--c-error-bg:    #FDF1F1            /* error: tint bg input/banner. Text on it is --c-error-text only —
                                       --c-fog lands at 4.13:1, and .t13/.t14 carry --c-fog with them */
--c-rule-card:   rgba(17,13,16,0.12) /* card/KPI border — light separation */
--c-input-border: rgba(17,13,16,0.30) /* input/select border + small controls — contrast on bej */
--c-backdrop:    rgba(17,13,16,0.25) /* CANONICAL scrim dim behind ANY modal-type overlay — ink @ 25% */
--scrim-blur:    6px                 /* pairs with --c-backdrop — the only sanctioned backdrop-filter blur in the DS */
--bp-desktop:    1024px             /* THE single breakpoint: <1024 mobile, >=1024 desktop */
--layout-max:    1440px             /* desktop functional width = the design canvas; the grid stops there — hugging the rail when there is one, centered when there is not — and the background still bleeds to the edges */
--grid-cols:     12                 /* desktop columns — with or without sidebar */
--grid-gutter:   24px               /* gutter between columns */
--sidebar-w:     240px              /* optional sidebar rail (icon-only: --sidebar-w-icon 64px) */
--flow-max:      560px              /* .flow-chrome column cap inside the mobile band */
--r-lg:          20px               /* inputs, btn md */
--r-xl:          24px               /* cards, modal */
--r-pill:        9999px
--sh-pink:       0 6px 16px rgba(238,67,121,0.30)
--t-em-bump:     2px                 /* Fraunces accent +2px fixed (at any size) — optical compensation vs Inter, applied in .t-em and `.t60 em, .t-display em, ...` */
```

---

## Voice samples

UI copy is Romanian — these samples are canonical product copy, do not translate them:

- Hero: "Contabilitatea ta, pe pilot automat."
- Empty state: "Încă nu ai nimic aici. Adaugă primul document și începem."
- Success: "Am primit documentul. Nu mai e nimic de făcut."
- Error: "Ceva n-a mers. Încearcă din nou sau scrie-ne."
- CTA: "Începe gratuit" / "Emite factura" / "Trimite la interpretare"

---

## Update process

1. Modify the component in `bono-ds.css` or the corresponding component HTML file. **If you touch `bono-ds.css`, the `modificat:` date in the line-1 banner AND in `--ds-version` must move** (the pre-commit hook does it automatically if installed). The version NUMBER is not touched here — it changes only when a release is cut, and between releases it carries `-dev`. `scripts/check-version-stamp.py` fails CI if the two writings of the stamp disagree
2. **If the component also appears in the reference pages** (`references/referinta-landing-aligned.html`, `references/referinta-dublin-aligned.html`, `references/referinta-blog-aligned.html`, `references/referinta-mobile-aligned.html`) — update the markup there too. The `-aligned` references must stay in sync with the components.
3. Update the status in the table above
4. Update the relevant rules if behavior changed
5. When ready to sync (not necessarily after every change) — push all modified files to GitHub with `/push-prototype`
6. **A change is not delivered until it is released.** `main` is real but unreleased; consumers copy from GitHub Releases. Releases are cut manually after the Thursday review, roughly every two weeks and only when there is something to ship — see `VERSIONING.md` in the root of the `bono-ro/edge-33` repo (it carries both the rules and the procedure, and is not vendored with this skill).

### Local hook (optional)

If you want `components/index.json`, the DS:markup blocks in the HTML files and the LOOKUP / STATUS tables in `SKILL.md` to regenerate automatically before each commit, install the hook:

```bash
ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit
```

The hook runs `python3 scripts/build-catalog.py` and re-stages the derivatives. The real guarantor is CI: `.github/workflows/check-contracts.yml` runs `build-catalog.py --check` on every PR, and drift between JSON, HTML and `bono-ds.css` blocks the merge.

### Two categories of references — `-aligned` vs frozen

There are **two distinct sets**, in two separate folders to eliminate confusion:

| Category | Location | Role |
|---|---|---|
| **`-aligned` references** (live) | `references/referinta-{landing,dublin,blog,mobile}-aligned.html` | **Layout source.** Conformant with the skill + `bono-ds.css`. Start here when you want to understand a surface's rhythm. Linted by `scripts/check-aligned-lint.sh`. |
| **Original documentation** (frozen) | `doc-originale/bono-ds-{landing,dublin,blog,system}.html` + `doc-originale/bono-ds-original.css` | **Visual only, historical drift kept intentionally.** They use old aliases, old patterns. Do NOT use them as layout reference. Do NOT modify them. They render against their **own pinned copy** of the stylesheet, not the live one — so the freeze covers what you see, not just the bytes. All five hashes are verified by `scripts/check-frozen.sh`. |

**Operational rules:**
- When generating new UI → look **exclusively** at `-aligned`. The 4 frozen pages inform no decision.
- When updating the DS → propagate into `-aligned`. The frozen ones stay untouched — a DS change cannot reach them, because they do not link `bono-ds.css`.
- **The freeze covers rendering, not only markup.** A hash over HTML freezes the bytes; it says nothing about the CSS those bytes resolve against. Until `bono-ds-original.css` existed the 4 pages linked the live `bono-ds.css` of this skill, so every commit to `main` moved their pixels while the guard stayed green — a stamped copy's failure mode (values move under names that stay valid) with no stamp to read. `bono-ds-original.css` is that copy: root `bono-ds.css` at `4f70c68` (2026-05-15, "add bono-ds.css") — the handoff, the first stylesheet in the repo, byte-identical to `23c123a` from the same batch that committed the pages. `bono-ds-{landing,dublin,blog}.html` are byte-identical to their 2026-05-15 versions apart from the `❄️ FROZEN` comment and that one link, so markup and stylesheet are the same snapshot.

  The stylesheet is what makes the pages readable as documentation, not only as layout. `bono-ds-system.html` captions its focus demo "**:focus (border + halo pink)**" and states "Focus = border pink + halo pink 10%" — true of `4f70c68` (`.input:focus` → `--c-accent` + `rgba(238,67,121,0.12)`), false of every stylesheet after 2026-05-20, when `c781a72` + `57f746b` moved focus to fog precisely because pink read as the error state. Under a later stylesheet the page contradicts its own caption. Same for `--c-error`: `#B91C1C` here, before `5288edd` (2026-07-16) opened it to `#DC2626`. That is the drift the freeze exists to preserve — and it is only visible with the handoff stylesheet attached.
- The lint (`check-aligned-lint.sh`) explicitly excludes the 4 frozen pages — they cannot produce confusing alerts.
- A deliberate bump of a frozen page (rare): rewrite the corresponding hash in `scripts/frozen.manifest` and document the reason in the commit.

### Working principles

- **Language policy.** Meta-documentation — this skill, the JSON contracts, docs, rules, commit messages — is written in **English**. Product copy stays in **Romanian**: UI text, voice samples, demo markup in `components/`, `templates/` and `references/` (they are meant to be copied verbatim), and the frozen `doc-originale/` pages. All future rules are added in English.
- **Copy = single source of truth.** A piece of text exists once and propagates everywhere (like in a CMS). Mobile copy = desktop copy; when you update a text, you update it in all places.
- **Sandbox → propagate → verify.** New patterns are iterated in a separate sandbox file, then propagated to the source. After propagating, **systematically verify they applied everywhere** — do not rely on "I edited it", search for all instances.
- **Complete and honest audits.** Never exclude classes/elements from an audit. What is flagged as non-conformant gets *fixed*, not justified.
- **Rules in retrospect.** Recurring point feedback is converted into reusable rules here. Note a one-off case explicitly only when asked; otherwise run the retrospective and extract the general rule.
- **Logo: real SVG, never text/CSS square.** In nav / hero `assets/logo-full.svg`; on 64px rails (e.g. Dublin sidebar) `assets/logo-mark.svg` at minimum 24px. Never `<div class="logo">B</div>` or a CSS rule that builds the logo as a colored rectangle. The `check-aligned-lint.sh` lint blocks regressions.
- **DS components are not duplicated under another name.** If the DS has `.tabs`, you use `.tabs` — you do not invent `.da-tabs`, `.x-tabs`, `.local-tabs`. DS markup (`<div class="tabs" role="tablist">`, `<button role="tab" aria-selected="…">`, `<span class="badge sm neutral">`) is copied exactly. "Parallel class trees per component" are a silent source of drift — they violate the "do not redefine DS locally" rule even though they carry a different name.
- **Focus on form controls = neutral (fog), never brand.** Input / select / textarea / `.amount-wrap` on `:focus` / `:focus-within`: `border-color: var(--c-fog)` + `box-shadow: 0 0 0 3px rgba(122,117,112,0.14)`. Pink on focus = a violation; brand does not mark functional states. The lint (rule #5) catches regressions.
- **Italic without Fraunces = font-style: normal, not Inter italic.** When you remove a `font-family: 'Fraunces'` from a rule, also remove the `font-style: italic` that accompanied it — decorative accents (pink, small, 13px) make visual sense only with Fraunces. Without Fraunces, keep only the color + weight, no italic. Inter italic in dense UI looks cheap; we do not use it as a fallback.
- **Form markup does NOT drop the DS class.** `<input>`, `<select>`, `<textarea>` must directly carry `class="input"` / `class="select"` (textarea also uses `.input`). Never bare, with styling captured through a local descendant selector (`.da-form input { ... }`). The reason: when the DS updates (focus halo, radius, padding), markup with the direct class syncs automatically; bare markup stays on local drift. The lint (rule #6) catches regressions.
- **A local card with bej-1 + r-xl = `.card.tonal`, not a parallel class.** If you need a card with a bej-1 / dark / pink-feature background, start from `.card.tonal` / `.card.dark` / `.card.feat` in the DS, then extend with a scoped selector for layout (`.da-actions .card { padding: 20px; ... }`). Never a new class (`.da-act { background: bej-1; ... }`) that reimplements the card.
- **The currency is written `Lei` — title case, always.** Not "lei", not "LEI", not "RON". Both in financial numerics (KPI, table amounts, card totals) and in prose (`"400–600 Lei"`). Suffixes on figures use `.t-suffix-lg` (after `.tn40`) or `.t-suffix-md` (after `.tn28`). See `docs/foundation.md → Numerice financiare`. The lint (rule #7) catches regressions.
- **`<em>` in Dublin only on the T48 greeting and the T28/H2 modal action word.** Per `docs/foundation.md → Tipografie per suprafață → Dublin`: allowed in the greeting title and in the modal title on the action word. Not in card titles, section titles or list items. On headings below T40 (h3.title, h5, ticker h4) — `<em>` is not allowed with or without `.t-em`. If you need visual accent below T40, use `<strong>` or `<span class="t-actor">` (only for AI agent names), not `<em>`.
- **Form field label = `<label class="field-label">`, never bare.** DS `.field-label` (Inter 12/500/+0.04em, fog, padding-left 16px) is the standard. Ad-hoc styling (10px uppercase, +0.08em) is drift — it produces labels misaligned with the Dublin spec. The lint (rule #10) blocks `<label>` without the DS class. **Sanctioned exception:** the label inside the border — `.input-group` with a `block-start` addon holding `.field-label` — colors the label brand (`--c-accent` → `--c-accent-deep` on focus) and moves it inside the field — still using the `.field-label` class, so it is not drift. Only on client-facing surfaces (Dublin, landing); internal forms stay on the neutral `.field`.
- **Inline font-size and font-family (`style="font-size:17px"` / `style="font-family:'Inter'"`) are forbidden.** Every text size comes from the DS scale `.t60`…`.t11` + `.tn40` / `.tn28` / `.t-mono`. For variants at another size, use the corresponding class from the DS scale; do not invent `font-size: 17px` as a workaround. The lint (rules #8 + #9) blocks both cases.
- **Destructive = BLACK, error = RED — two separate concepts.** Destructive actions (deletion) are **black** (`--c-danger`) and **always carry an icon**: `.btn.danger` (black fill) for the primary deletion, `.btn.danger.outline` (ink border) for lower impact. The black button is reserved *exclusively* for destructive — do not use it as a neutral variant; cancellations stay ghost/outline/secondary. **`.btn.dark` has been retired** — black now means "destructive", so a neutral black button no longer exists. **Red (`--c-error`) never appears on buttons** — it is error messaging only: `input.error`/`select.error` (border + tint, lifts on focus-in, re-validates on blur), `.field-help.error` (text `--c-error-text`, no icon), `.banner.error` (tint + border + `--c-error-text`), `.badge.error`. **The label does NOT change color on error** (stays fog / brand). **Every semantic family is fill / bg / text, and text is always the `-text` token** — `error`, `warn`, `success`, `read`. The fill tokens are border, icon, dot and `.filled` background only; measured as text they land at or below AA (`--c-error` 4.38 on its own tint, `--c-warn` 4.23, `--c-read` 4.25, `--c-success` 4.53 on bej-1 — a non-text threshold). `.ai` is the sole exception, deliberately: `--c-ai` passes as text everywhere, so it has no `-text` token — do not mint one for symmetry. **`.badge.filled.<variant>` is always the fill token with white text**, and white-on-fill ≥4.5 is what caps how light a fill may go — it is why `--c-success` gets its green from hue (150°→138°) rather than lightness. **Brand never appears in an error state**: on the label inside the border (`.input-group.error > .block-start > .field-label`) the label leaves brand for `--c-ink` and stays neutral on focus. Lint rules #17 and #18 enforce both. **All error text is `--c-error-text`** — `--c-error` is fill/border/icon only. Measured: `--c-error` as text lands at 4.38:1 on its own tint and 4.06:1 on `--c-bej-1`, below AA at every size under 24px (bold does not move the WCAG threshold — that needs ≥18.66px *and* bold). `--c-error-text` passes on all four DS surfaces. Same split, same reason, as `--c-read` / `--c-read-text`. A block on the error tint must set its text color explicitly: `.t13` and `.t14` hard-set `--c-fog`, which is 4.13:1 on the tint and beats an inherited color. `scripts/check-contrast.mjs` measures this in a real browser. The deletion dialog: `.modal-actions.stacked` with `.btn.outline` (dismiss) above and `.btn.danger` (black + icon) below; high impact → confirmation by name with `input.error`. **No brand moments in destructive** — no `.t-em` (pink Fraunces accent) in deletion/destructive-confirmation titles. **Destructive means removal** — delete, revoke, terminate, close; irreversible-but-constructive actions (submit filing, send e-invoice, sign) are NOT destructive — they stay `.btn.primary` + confirmation dialog. **`--c-danger` and `--c-ink` are separate tokens with separate roles** — `--c-danger` is background-only (its sole use is the destructive fill), `--c-ink` is text-only; never `background: var(--c-ink)`, never `color: var(--c-danger)`, even though they share #110D10. `--c-dark` is a deprecated alias of `--c-danger` kept only for the frozen pages — never use it. Dark decorative surfaces (`.sec-dark`, `.card.dark`, `.kpi.dark`, `.tabs.dark`, `.avatar.dark`, `.page-banner.dark`, `.vox-fab`) use `--c-dark-grad` and carry no destructive meaning. Lint (rules #14 + #15) catches the regressions.
- **Text selection is NEUTRAL — `::selection` is `--c-selection` (#DDD6C9) with `--c-ink` text**, declared once for the whole document in `bono-ds.css`. Never brand it: selection belongs to the USER, not the app — pink is what the APP chooses (`.popover-item.is-active`, a selected `.check-item.card`, `.doc-item.current`), and a `cmd+A` in a search field chooses nothing. A shadcn host falls back to `selection:bg-primary` (the class ships on `<Input>`), which is how a focused search box ended up with a solid pink band and white text — when adopting, strip `selection:bg-primary` / `selection:text-primary-foreground` off the controls, because a utility class beats a document rule. Declare the pair WHOLE (background AND colour): background alone leaves `--c-fog` text at 3.16:1 on the band. See `docs/foundation.md` → Selecția de text.
- **A table's corner radius is `--r-sm` (10px) and it lives on `.tbl-wrap`, never on `.tbl`.** The table element declares no radius at all, so a bare `.tbl` dropped in the page has square corners — the wrapper is not optional, and it is also what clips thead's rectangular fill through `overflow-x: auto`. Do not round a table on the card step (`--r-md`/`--r-lg`/`--r-xl`): a table body is a grid flush to the wrapper's edge, so a wide corner cuts into the header band. Production shipped the same table at `--r-md` AND `--r-lg` off one spec — the number is `--r-sm`.
- **Two buttons joined into one object is `.btn-group.split`, never hand-rolled.** The "Export ▾" control: a labelled `.btn` plus a `.btn.icon-only`, same variant on both halves (`.outline`, `.outline-pink` or `.secondary`), at most two. Hand-rolling it ships a DOUBLE seam, because `.outline`/`.outline-pink` draw their ring as `box-shadow: inset 0 0 0 1px` — two touching buttons lay two 1px rings side by side. `.split` overlaps them onto one pixel, DRAWS the hairline for `.secondary` (which has no ring at all), and cancels the hover lift, the way `.input-group` does. An exclusive choice among several options is `.toggle-group`, not this.
- **Where React exists, the React component is the DEFAULT form — `react/select.tsx`, `react/input.tsx`.** They are copy-from source like `components/*.html`, not a package: copy the file over `components/ui/<name>.tsx` and commit it, the way the stamped `bono-ds.css` already lives in your repo. The API is shadcn's, so call sites do not change; what changes is the style layer. **They wear `bono-ds.css` classes, never utility classes** — a `<SelectItem>` written in utilities re-declares the row's padding, radius, hover and chosen state in TypeScript, which makes it a second definition of one rule and sends the next token change to HTML only. `scripts/build-catalog.py` fails on utilities in `react/`, and `scripts/check-icons.py` covers the inline SVG there too. A native `<select class="select">` is now the SECOND form — still valid, never deprecated, and still the right answer with no React on the surface, where the mobile wheel is better, or where zero JS is the requirement (the calendar's own month/year pickers). See `react/README.md`.
- **A chosen row in a menu is `.popover-item.is-selected` — a brand `.popover-check` at the end plus weight 500, and NOTHING else.** No background, no text colour: the row keeps the surface and colour of every other row. The check is at the END and the weight marks the word actually being read — different channels at different ends of the row, not two signals in one. In React the weight arrives through `[data-state="checked"]`, the attribute Radix sets. Hover does NOT retreat on it, because it has no colour left to defend. The check goes on EVERY row of the list in hand-written HTML and lights up on the chosen one — that is what reserves the slot so the menu does not change width when the selection moves. **`.is-active` is a different state and keeps its pink tint**: it is the row `aria-activedescendant` points at in a typeahead/combobox — the KEYBOARD cursor, which moves on every arrow and does not survive the list closing. The two were one class until 2026-09-21; they behave differently, so they cannot share a selector. Never mark an ACTION row chosen — "Download PDF" is a verb, it cannot be the current value of anything.
- **Every `<button>` carries a recognized DS class.** `.btn` (with the primary/secondary/ghost/outline/outline-pink/danger/danger.outline variants and lg/md/sm/xs sizing) is the default. For special functions there are `.modal-close` (the X in the modal corner), `.flow-bar-btn` (back/menu in the mobile .flow-chrome), `.switch` (toggle), `.vox-fab` (the Vox FAB), `.accordion-trigger` (the row of an `.accordion`, with `aria-expanded` + `aria-controls`), `.list-row` (the whole-row target of a phone list — `list-row.json`), `.data-card` (WIP), or `role="tab"` inside a `.tabs` container. Invented names (`.pay`, `.close`, `.submit`, `.da-btn-emit`) are drift — they reimplement the DS under another name and miss global updates. The lint (rule #11) blocks buttons without a DS class. Full-width button = `.btn.full`, not `style="width:100%"`.
- **Every modal-type overlay uses the SAME scrim token pair — never a local dim/blur.** `--c-backdrop` (ink @ 25%) + `--scrim-blur` (6px) is the canonical edge-33 scrim, per design review — it belongs on `.modal-backdrop`, `.sheet-scrim` (deprecated aliases: `.doc-sheet-backdrop`, `.flow-sheet-scrim`), and any future overlay of the same kind, always as `background: var(--c-backdrop)` + `backdrop-filter: blur(var(--scrim-blur))`. This is what "formalizing a token" fixes that a one-off note doesn't: before this pair existed, `.modal-backdrop` hardcoded the right value (ink@25%+blur6px) while the shared `--c-backdrop` token silently drifted to ink@35%, `.flow-sheet-scrim` shipped with **no blur at all**, and a Dublin reference page reimplemented the whole modal under a parallel `.du-modal-backdrop`/`.du-modal` class instead of reusing `.modal-backdrop`/`.modal` — three independent misses from the same missing token. Fix the scrim by editing the two tokens in `bono-ds.css` `:root`, never by touching a consumer rule or an artifact's local `<style>`.
