# ui_wx/AGENTS.md

`ccm_ui_wx` static library — wxWidgets adapter. The **only** target that may include `wx/...` headers. Read the root `AGENTS.md` first.

## Layer pointers

- `include/ccm/ui/AppContext.hpp` — the boundary type. A struct of references to shared core services + per-game modules and a `std::vector<IGameView*>` of all UI bundles. UI code talks to core only through this struct (and the typed pointers go through `IGameView`, never directly).
- `include/ccm/ui/IGameView.hpp` — abstract base class for per-game UI bundles. `MainFrame` only ever sees `IGameView` references; this is the seam that lets the frame swap between Magic, Pokemon, and any future TCG without knowing their card types. Optional `contentPanel` / `hostsOwnLayout` / `contentPanelIfCreated` let Digimon, Yu-Gi-Oh!, and Pokemon own a tabbed layout without changing Magic’s splitter mounting.
- `include/ccm/ui/MainFrame.hpp` + `src/MainFrame.cpp` — top-level window (default size `1210×770`), menu strip (`File` / `Game` / `Sets` / `Help`), shared toolbar (Add / Edit / Delete + filter input; hidden via `toolbarPanel_` when `hostsOwnLayout()`), and a `contentHost_` that either shows the shared splitter (Magic) or a game’s `IGameView::contentPanel` (Pokémon / Yu-Gi-Oh! / Digimon Digi-Battle notebooks). The `Game` and `Sets` menus are built dynamically from `AppContext::gameViews` so adding a new game lights up its menu entries automatically. Filter and toolbar actions forward to `activeView()`. `EVT_PREVIEW_STATUS` (preview fetch outcome → status label; empty string resets to `"Ready"`) is the only event the frame binds; `EVT_CARD_SELECTED` is bound *per view* (each `IGameView` connects its typed list panel to its typed selected panel internally). About is a custom themed dialog (not `wxAboutBox`) so dark mode behavior stays consistent.
- `include/ccm/ui/BaseCardListPanel.hpp` — header-only template `BaseCardListPanel<TCard, TSortColumn>` that owns ALL the non-game-specific `wxListCtrl` machinery: hidden zero-width spacer column (legacy of the MSW comctl32 image-list gutter workaround, kept to preserve column-index math), themed header row (clickable to sort, edge-drag to resize, divider double-click to autosize), per-icon-column cached `wxBitmap` pairs (normal + selected color) consumed by `IconListCtrl::MSWOnNotify` so row icons are pixel-perfect centered under the themed-header icons, rebuild guard so DESELECTED/SELECTED storms collapse into a single bubbled `EVT_CARD_SELECTED`, case-insensitive substring filter via `setFilter(...)`, per-column toggle-direction sort. Subclasses fill in column descriptors + per-row text + per-icon-column flag predicates + dispatch hooks (`sortBy`, `matchesFilter`).
- `include/ccm/ui/IconListCtrl.hpp` + `src/IconListCtrl.cpp` — small `wxListCtrl` subclass that intercepts `NM_CUSTOMDRAW` on Windows and paints flag-icon sub-items at the exact center of each cell. It owns a `HIMAGELIST` (built from the cached `wxBitmap` pairs via straight-RGBA 32 bpp DIB sections) and draws each cell's icon with `ImageList_Draw(ILD_TRANSPARENT)` onto the native `HDC` from `NMLVCUSTOMDRAW`. This is the same low-level pixel path `wxImageList` uses internally, which is the only rendering path that has reliably preserved SVG transparency + correct fill color across light/dark themes on MSW. Two earlier attempts — `wxGraphicsContext::DrawBitmap` and a manually-premultiplied-DIB `AlphaBlend` — both rendered runtime-fill SVG icons as solid white in light mode and were abandoned (see convention 11). The custom-draw is purely about positioning; pixel format handling is delegated to comctl32.
- `include/ccm/ui/BaseSelectedCardPanel.hpp` — header-only template `BaseSelectedCardPanel<TCard>` that owns the right-hand-side detail panel: preview image fetched via `CardPreviewService` (with the `shared_ptr<State>` + `std::atomic alive`/`currentGen` cancellation pattern), 2-column detail grid, flag-icon strip that collapses when no flags are set, image list with double-click viewer. If preview lookup fails or returns empty bytes, the panel loads a per-game **card-back fallback**: Magic and Pokémon West use fixed HTTPS URLs (`fallbackImageUrlForGame`, CCM2-aligned); **Pokémon Asia** uses the Japanese TCG back via `previewGameFor(card)` → `Game::JapanesePokemon`; **Yu-Gi-Oh!** tries Yugipedia thumbnail URL, then full `Back-EN.png` on `ms.yugipedia.com`, then reads `<exeDir>/assets/ygo_card_back.png`; **Digimon Digi-Battle** reads `<exeDir>/assets/digibattle99_card_back.png` (both bundled assets copied by `app/CMakeLists.txt` on link). The constructor caches `<exeDir>/` for that disk path. Subclasses describe the detail rows / flag icons / preview lookup `(name, setId, setNo)` and own a `Game` constant; override `previewGameFor` when preview routing differs from collection `gameId()` (Pokemon West/Asia).
- `include/ccm/ui/BaseCardEditDialog.hpp` — header-only template `BaseCardEditDialog<TCard>` that owns the standard Add/Edit form: Name, optional `appendPreSetRows` (Pokemon West/Asia region), Set picker (read-only `wxComboBox` with typeahead — prefix first, then substring, ASCII-fold so `Pokemon`/`Jungle` match `Pokémon Jungle` — and case-insensitive id matching for legacy data), Amount spin, Language and Condition choices (`languagesForChoice()` hook; Pokemon filters by region), Note, image management (Add multiple via `wxFD_MULTIPLE`, Remove, double-click to view), OK/Cancel + validation. The **Set** row is built on a host `wxPanel` with a horizontal `wxBoxSizer`; games may override `customizeSetPickerRow(row, combo)` to wrap the combo (default: combo only). After a programmatic selection, `applySetSelectionByIndex` updates `card_.set` and calls `onSetSelectionApplied()` (default no-op). After `buildAndPopulate()`, the template snapshots the loaded card into `openingSnapshot_`; in **`EditMode::Edit`**, OK asks **Yes/No** (“Save changes to this card?”) only when the card differs from that snapshot (dirty-only confirm). **Create** mode never prompts. Subclasses build the flags row (`buildFlagsRow`), append game-specific extra rows (e.g. Pokemon's `Set #`) via `appendExtraRows`, and copy values in/out of the typed card (`readExtraFromCard` / `writeExtraToCard`). The template binds `EVT_TEXT` on **Name** and invokes `onCardLookupContextChanged()` so games can drop stale keyed metadata when the user edits the lookup identity (Yu-Gi-Oh! clears its YGOPRODeck print-variant cache here). `YuGiOhCardEditDialog` overrides `customizeSetPickerRow` to add a **`SwitchCtrl`** pill switch plus a **hint** label (`Set name` / `Set code`), a text field, and **Auto detect** (resolves `Set.id` via `ccm/util/YuGiOhSetLookup.hpp` against `availableSets()`, then returns to the dropdown on success); it overrides `onSetSelectionApplied` to match manual set-change behavior. It additionally `CallAfter`s a silent `detectPrintVariants` when opening **Edit** (and after changing **Set**) so multi-print **Next** buttons can appear without pressing Auto detect first, as long as name + display set are populated. The base also exposes helpers to sync current control values and inspect the currently-selected set when a subclass needs derived-field UI.
- `include/ccm/ui/SwitchCtrl.hpp` + `src/SwitchCtrl.cpp` — custom pill-track + thumb switch for small modal rows (Yu-Gi-Oh! set picker); fires `EVT_CCM_SWITCH` on user toggle and reads colors from `inferThemeFromWindow` / `paletteForTheme`.
- `include/ccm/ui/Magic*.hpp` + `src/Magic*.cpp` — Magic implementations: `MagicCardListPanel`, `MagicSelectedCardPanel`, `MagicCardEditDialog`, `MagicGameView`. Each is ~50–100 lines of hook overrides on top of the matching base template.
- `include/ccm/ui/Pokemon*.hpp` + `src/Pokemon*.cpp` — Pokemon implementations: `PokemonCardListPanel`, `PokemonSelectedCardPanel`, `PokemonCardEditDialog`, `PokemonGameView`, `PokemonSetCompletionPanel`. Same Add/Edit shape as Magic for the card form; the game view hosts **Single Cards | Set Completion** via `contentPanel` / `hostsOwnLayout` (like Digimon/Yu-Gi-Oh!). Catalog from `PokemonSetCatalogService` (`set-catalog-west.json` / `set-catalog-asia.json`), filled on Update Pokemon. The Add/Edit/Delete + filter toolbar lives inside the Single Cards tab; MainFrame hides its shared toolbar while Pokemon is active.
- `include/ccm/ui/DigiBattle99*.hpp` + `src/DigiBattle99*.cpp` — Digimon Digi-Battle: list/selected/edit plus `DigiBattle99GameView` via `contentPanel` with a **palette-painted tab strip** + `wxSimplebook` (**Single Cards** | **Set Completion**) — not native `wxNotebook`, which stays light on MSW dark mode — and `DigiBattle99SetCompletionPanel` (pack progress tiles + greyed checklist). Catalog from `DigiBattle99SetCatalogService` (`set-catalog.json`), filled on Update Sets. The Add/Edit/Delete + filter toolbar lives **inside** the Single Cards page; MainFrame hides its shared toolbar while Digimon is active (`hostsOwnLayout`).
- `include/ccm/ui/YuGiOh*.hpp` + `src/YuGiOh*.cpp` — Yu-Gi-Oh!: list/selected/edit plus `YuGiOhGameView` notebook (**Single Cards** | **Set Completion**) via the same `hostsOwnLayout` / `contentPanel` pattern as Digimon, and `YuGiOhSetCompletionPanel`. Catalog from `YuGiOhSetCatalogService` (`yugioh/set-catalog.json`), filled on Update Sets from YGOPRODeck `cardinfo.php`.
- `include/ccm/ui/SvgIcons.hpp` + `src/SvgIcons.cpp` — embedded SVG templates with a `@FILL@` placeholder. Magic flags: `kSvgFoil` / `kSvgSigned` / `kSvgAltered`. Pokemon flags: `kSvgHolo` (sparkle, mirroring the original `IconHolo` from `PokemonTable.tsx`) and `kSvgFirstEdition` (themed "1" inside an outlined badge, rebuilt from the original `IconPokemonFirstEdition.tsx` — every fill/stroke uses `@FILL@` so the icon themes alongside the others). Toolbar glyphs: `kSvgToolbarAdd` / `kSvgToolbarEdit` / `kSvgToolbarDelete` (vscode-codicons). `svgIconBitmap` / `paddedSvgIcon` helpers backed by `wxBitmapBundle::FromSVG`. Bitmaps from `svgIconBitmap` go straight to `wxStaticBitmap` / `wxBitmapButton::SetBitmap` cleanly; for the row-icon path `IconListCtrl` packs them into a private premultiplied-BGRA `HIMAGELIST` and draws with `ImageList_Draw`. See convention 11 for the full pitfall write-up.
- `src/BaseEvents.cpp` — single-translation-unit definitions for `EVT_CARD_SELECTED` and `EVT_PREVIEW_STATUS`. Both events are template-instantiation-agnostic so all per-game panels share the same event types.
- `include/ccm/ui/SettingsDialog.hpp` + `src/SettingsDialog.cpp` — edits `Configuration` via `ConfigService::store`.
- `include/ccm/ui/ImageViewerDialog.hpp` + `src/ImageViewerDialog.cpp` — full-size viewer with prev/next.
- `include/ccm/ui/Theme.hpp` + `src/Theme.cpp` — shared theme helpers and popup helpers (`showThemedMessageDialog`, `showThemedConfirmDialog`) for consistent dark/light dialogs. `applyThemeToWindowTree` paints `wxButton`, `wxBitmapButton`, and **`wxToggleButton`** in dark mode (custom `wxEVT_PAINT` + hover/focus) so native Win32 theming cannot flash a light hover plate; light mode leaves buttons native where possible. `SwitchCtrl` is palette-driven and self-painted (not native `wxToggleButton`).

## Conventions

1. **Only consume core through `AppContext`.** Do not include any header from `ccm/infra/` here. The set of allowed `ccm/...` includes is `domain/`, `services/`, `games/IGameModule.hpp`, `ports/ICardPreviewSource.hpp`, and `util/` headers that remain UI-agnostic (for example `util/Result.hpp`, `util/YuGiOhPrintingSlot.hpp`, `util/YuGiOhSetLookup.hpp`). Do not pull arbitrary `util/` or `games/` implementation headers beyond what a panel/dialog already needs for display or small shared helpers.
2. **Image decoding lives here, not in core.** Use `wxImage::LoadFile(path.string())` against the path returned by `IImageStore::resolvePath`. Core stays free of any image library.
3. **Ownership**: dialogs and panels are heap-allocated and parented to a `wxWindow`. wxWidgets owns the lifetime — do **not** wrap them in `unique_ptr`. `IGameView` instances themselves are owned by `app/main.cpp` (`std::unique_ptr<>`); the panels owned by the views become children of the `MainFrame` splitter on first mount.
4. **Custom events**: `EVT_CARD_SELECTED` is fired by the list panel on itself (not its parent). Each `IGameView` binds it on its typed list panel inside the panel's first construction so the typed selection flows directly into the typed selected panel — `MainFrame` never sees a `MagicCard` or a `PokemonCard`. Do not move that binding back into `MainFrame`.
5. **wxFont modifications** mutate in place: `font.MakeBold().MakeLarger()` — do not call `Scale` (it does not exist on wxFont 3.2; use `MakeLarger` / `SetPointSize`).
6. **Single-active-game UX.** `MainFrame` only ever shows one game's panels at a time; the content host swaps either the shared `listPanel()` / `selectedPanel()` splitter or a game’s `contentPanel()` when the user picks a different `Game` menu entry. Do not stand up parallel side-by-side tabs for different games. Digimon’s, Yu-Gi-Oh!’s, and Pokémon’s Single Cards / Set Completion switch is an in-game mode switch (themed tab strip + `wxSimplebook`), not multi-game tabs.
7. **No `ccm_warnings`.** This target intentionally does **not** link the strict warning interface — wxWidgets headers trip `-Wpedantic` / `-Wshadow`. Keep it that way; do not add the link.
8. **Async background work** must not capture `this` raw. Use the pattern from `BaseSelectedCardPanel`: a `std::shared_ptr<State>` holding `std::atomic<bool> alive`, `std::atomic<unsigned> currentGen`, and a back-pointer to the panel; spawn a detached `std::thread`, then deliver the result with `wxTheApp->CallAfter([state, gen, ...]() { if (!state->alive) return; if (state->currentGen != gen) return; ... })`. Flip `alive=false` in the panel destructor so late callbacks become no-ops.
9. **Icons come from `SvgIcons.hpp`.** Don't inline new SVG strings in panel sources; add them to `SvgIcons.{hpp,cpp}` so all panels stay in sync. Always pass a runtime fill color (`wxSystemSettings::GetColour(...).GetAsString(wxC2S_HTML_SYNTAX)`); never bake one into the SVG.
10. **Sort key != display key.** When you add a new column to a list panel, follow the existing pattern: the `wxListCtrl` cell text is one thing; the *sort* comparator lives in `ccm::services::CardSorter` and may key off a different field (the canonical case is `set.name` shown but `set.releaseDate` sorted, so collections list chronologically). New columns must extend `MagicSortColumn` / `PokemonSortColumn` and add a corresponding `case` in `sortMagicCards` / `sortPokemonCards`.
11. **`wxListCtrl` + flag-icon centering (MSW comctl32):**
    - Native `LVS_REPORT` sub-item image rendering on MSW left-anchors the bitmap with a small built-in inset, regardless of `wxLIST_FORMAT_CENTER`. It can never align pixel-perfect with our wx-sizer-centered themed header icons, especially after column resize. Don't try to compensate by padding the image-list bitmap or nudging it horizontally — that path was tried and abandoned.
    - **Authoritative path:** flag-icon sub-items go through `IconListCtrl::MSWOnNotify` (`NM_CUSTOMDRAW`). It computes the live sub-item rect via `LVM_GETSUBITEMRECT(LVIR_BOUNDS)` and composites the cell's icon at the rect center with `AlphaBlend(... AC_SRC_OVER | AC_SRC_ALPHA)` straight onto `cd->nmcd.hdc`. Each (icon, selection-state) pair has its own pre-built premultiplied 32 bpp BGRA DIB section in `dibBitmaps_`; index `i` holds the normal variant and index `i + iconColCount` holds the selected variant. The cache rebuilds whenever the theme changes (via `setIconBitmaps(...)` from `BaseCardListPanel::rebuildIconBitmaps`).
    - We deliberately do **not** route through `ImageList_Draw` / `HIMAGELIST` here. On the verified MinGW-w64 + comctl32 v6 stack, `ImageList_Draw` on an `ILC_COLOR32` list with `ILD_TRANSPARENT` ignored the alpha channel of the bitmap and the "transparent" canvas around each glyph painted as opaque black behind the icon — every row flag rendered as a black rectangle with a white glyph regardless of theme. `AlphaBlend` directly on the listctrl's HDC works in every case we've tested.
    - **Bitmap format pitfall — `AlphaBlend` requires PREMULTIPLIED BGRA**, not straight alpha. With straight RGBA the function returns `FALSE` (or, depending on the driver, paints garbage). `makePremultipliedDib` in `IconListCtrl.cpp` does the per-pixel premultiply with the rounded form `(c * a + 127) / 255`. **Do not** simplify that to `c * a / 255` (loss of precision on `c=0xFF, a=0xFF`) and **do not** skip the divide-by-255 entirely (`c * a` overflows the byte and renders the icon as solid white — that was the original failure mode that made an earlier dev abandon premultiplication for a while). Hardcoded-fill SVGs (e.g. baked-in black/white badges) happen to look correct on every path and are **not** a useful sanity check on their own — always verify rendering against a runtime-fill icon (foil / signed / altered / holo) on both light and dark themes.
    - The hidden zero-width spacer column at index 0 stays. It's no longer load-bearing for any image-list gutter, but it keeps every other column index stable across the codebase. Start real columns at index 1.
    - Insert each row through the spacer column with a `wxListItem` whose mask includes `wxLIST_MASK_IMAGE` and image `-1` so MSW doesn't try to render an item icon for column 0 if a public image list ever gets attached again.
    - `AlphaBlend` lives in `msimg32.lib`; `ui_wx/CMakeLists.txt` links `msimg32` on `WIN32`. Don't rely on `gdi32` being enough — `AlphaBlend@44` is **not** in `gdi32`.
12. **Startup/dialog responsiveness rules:**
    - Keep first paint fast: avoid heavy synchronous work in window/dialog constructors.
    - In `MainFrame`, defer initial collection load with `CallAfter(...)` so the frame paints before I/O/parsing.
    - Keep startup's "first row selected" behavior, but schedule initial selection with `CallAfter(...)` in `BaseCardListPanel` to avoid blocking first render.
    - Avoid reloading/reparsing sets on each Add/Edit open: each `IGameView` caches its own set list and passes it into the dialog by pointer.
    - Pass preloaded sets into `BaseCardEditDialog` by pointer/reference (not by value) to avoid vector copies per open.
    - For heavy dialog setup, wrap constructor-time UI population in `Freeze()` / `Thaw()` and append choice items in bulk via `wxArrayString` (`BaseCardEditDialog::buildAndPopulate` does this).
13. **String encoding on Windows (avoid mojibake):**
    - Domain/service strings are UTF-8 `std::string`. Do not rely on implicit `std::string <-> wxString` conversions on Windows; those can route through the active ANSI codepage and render `Pokémon` as `PokÃ©mon`.
    - UI display path (`std::string` -> wx control): always convert with `wxString::FromUTF8(str.c_str())` before `SetLabelText`, `SetItem`, `Append`, control constructors, etc.
    - UI write-back path (wx control -> `std::string`): always convert with `ToStdString(wxConvUTF8)` so persisted/domain text stays UTF-8.
    - Apply this rule consistently in shared templates (`BaseCardListPanel`, `BaseSelectedCardPanel`, `BaseCardEditDialog`) because a single implicit conversion in those bases affects every game view.
14. **Theme consistency rules (Windows):**
    - Treat dialog roots as `panelBg`, not a separate shade, otherwise label rows can look like mismatched darker boxes.
    - Theme dialogs before `ShowModal()` with `applyThemeToWindowTree(...)` (and root background/foreground colors as needed); this includes Settings, image viewer, About, and custom popup dialogs. Per-game **Add/Edit** flows use `themeModalDialog(wxDialog*, Theme)` from `Theme.hpp` so `MagicGameView` / `PokemonGameView` / `YuGiOhGameView` share one path instead of duplicating palette wiring.
    - Do not use native `wxMessageBox` / `wxAboutBox` for app-facing flows that must match dark mode. Use themed popup helpers (or a custom themed `wxDialog`) so body/buttons stay in sync with the app palette.
    - Center popup dialogs on the app window (`CentreOnParent()`) so confirmations/info boxes open relative to the current app window.
    - Include `wxSpinCtrl` in themed input controls (Amount field) or it will keep a mismatched native background.
    - Do not call `applyNativeClassTheme(..., "DarkMode_Explorer", "Explorer")` for `wxTextCtrl`; on some Windows builds this causes black typed text in dark mode. Keep text inputs palette-driven (`SetThemeEnabled(false)` in dark/high-contrast as needed).
    - If a specific text field still renders wrong while typing (notably `MainFrame`'s filter box), enforce text/background in `MainFrame::MSWWindowProc` via `WM_CTLCOLOREDIT` for that control handle.
    - Keep toolbar button behavior stable under dark/high-contrast: avoid changes that break click/tooltip affordances while experimenting with hover contrast fixes.
    - For dark/high-contrast button readability, do not trust native hover/pressed rendering on Windows; custom state painting in `Theme.cpp` is allowed when native visuals ignore configured colors.
    - Button event handlers must use per-button state that is refreshed when theme changes. Avoid one-time captures of theme colors/mode in lambdas; these can leak dark-mode behavior into light mode.
    - In High Contrast, use stronger hover/pressed deltas than regular dark mode and keep the button border in the foreground/text color for visibility (currently yellow in this palette).
    - When validating UI theming changes, rebuild and run `ccm` (the executable), not just `ccm_ui_wx`.
15. **Preview fallback behavior (CCM2 parity where applicable):**
    - Keep unresolved external previews user-visible by showing a per-game card-back image in `BaseSelectedCardPanel` instead of a blank/transparent bitmap.
    - Magic / Pokémon use single fixed HTTPS URLs (`Magic_card_back.jpg`, Bulbagarden `Cardback.jpg`). Japanese Pokémon uses the Japanese TCG Bulbagarden back (`TCG_Card_Back_Japanese.jpg`). Yu-Gi-Oh! uses Yugipedia-hosted backs plus a **bundled** PNG beside the exe (`assets/ygo_card_back.png`) when the network path fails. Digimon Digi-Battle uses a **bundled** PNG (`assets/digibattle99_card_back.png`) — keep those chains working when touching preview code.
    - If you change fallback sourcing (URLs or bundled asset), keep the "always show a reasonable card-back fallback" behavior intact for **every** game with remote previews.
16. **Per-game auto-detect controls:**
    - Auto-detect actions in edit dialogs (e.g. detect set print number / rarity from API) are opt-in per game.
    - Keep shared templates game-agnostic: put buttons and detection behavior in `<Name>CardEditDialog`, not in `BaseCardEditDialog`. Yu-Gi-Oh!'s **Set code** entry (`SwitchCtrl` + text + **Auto detect** against cached sets) is wired through the template hook `customizeSetPickerRow` so Magic/Pokemon keep the default single-combo row unchanged.
    - For games that use composed print IDs (prefix + numeric suffix), allow user editing on the numeric portion and render the full code as a read-only derived label beside the input.

## Required follow-ups

- If you replace **`ui_wx/assets/ygo_card_back.png`** or **`ui_wx/assets/digibattle99_card_back.png`**, rebuild the **`ccm`** target so `app/CMakeLists.txt`'s `POST_BUILD` copy refreshes `<exeDir>/assets/`; do not remove an asset without updating `BaseSelectedCardPanel` / `docs/assets-and-info-apis.md`.
- After adding a new dialog/panel `.cpp` you **must** add it to `ui_wx/CMakeLists.txt`.
- After adding a new menu action you **must** allocate an `Ids::*` value in `MainFrame.hpp` (don't reuse `wxID_HIGHEST` math inline) and `Bind` it in `buildMenuBar`. The dynamic Game / Sets menus consume the `IdGameMenuBase` / `IdSetsMenuBase` ranges; do not stomp on those id ranges.
- After changing `AppContext` you **must** update `app/main.cpp` so the composition root populates the new field.
- After adding a new icon to `SvgIcons.{hpp,cpp}` you **must** keep the `@FILL@` placeholder so both light- and dark-variant rendering keeps working, and add a small unit-test-equivalent visual check by running the binary (no automated UI tests in this repo).
- After changing one of the `Base*` template hooks (or adding a new one) you **must** keep `docs/adding-a-new-game.md` in sync — the per-game derived classes are the readers of that contract and the doc is what onboarding agents read first.

## Adding a new game UI

1. Implement three derived classes under `include/ccm/ui/` mirroring the Magic / Pokemon trio:
    - `<Name>CardListPanel : public BaseCardListPanel<<Name>Card, <Name>SortColumn>` — override `declareTextColumns()`, `declareIconColumns()`, `renderTextCell()`, `isIconColumnSet()`, `sortBy()`, `matchesFilter()`.
    - `<Name>SelectedCardPanel : public BaseSelectedCardPanel<<Name>Card>` — override `declareDetailRows()`, `declareFlagIcons()`, `detailValueFor()`, `isFlagSet()`, `previewKey()`, `gameId()`. Define a local `enum` of `DetailKey` constants for clarity.
    - `<Name>CardEditDialog : public BaseCardEditDialog<<Name>Card>` — override `buildFlagsRow()`, optionally `appendExtraRows()`, `readExtraFromCard()`, `writeExtraToCard()`, `updateMenuName()`.
2. Add a `<Name>GameView : public IGameView` that owns those panels and the typed `CollectionService<<Name>Card>&`. Bind `EVT_CARD_SELECTED` on the list panel inside `listPanel(parent)` to push the typed selection into the selected panel. The `MagicGameView` / `PokemonGameView` pair is the canonical reference.
3. Re-add the new view to `AppContext::gameViews` in the composition root (`app/main.cpp`). The `Game` and `Sets` menus pick it up automatically.
4. Add SVG glyphs for any new flag columns to `SvgIcons.{hpp,cpp}` (with the `@FILL@` placeholder).
5. Register all new `.cpp` files in `ui_wx/CMakeLists.txt`.

## Commands

Build UI only: `cmake --build build --target ccm_ui_wx`
