---
name: gear-panel-generator
description: Build a custom front-panel renderer for a piece of gear (amp, pedal, compressor, fuzz, preamp, power amp…) in Capture Finder from a front-on product photo and its captures' control list. Use when the user sends a gear image (and optionally controls) and wants a custom panel (definition in data/custom-amps.json, a Preact panel component + CSS in src/ui/panels/<name>/, artboard, verification, report).
---

# gear-panel-generator — new custom gear panel for Capture Finder

Turns **(a)** a front-on photo and **(b)** the gear's control list (from its captures, plus anything the user adds) into a custom panel that matches the existing custom panels (Preact components in `src/ui/panels/<name>/`) in style and behaviour.

**Never modify** the approved panels, the generic panel (`src/ui/panels/generic`), the app shell or the similarity engine (`src/domain/similarity.ts`) unless the user asks. If new gear needs engine changes (like the `shred` special case in `switchValue`), stop and say so first.

Read first: `references/conventions.md` (repo facts), `references/design-rules.md`, `references/templates.md`. Finish with `references/checklist.md`.

## Ground rules from the user (override older assumptions)
- The gear list in `data/` is the source of truth. Every capture type is gear: amps, pedals, compressors, fuzz, overdrive… Each entry has a `category` (from its captures' Cortex device type: Amps, Compressors, Fuzz, Overdrive, Pedals). Custom panels are defined in `data/custom-amps.json` (the file and the `AMP_DEFS`/`ampId` names are historical; they hold all gear). The repo has real captures only. **No DEMO captures, ever** (the browser test asserts no "DEMO" text). Test matching against real captures of that gear.
- Real captures decide values: read `data/captures.json` for the values actually used (options, spellings, `N/A`) before fixing options and ranges. Photo and manual decide layout and labels.
- Single-channel gear = `channels:null`, all controls `scope:'global'`, channel `null` (ids end in `-`, e.g. `k-bass-`), no channel tabs. Only amps show the "Single channel" pill.
- Generic gear with odd description wording gets a rule in `data/gear-identities.json`, never code: an identity rule (`{id, brand, model, match}`) and/or `valueAliases` for one value written several ways (`{id, valueAliases: {"<label>": {"<spelling>": "<value>"}}}`). Custom definitions can carry `valueAliases` too, or use option `aliases`.
- The Twin/5150 control lists are deferred; don't touch them.

## Workflow
1. **Collect inputs; ask only for what's missing.** Photo (front-on; if unclear ask for a better one or a crop — never guess), exact identity (brand, model, variant; look-alikes it must not be confused with), control list with ranges if the user has one, optional manual link / starting settings / which controls count for matching.
2. **Study the photo.** Left→right: each control, label (above/below), knob style (smooth cap vs fluted vs chicken-head), scale marks/numbers, indicators, where jacks and switches sit, panel colours. Cross-check with the manual and with the capture data. Record every conflict (photo / manual / user list / captures) — real panel wins, flag it.
3. **Decide the crop.** Mark what the panel keeps and what it cuts (see *Shape* below): keep the gear's shape and arrangement, cut only what isn't a setting.
4. **Mine the captures.** `npm run capture-values -- <gearId>` lists the values/option spellings, `N/A` counts and uninterpreted lines. Use these for `options`, ranges and the defaults note.
5. **Plan.** Control list → definition draft (weights, one `primary`, aliases, `match`) + layout plan (flex wrap, or absolute coordinates in fixed-size sections). Show the plan to the user if anything is ambiguous.
6. **Build.**
   - Add the definition to `data/custom-amps.json` — reuse the generated `id` from `public/data/gear.json`, keep its `category`, and add `match`, `aliases` and `parse` rules as the descriptions need (see README › Data) — then run `npm run build:data` and commit `public/data/` with it. Never edit `data/captures-raw.json` or the generated JSON (`gear.json`, `captures.json`, `mapping-report.json`) by hand, and never add gear-specific code to `tools/build-data/`: if a new kind of description structure needs a rule, add a generic rule type to `applyParseRules` (`tools/build-data/settings.ts`, with a unit test in `tests/unit/tools/`) and use it from the data.
   - Add the panel folder `src/ui/panels/<name>/` (component + CSS) following `templates.md`, register it in `src/ui/panels/registry.tsx`, and import its CSS in `src/styles/index.css` (before `generic/generic.css`). Panels are pure views over `{def, as, compact}`: build every interactive element from the primitives (`src/ui/primitives`), which handle keys, drag, clicks and double-click. `index.html` and the app shell never need editing for a panel.
   - Create the artboard `artboards/<Name>Mock.dc.html` with `npm run artboard -- <gearId> <Name>` — it renders the app's own panel component into a standalone, interactive page, so the two never drift. Screenshot it and fix the look before checking it in the app. Never hand-edit or overwrite other people's files in `artboards/`.
   - Tests: bump the custom-panel count in `tests/unit/matching.test.ts` and `tests/legacy/test_app.cjs` (`AMP_DEFS.filter(a=>a.panel!=="generic").length`), add the gear to the "starting capture scores 100" list in `tests/unit/matching.test.ts`, and add a few assertions for its parsing (mode/channel, repeated labels). `tests/component/panels.test.tsx` checks every panel draws and exposes every matched control automatically.
7. **Verify.** `npm run verify:gear -- <gearId>`, `npm test` (vitest), `npm run test:e2e`, `npm run test:legacy:new` and `npm run test:visual` (run `npm install` once; they use the installed Chrome). Also load every capture of the gear and check it scores 100 (a capture that doesn't state a weighted control can't — report those). Fix until clean.
8. **Report:** what was traced from the photo and what was cropped away; every photo/manual/list/capture disagreement; open questions; what changed.

## Shape: keep the gear, crop what isn't a setting
The panel is the gear's own face with the parts that aren't settings cut away — not a re-layout. Anyone who knows the gear should recognise it at a glance.
- **Keep the shape and arrangement.** Same orientation and proportions as the photo, controls in their real positions and relative sizes, labels where the gear prints them (above or below each control), the name where the gear shows it. Don't reflow controls into a row, move the name into a side column, or pad empty space to fill the workbench.
- **Crop only what isn't a setting.** Remove empty enclosure or chassis, footswitches, status LEDs, jacks, power and battery areas, screws, vents, logos and artwork — unless a capture records it (a recorded footswitch or LED state becomes a control). Pedals and other small boxes are mostly enclosure, so usually only their control area plus a thin margin of paint remains; amp faceplates and rack faces usually stay nearly whole.
- **Shorten, don't relocate.** If an element has to stay for recognition (the name, a badge strip), keep it in its place but compact: one short line, or two small ones, on a simple flat version of its backing if the gear has one.
- **Name as plain text** — never the logo, script lettering or artwork.
- **Colours and control styles from the photo:** paint or faceplate colour as the fill, real knob shapes, colours and printed scales, label colour; recolour focus rings on light paint.
- **No cabinet or grille unless the gear has one.** Use `.cab` with a flat override class (as `.pedalcab` does) for anything that isn't a tolex amp — a thin dark frame at most.
- **Narrow screens:** fixed-size internal coordinates are fine; parts may wrap under each other at 390px, but keep their order. A fixed-size face wider than about 290px must still fit at 320px: scale the whole face with `zoom` in the `max-width:380px` media query instead of reflowing it (pointer positions and dragging keep working).
- The generic fallback follows this too: non-amp categories render their generic controls in a `.pedalcab` without a grille.
- The generic fallback (`src/ui/panels/generic`) lays controls out by heuristics only, never per gear: amps are wide, so it runs horizontally: the name on one line on top, channel sections side by side as far as they fit, and inside each section the blocks left to right (switches up to three rows tall → knobs → EQ sliders). Knobs follow signal-flow order (gain stages, tone stack bass → treble, presence-type controls, others, output levels last) on one row when it fits, else two rows read column by column; switches come in the order of the knob their name relates to (shared word, or a usual pairing such as Bright → Treble, Deep → Bass), unrelated ones first. Blocks only wrap under each other when even that doesn't fit (phones). Improve the heuristics there for every generic gear at once; anything that needs one gear's layout is a custom panel.

## Patterns learned
- **Data first.** If the gear's captures already exist as a generic entry in `public/data/gear.json`, reuse that exact `id` in `custom-amps.json` so captures stay mapped (`model` can be corrected) **and always add a `match` rule** on the described source so reworded descriptions in a future HAR still map. Value spellings go in option `aliases`, never in code.
- **One choice written as several rows** (e.g. one row per mode group, all but one `N/A`): model the choice as `channels` (with `aliases`) and use `"parse": {"channelFromRows": [...]}` so the one non-N/A row becomes the channel. Controls that only apply to some of those modes get `channels:[…]` while staying `scope:'global'`.
- **Two units in one capture** (e.g. preamp + power amp) can repeat labels. Use `"parse": {"sections": [{"startsAt": "<first row of the second unit>", "rename": {"<label>": "<new label>"}}]}`; otherwise the parser drops both as "Conflicting … values".
- **A second unit behind a device header** (e.g. a "Power amp: Brand® Model®" row after the preamp settings): the parser stops at a ® header by default; add `"parse": {"readOn": ["Power amp"], "sections": [{"startsAt": "Power amp", "rename": {...}}]}` so it keeps reading and renames the repeated labels. Reuse the unit's existing panel (e.g. `Simul290` in `src/ui/panels/shared.tsx`) stacked under the new one.
- **Never-stated enable switches.** If no capture states an on/off that gates other controls, use `"parse": {"assumeWhenMissing": {"<key>": true}}` only when the user agrees the default is safe; the details drawer marks the value "(assumed)".
- **Not every control is a knob.** Trace the real input (displays with up/down keys, selectors, LED matrices). Step keys are plain `data-act="pick"` buttons whose `data-v` is the value one step away (`snap(c, v ± c.step)`), so no new event code is needed; a display can carry `data-drag="knob"` and the hidden `k-<key>-` range input.
- **Stepped knobs.** When captures only ever use a few fixed values (a detented selector), model the control as a `switch` with numeric option values (`{"v": 8, "label": "8:1"}`) so it matches by exact position, and draw it as a knob that turns to the selected detent: the printed position numbers are the `pick` buttons, the knob face is a `cycle` button, and the selected number doubles as the amber readout. Ask the user when it's unclear whether the real control is continuous.
- **Unrecorded controls that hold the shape.** If the gear has a control no capture records and leaving it out would leave a visible hole in the layout, ask the user; if they agree, show it with weight 0 (marked ⊘, not matched, never changed by Load) and say so in the defaults note.
- **Printed vs moving parts.** Draw printed scales, rings and numbers outside the rotating group; only the knob (skirt, cap, pointer) rotates. When the numbers are on the knob's own skirt and read against a fixed index line on the panel, they rotate with the knob (number v printed at −v·step° so it turns to the line) and the index line is drawn on the panel.
- **Controls that only work with a switch on** (a lead drive that only acts when its pull is engaged): give them `requires` on that switch and dim them, still adjustable, while it's off, with the reason in their aria-labels, like dimming other channels' controls.
- **Free-form layouts** (staggered rows, triangles, mixed knob sizes): place controls absolutely inside a fixed-size block, with labels and readouts positioned next to each control as on the gear (readout under the label, or under the knob when the label is above).
- **On-panel selectors instead of tabs.** When the gear has its own mode or channel selector, hide the stage tabs for that panel and make the selector buttons `data-act="channel"`, with a text readout of the current selection so colour isn't the only signal.
- **Several units together** (stacked rack units, head + power amp): draw each unit's face and stack them in signal order.
- **One piece of hardware, several gear entries** (e.g. two plugins built around the same pedal): write one panel function and give each entry its own definition, `match` rule, category and defaults, all pointing at the same `panel` id. When a capture has other units' sections before the one you match (a compressor before the pedal), check that parsing starts at the right section header and report captures whose description lacks it.
- **Shared toggles** (`Toggle`, `Lever`) work in any panel; switch keys must not normalise to the same name as a knob (e.g. label a mid-frequency toggle `LO MIDS SWITCH` when there is a `LO MIDS` knob) and the panel can still print the short name.
- **Same label, different controls.** Descriptions can reuse a label for different controls (a 0–10 "Gain" knob and a Lo/Hi "Gain" switch in one capture) or repeat it for twin controls ("Pull Bright" twice). The generic build already splits these; in a custom definition give each its own key and label, keep the shared spelling in `aliases`, and parsing sends numbers to the knob, text to the switch whose options contain it, and repeated lines to the next unused control. Check `capture-values` for values of mixed types under one key, and per-channel option sets.
- **Recorded rear-panel controls.** When captures record a control that sits on the back (e.g. a mode or variac switch), bring it to the front: put it where a cropped non-setting part was (standby switch, jewel light, input) and mark it with a small "REAR PANEL" caption, styled like the other front controls.
- **Physical knobs per channel.** When channels have their own knobs (VOL. 1/2/3, GAIN 1/2/3) or share some (one tone stack for channels 2 and 3), model every physical knob as its own `scope:'global'` control limited to its channels with `channels:[…]`, all sharing the description's label in `aliases` ("Volume", "Treble"). Parsing picks the control for the capture's channel, the panel draws each knob once, and each channel gets exactly one `primary` (its gain).
- **Dim other channels' controls.** When one faceplate holds every channel's controls together (separate VOL./GAIN knobs per channel, per-channel switches, a tone stack shared by some channels), dim every control whose `channels` doesn't include the selected channel — knob, label/plate, readout, toggle and its position labels — with a single class (e.g. `opacity:.3`), keep them adjustable, and add ", not used on channel N" to their aria-labels so the state isn't only visual. Controls without `channels` (global ones) stay at full strength. Use `available(c, as.channel)` to decide.
- **When generic gear becomes custom,** update tests that used it as their "generic" example.
- **Shared-helper touches** (keep them minimal and report them): `channelLabel` wording (`src/domain/channels.ts`), tab visibility (`StageHead.tsx`), picker subtitles (`GearPicker.tsx`).
- **Reference code:** each existing custom panel is a folder in `src/ui/panels/` (component + CSS) plus a line in `registry.tsx`; read the closest one before writing a new panel.
- **Screenshots:** headless Chrome `--screenshot` can't go below ~500px wide; check 390px with Playwright (`npm run verify:gear`), not with screenshots. Playwright comes from `npm install` (driving the installed Chrome; no browser download).

## Pedals (chains)
- "Pedal N: Brand® Model®" blocks after the settings are the capture's chain (`capture.chain`, built by `pedalChain`): don't model a pedal's knobs as amp controls. A pedal is drawn with its own panel (custom if it has one, else generic) to the left of the gear — or to its right, in the FX LOOP group, when its block ends with "In efx loop" (`loop: true`) — in a compact size (`compact` prop); a custom pedal panel with fixed coordinates scales its knobs and case (text and switches keep their size; see `BbPreampPanel`/`Ts9Panel`). Its control keys and ids get the prefix `p0:`, `p1:`… from the pedal's control scope; panels need no changes for that. When a pedal's name is written several ways, add an identity rule (`gear-identities.json`) or a `match` on its custom definition. Pedals with no captures of their own get a generic `chainOnly` definition.

## Starting settings
- The build sets every gear's starting settings (Reset, double-click) from its captures: per channel the most downloaded capture, gaps from the next most downloaded; the start channel is the one of the most downloaded capture; the (i) notes list them (`defaultsFrom`). Still write complete `defaults` in a definition (they are the fallback for values no capture states), but don't hand-pick a starting capture or write "Starting settings: …" in the `defaultsNote`.

## Weights and matching
- Exactly one `primary:true` control per channel (main gain/drive/volume; for a compressor the compression amount, e.g. Input/Comp); gear with a gain knob per channel marks each, limited to its channel. Every control gets a deliberate weight (0 = shown, not matched). Typical: primary 2–3, tone knobs 1, switches that change voicing 0.8–2, output/level ~0.5–1, secondary pulls ~1.
- Controls that depend on an enable switch use `requires` + `group`; unknown enablement never implies ON unless the definition says so with `assumeWhenMissing`.
- Aliases: every wording found in captures/manual (Mid/Middle/Mids, Volume 1/I, Master/Master Volume, Pre-amp/Preamp Volume, Comp/Compression).
