# Exceptions tab — mockup fidelity + control primitive repair

- **Status:** DONE
- **Owner request (2026-08-14):** "This is the original mockup [Exceptions settings screen] and this is what it looks like after agents did it. I understand it's not 100% compatible to our primitives, but this is the way I want it to look. Change the primitives that are required to achieve this. Retire the inline boxes for our existing input primitive and make the dropdown boxes look like the mockup, and make it look like the mockup overall — find the gaps."
- **Worktree:** `.worktrees/exceptions-mockup` (branch `wt/exceptions-mockup`, from `origin/master` @ 55d0b70d7)

## Outcome

The Exceptions tab of Translation Settings renders as the supplied mockup, and the shared admin control primitives are corrected so every dropdown in the plugin renders a proper chevron affordance.

## Gap analysis (mockup vs shipped)

| # | Gap | Fix |
|---|-----|-----|
| 1 | Selects render as bare boxes with no chevron | Shared control block sets `background:` shorthand, wiping any `background-image`; no `appearance:none`. Set `background-color`, add `appearance:none` + chevron data-URI on `select`/`.ipz-control--select`, with an RTL position. |
| 2 | Page-local `input,select,textarea` block in `_exceptions.scss` overrides the shared control system (zero-specificity `:where()` loses to it) — 36px/legacy borders here, 38px/tokens everywhere else | Retire the page-local bare-element styling and the page-local `labelledInput`/`labelledSelect`; compose `InputControl`/`SelectControl`/`SearchControl`/`TextareaControl` from `FormControls.js`. |
| 3 | Stacked bold labels above every control | Mockup is label-less: placeholder carries the meaning. Bare controls + `aria-label` (never drop the accessible name). |
| 4 | Scope list is a stack of full-width buttons, selected one a solid blue bar | Card titled "Exception Scope" with description; Global tile (icon, name, "All languages", count); "Per Language" group heading; per-language tiles with flag + native name + count pill; dashed "+ Add Language". |
| 5 | No scope chip, no info notice in the detail header | `Badge` chip beside the heading + `Notice({variant:'info'})`. |
| 6 | Add form is a flat row of labelled fields | Bordered "Add Exception" panel: input + matching-type select + primary "Add Exception", with the Exact/Contains help lines beneath. |
| 7 | List toolbar controls are stacked-label fields on their own row | "Exceptions List (N)" panel header: title + subtitle left, search / type filter / Import-Export menu right. |
| 8 | Matching-type cell uses a page-local badge class | Shared `Badge` (success = Exact, info = Contains). Retire `ipz-exceptions__badge`. |
| 9 | Row actions are text "Edit"/"Delete" buttons | Icon-only pencil/trash with `aria-label`; new shared `.ipz-icon-btn`. |
| 10 | Pagination is text Previous/Next; rows-per-page sits in the top controls row | Footer: "Showing X to Y of N entries" left, numbered pager `‹ [n] ›` + rows-per-page select right. New shared `Pagination` primitive. |
| 11 | Import/Export are two loose buttons | New shared `Menu` dropdown primitive (`aria-haspopup`, `aria-expanded`, `role=menu`, Escape, click-outside, focus return, arrow keys). |
| 12 | "How matching works" columns have no divider/rhythm | 3-column card with rule between columns. |

## Deliberate deviations

- The mockup shows two dropdowns ("More Actions" top-right and "Import / Export" in the list toolbar). There are only two real actions (import, export). Shipping both menus would duplicate them, so one menu ships, labelled "Import / Export", in the mockup's toolbar position. No invented menu items.
- "+ Add Language" has no backing create-language API on this screen; it navigates to the existing Languages route (`#/languages`).

## Acceptance criteria

- Chevron visible on every admin select; no double chevron on Settings/Translations/Languages.
- No bare-element control styling left in `_exceptions.scss`.
- Every control keeps an accessible name; icon-only buttons have `aria-label`.
- Loading, empty, no-match, editing, importing, unknown-scope and pending states all still render sanely.
- `tests/e2e/scoped-translation-exceptions.spec.js` updated for the new labels and passes.
- `admin/dist` rebuilt and committed.

## Receipt

- Commit `bfdc8f00d` on `wt/exceptions-mockup`.
- Focused E2E `scoped-translation-exceptions.spec.js`: 2 discovered / 2 executed / 0 skipped, Chromium and Firefox both pass on debian1 through `e2e-remote`.
- Regression sweep (snappy visual contract, settings, settings-visual, languages, UI parity) run on both this branch and a clean `origin/master` worktree: 70 tests, an identical set of 30 pre-existing failures on both arms, so zero regressions from this change.
- Rendered screenshot reviewed against the mockup at the populated Global scope.

Blocked/unresolved: the 30 pre-existing E2E failures on those other suites are untouched by this work and remain open.

## Also fixed along the way

The remote build transport never shipped `package-owner.sh`, which `remote-runner.sh` sources from its own directory. Buildboxes kept a months-old copy without the `composer.lock` branch, so every E2E dispatch from a PHP-rooted worktree failed closed with "no package lock owns cwd". Fixed in overdeck (`8c6ebc6de`, landed and deployed) with a transport regression test.

## Next action

Land on `master`, deploy per project rule.
