# Plan: Feather S3 signer interactive UI (splash, main menu, generate, view, enter, working)

Status: ready to implement.
Related firmware: [`firmware/feather_s3_tft/main/main.c`](../firmware/feather_s3_tft/main/main.c:1), [`firmware/feather_s3_tft/main/display.c`](../firmware/feather_s3_tft/main/display.c:1), [`firmware/feather_s3_tft/main/display.h`](../firmware/feather_s3_tft/main/display.h:1), [`firmware/feather_s3_tft/main/mnemonic.c`](../firmware/feather_s3_tft/main/mnemonic.c:1), [`firmware/feather_s3_tft/main/mnemonic_wordlist.h`](../firmware/feather_s3_tft/main/mnemonic_wordlist.h:1), [`firmware/feather_s3_tft/main/key_derivation.c`](../firmware/feather_s3_tft/main/key_derivation.c:1).
Companion: persistence will be added in a follow-up plan (option 3 from design discussion: PIN-derived encryption of NVS-stored seed).

## 0. Color palette (global)

Strict 4-color palette for ALL screens (no other colors used):

| Color | Constant | Use |
|---|---|---|
| Black | `RGB565_BLACK` | Background everywhere |
| White | `RGB565_WHITE` | Default text / active labels |
| Red   | `RGB565_RED`   | Selection / focus / important call-outs (e.g., `N_SIGNER` title) |
| Dim gray | `RGB565_DIM_GRAY` (`0x4208`) | Disabled / unselectable items, hints, footers |

No yellow, cyan, green, magenta, blue. Existing screens (`show_idle_screen`, `show_approval_prompt_with_caller`) get retro-fitted to this palette in §5.4. The `RGB565_*` constants for unused colors stay defined in [`display.h`](../firmware/feather_s3_tft/main/display.h:14) (harmless) but the new code never references them.

Selection convention:
- **Selected** menu item: red text on black background (no inverse-video fill needed in the 4-color scheme; red is bright enough as the focus color). A leading `> ` prefix is also drawn for redundancy.
- **Non-selected** items: white on black.
- **Disabled** items (e.g., `View` when no mnemonic is loaded): dim gray on black.
- **Footers / hint text**: dim gray on black.

## 1. Goal

Replace the current "generate-random-mnemonic-on-boot, immediately go to working screen" proof of concept with a navigable on-device UI that lets the operator:

1. See a splash screen at power-on.
2. Choose at the main menu between **Generate** a fresh mnemonic, **Enter** an existing mnemonic, or **View** the currently loaded mnemonic.
3. After Generate or successful Enter, transition into the existing **Working Page** (sign / approve flow that already exists today).
4. Persistence is **out of scope** for this plan — every boot starts at the splash, mnemonic lives only in RAM. A follow-up plan will add PIN-protected NVS storage.

## 2. Hardware / display constraints

From the existing firmware ([`display.c`](../firmware/feather_s3_tft/main/display.c:38)):

- Panel: 240×135, ST7789, landscape.
- Font cell at `CHAR_SCALE = 2`: `CHAR_W = 12`, `CHAR_H = 16`.
- Usable text grid: **20 columns × 8 rows** (with ~0px margin) or **19 columns × 6 rows** with the comfortable insets the user noted. Plan assumes 19×6 so we have ≥4px breathing room on every edge.
- 3 buttons (current physical mapping in [`main.c`](../firmware/feather_s3_tft/main/main.c:31)):
  - `D0` = GPIO0, active **LOW** (boot button, internal pull-up)
  - `D1` = GPIO1, active **HIGH** (external pull-down)
  - `D2` = GPIO2, active **HIGH** (external pull-down)

### 2.1 New menu-button semantics

| Button | Role in new UI | Existing role (working page) |
|---|---|---|
| `D2` | **Up** (scroll up) | "always allow" approval |
| `D1` | **Down** (scroll down) | "approve once" |
| `D0` | **Enter / select** | "deny" |

The working-page button semantics intentionally **stay the same** as today (D0=deny, D1=approve once, D2=always). Only menu screens reinterpret them as Up/Down/Enter. The button-handling code is split into two layers (see §5.2) so the working-page approval flow does not have to change behavior.

## 3. Screen flow

```mermaid
flowchart TD
    A[Power on] --> B[Splash: N_SIGNER]
    B -- D0 or D1 or D2 --> C[Main Menu]
    C -- Generate selected --> D[Generate page: random 12 words]
    D -- auto --> E[View page]
    C -- Enter selected --> F[Enter page: pick letter]
    F -- letter chosen --> G[Pick word starting with letter]
    G -- word chosen --> H[Phrase preview: continue or delete]
    H -- continue and len 12 --> I[Save prompt]
    H -- continue and len lt 12 --> F
    H -- delete --> H2[Drop last word]
    H2 --> H
    I -- save --> W[Working page]
    I -- back --> C
    C -- View selected --> V[View page read-only]
    V -- any button --> W
    E -- any button --> W
    W -. menu hotkey not in scope .-> C
```

Notes:
- From the **Working page** there is currently no "back to menu" hotkey. That is **out of scope** for this plan; once entered, the working page is the run-mode terminus until reboot. Adding a long-press-to-menu is left as a tracked TODO at the end of this doc.
- The View page is reachable from two places: post-Generate (auto), and from the main menu (read-only). Both behave the same — "any button returns to caller". Caller is tracked so that View-from-Generate goes to Working, and View-from-Menu returns to Menu.

## 4. Per-screen specification

All screens use `display_clear(RGB565_BLACK)` as the base, then draw text with the existing 12×16 cell. Coordinates below are pixel offsets from top-left.

### 4.1 Splash screen

- Centered horizontally and vertically: the literal string `N_SIGNER`, color `RGB565_RED`, on black background.
- 8 chars × 12px = 96px wide → x = (240−96)/2 = 72.
- 1 row × 16px → y = (135−16)/2 ≈ 60.
- Below the title (y = 60 + 24 = 84), a hint line `"press any button"` in `RGB565_DIM_GRAY`, centered (small font).
- Wait for **any** of the three buttons (debounced, see §5.1). The "drain held buttons first, then read fresh edge" pattern from [`wait_for_approval()`](../firmware/feather_s3_tft/main/main.c:122) is reused.
- **No timeout** — strictly button-only advance, per user direction.

### 4.2 Main menu

- Title row at y=4: `"MAIN MENU"` in `RGB565_WHITE` (centered).
- Three menu items at y=28, y=52, y=76:
  - `Generate`
  - `Enter`
  - `View`
- The selected item is rendered as red text on black with a leading `> ` prefix. Non-selected items are white on black with no prefix (a 2-char gap so positions don't shift). Disabled items are dim gray.
- Footer at y=120: `"D2 up   D1 dn   D0 ok"` in `RGB565_DIM_GRAY` (small font).
- Behavior:
  - `D2` (Up): selection moves up, wrapping from Generate → View at the top edge.
  - `D1` (Down): selection moves down, wrapping from View → Generate at the bottom edge.
  - `D0` (Enter): commit the selected item.
- **View** is drawn in `RGB565_DIM_GRAY` and **not selectable** when no mnemonic is loaded. The cursor skips it on Up/Down navigation in that state.

### 4.3 Generate page

- Renders the title `"GENERATING..."` centered in `RGB565_WHITE` while [`generate_mnemonic_12()`](../firmware/feather_s3_tft/main/mnemonic.h:6) is called.
- On success: store the resulting mnemonic in `s_mnemonic` (already exists in [`main.c`](../firmware/feather_s3_tft/main/main.c:63)), then transition to the View page with `caller=GENERATE`.
- On failure: show `"GEN FAILED"` in `RGB565_RED` for ~2 seconds, then return to main menu.
- This page has no button input loop of its own — it's a transient pass-through.

### 4.4 View page

- Title row at y=2: `"YOUR MNEMONIC"` in `RGB565_WHITE` (small font), centered. Only shown if there is enough vertical room — see layout below.
- 12 words laid out as **3 columns × 4 rows**.
  - Column widths: each column gets ⌊240/3⌋ = 80px.
  - Each cell shows `"NN word"` where `NN` is 1-based index zero-padded to 2 digits and `word` is the BIP-39 word.
  - Longest BIP-39 word is 8 chars, plus `"NN "` prefix = 11 chars total. 11 × 12 = 132px — that does NOT fit in an 80px column.
  - **Resolution:** drop the index prefix from the cell and rely on row-major order (top-left = #1, top-right = #3, bottom-left = #10, bottom-right = #12). Give a tiny index legend in the footer ("L→R, top→bottom") instead. 8 chars × 12 = 96px still > 80px column.
  - **Real resolution:** drop `CHAR_SCALE` to 1 (6×8 cells) for the View page only. That gives 40 cols × 16 rows. 12 words in 3×4 grid then becomes trivial: each cell is up to 13 chars (`"NN. word    "`) at 6px = 78px, fits in the 80px column with margin. We add a `display_draw_text_small()` variant (see §6.1) that uses `CHAR_SCALE=1`.
  - Final layout (small font):
    - Title at y=2, height 8.
    - Grid starts at y=14. Row pitch = 24px (8px cell + 16px gap for readability). Last row at y=14+3·24 = 86.
    - Footer at y=110: `"any key = back"` in `RGB565_DIM_GRAY`.
  - Cell text format: `"%2d. %s"` (e.g., `" 1. abandon"`), color `RGB565_WHITE` on black.
- Behavior: any button (debounced) returns to the caller (`GENERATE` → working page; `MENU` → main menu).
- The working-page button semantics are *not* active here.

### 4.5 Enter page (multi-stage)

This page is the most complex. It has three sub-states implemented in a sub-state machine:

#### 4.5.1 Stage A — Letter picker

- Top line: `"WORD %d/12"` (e.g., `"WORD 3/12"`) in `RGB565_WHITE`.
- Second line: `"PICK FIRST LETTER"` in `RGB565_DIM_GRAY` (small font).
- A 26-letter grid of the alphabet:
  - With small font (6×8): a row of 26 chars = 156px, fits in one row. But we want a visible cursor on the selected letter, so use 2 rows of 13 letters each (`A..M` / `N..Z`) at large font (12×16 with extra spacing) — 13 × 14px = 182px wide, fits.
  - At y=40 row 1 (`A B C D E F G H I J K L M`).
  - At y=64 row 2 (`N O P Q R S T U V W X Y Z`).
  - Non-selected letters: `RGB565_WHITE` on black. Selected letter: `RGB565_RED` on black (no fill — pure color cue).
- Footer at y=120: `"D2 up   D1 dn   D0 ok"` in `RGB565_DIM_GRAY` (small font).
- Behavior:
  - `D2`: move cursor up — A↔N, B↔O, …, M↔Z (wrap top-to-bottom within column).
  - `D1`: move cursor right (and wrap onto next row); from `Z` wrap to `A`.
  - **Note:** with only 3 buttons we cannot have separate Left/Right/Up/Down. The Up/Down semantics here mean "move within the alphabet" — `D2` = previous letter, `D1` = next letter, with wrap. The visual 2-row layout is purely cosmetic (so the operator can see the whole alphabet at a glance); navigation is **linear**, not 2D. The selected letter is highlighted wherever it lands in the 2-row display.
  - `D0`: commit selected letter, move to Stage B.
- A **back** mechanic is needed too. Convention: holding `D0` for >1 second cancels Stage A and returns to the main menu (only meaningful if no words have been entered yet); otherwise it returns to Stage C of the previous word. See §4.5.4.

#### 4.5.2 Stage B — Word picker

- Top line: `"WORD %d/12 [%c]"` showing the chosen letter (e.g., `"WORD 3/12 [P]"`) in `RGB565_WHITE`.
- Below: the list of all BIP-39 words starting with that letter, scrollable. Word counts per letter range from 1 (e.g., `x`) up to ~150 (most common letters). Use small font (6×8).
- Layout with small font: 16 visible word rows at row pitch 8+1=9px starting at y=14, last row at y=14+15·9=149 — overflows. Cap visible rows at **12**, row pitch 9px, y=14..14+11·9=113.
  - 12 rows × 1 column is fine; longest word ≤ 8 chars × 6px = 48px, leaves plenty of room. We can also do **2 columns of 12** if needed for letters like `s` (~250 words won't happen — BIP-39 max per letter is around 91 for `s`). 1 column × 12 visible with scroll keeps it simple.
- Cursor: highlighted row drawn in `RGB565_RED` on black (with a leading `> ` prefix for redundancy). Non-cursor rows in `RGB565_WHITE` on black.
- Footer at y=120: `"D2 up   D1 dn   D0 ok"` in `RGB565_DIM_GRAY` (small font).
- Behavior:
  - `D2`: prev word (with scroll up at top of viewport, wrap from first to last word in that letter group).
  - `D1`: next word (with scroll down at bottom of viewport, wrap last → first).
  - `D0`: commit selected word, append to the in-progress mnemonic buffer, move to Stage C.

The full BIP-39 wordlist from [`mnemonic_wordlist.h`](../firmware/feather_s3_tft/main/mnemonic_wordlist.h:1) is already linked in. We add a small helper that produces `(start_index, count)` pairs for each letter on first use and caches them in a static array of 26 `(uint16_t start, uint16_t count)`. See §6.2.

#### 4.5.3 Stage C — Phrase preview / continue / delete

- Top line: `"WORDS SO FAR (%d/12)"` in `RGB565_WHITE`.
- Body: the entered words rendered in a 3-column grid using small font, same as the View page (§4.4). Empty slots are blank. The most recently added word is drawn in `RGB565_RED` (visual confirmation of the just-committed entry); previously confirmed words in `RGB565_WHITE`.
- Footer (two lines) at y=104, y=120, small font:
  - y=104: `"D2 continue   D1 delete"` in `RGB565_DIM_GRAY`.
  - y=120: `"D0 save"` in `RGB565_WHITE` when `count == 12`, in `RGB565_DIM_GRAY` otherwise.
- Behavior:
  - `D2` (continue): if `count < 12`, go back to Stage A for word `count+1`. If `count == 12`, no-op.
  - `D1` (delete): drop the last word from the buffer, redraw. If `count` was 1, drop to 0. If `count` becomes 0, this stays on Stage C with an empty grid; pressing continue again restarts at word 1.
  - `D0` (save): only valid if `count == 12`. Otherwise no-op. On save, validate the full mnemonic via the BIP-39 checksum check ([`mnemonic_validate()`](../firmware/feather_s3_tft/main/mnemonic.h:1), see §6.3). On valid: copy buffer into `s_mnemonic`, transition to working page. On invalid: show `"BAD CHECKSUM"` in `RGB565_RED` for 2s and return to Stage C with all 12 words still present (so user can delete and try again).

#### 4.5.4 Cancel / back from Enter

- Long-press `D0` (>1 second) at any stage of Enter cancels and returns to **main menu** without saving; the in-progress buffer is wiped via `secure_memzero`-equivalent (we will add `secure_memzero` for the firmware — see §6.4).
- Short-press `D0` is the normal "select / save" semantic.

### 4.6 Working page (existing, retro-fitted to 4-color palette)

- This is the existing approval-loop screen. Behavior change in this plan: we **arrive** here from the menu/save flow rather than from boot directly. Button semantics remain unchanged (D0=deny, D1=once, D2=always).
- Existing [`show_idle_screen()`](../firmware/feather_s3_tft/main/main.c:201) and [`show_approval_prompt_with_caller()`](../firmware/feather_s3_tft/main/main.c:163) currently use yellow/cyan/green/magenta. They are **retro-fitted** to the 4-color palette:
  - All informational labels → `RGB565_WHITE`.
  - The "APPROVE sign_event?" header → `RGB565_RED` (it's a focus/attention call-out).
  - All hints/footers ("D1=allow once" etc.) → `RGB565_DIM_GRAY`.
  - Status counters at the bottom of the running screen → `RGB565_DIM_GRAY`.
- The existing key derivation (mnemonic → seed → privkey → pubkey → npub) is moved out of `app_main()` into a function `apply_mnemonic_and_enter_working()` that is called from both `Generate→View→Working` and `Enter→Save→Working` paths. See §5.4.

## 5. Code structure changes

### 5.1 Button input layer

Add a small input module: [`firmware/feather_s3_tft/main/buttons.h`](../firmware/feather_s3_tft/main/buttons.h:1), [`firmware/feather_s3_tft/main/buttons.c`](../firmware/feather_s3_tft/main/buttons.c:1).

Public API:
```c
typedef enum {
    BTN_NONE = 0,
    BTN_D0,    /* aka ENTER in menu mode, DENY in working mode */
    BTN_D1,    /* aka DOWN in menu mode, APPROVE_ONCE in working mode */
    BTN_D2,    /* aka UP   in menu mode, APPROVE_ALWAYS in working mode */
} btn_id_t;

typedef enum {
    BTN_EVT_NONE = 0,
    BTN_EVT_PRESS,         /* short press (release before LONG_MS) */
    BTN_EVT_LONG_PRESS,    /* held for >= LONG_MS, fires on release */
} btn_event_kind_t;

typedef struct {
    btn_id_t id;
    btn_event_kind_t kind;
} btn_event_t;

void buttons_init(void);
/* Block until a press/long-press event arrives, or timeout_ms expires.
 * Returns BTN_EVT_NONE on timeout. timeout_ms == 0xFFFFFFFFu = wait forever. */
btn_event_t buttons_wait(uint32_t timeout_ms);
/* Non-blocking: returns BTN_EVT_NONE if no event ready. */
btn_event_t buttons_poll(void);
```

The existing `wait_for_approval()` in [`main.c`](../firmware/feather_s3_tft/main/main.c:122) is rewritten as a thin wrapper over `buttons_wait()` so the existing approval semantics still work without code duplication.

Constants:
- `BTN_DEBOUNCE_MS = 30` (existing)
- `BTN_LONG_MS = 1000`
- `BTN_POLL_MS = 10`

### 5.2 UI state machine

Add [`firmware/feather_s3_tft/main/ui.h`](../firmware/feather_s3_tft/main/ui.h:1), [`firmware/feather_s3_tft/main/ui.c`](../firmware/feather_s3_tft/main/ui.c:1).

```c
typedef enum {
    UI_SPLASH,
    UI_MAIN_MENU,
    UI_GENERATE,
    UI_VIEW,
    UI_ENTER_LETTER,
    UI_ENTER_WORD,
    UI_ENTER_PREVIEW,
    UI_WORKING,         /* delegates to existing main loop */
} ui_screen_t;

typedef enum { VIEW_FROM_GENERATE, VIEW_FROM_MENU } view_caller_t;

typedef struct {
    char words[12][12];     /* room for 8-char word + nul, padded for safety */
    uint8_t count;          /* 0..12 */
    int8_t letter_idx;      /* 0..25, current cursor in Stage A */
    uint16_t word_cursor;   /* cursor index inside current letter group */
    uint16_t word_scroll;   /* top of viewport in word list */
    view_caller_t view_caller;
} ui_state_t;

/* Run the UI until a mnemonic is loaded and the user requests transition to working. */
void ui_run_until_working(char out_mnemonic[256]);
```

`ui_run_until_working()` is called from `app_main()` after display init and before the main USB/sign loop. It returns when the user has either:
- generated a mnemonic and dismissed the View screen, or
- entered a valid 12-word mnemonic and pressed Save.

The mnemonic is written into `out_mnemonic`. Caller is responsible for zeroing the local UI buffer (this is done inside `ui.c` before return, but main.c also keeps its own copy ephemeral).

Each screen is a function `ui_screen_splash()`, `ui_screen_main_menu()`, etc. Each one returns the next `ui_screen_t` enum, plus mutates `ui_state_t` as needed. The dispatcher is a simple `while (current != UI_WORKING) { current = handlers[current](&state); }`.

### 5.3 Display additions

Add to [`display.h`](../firmware/feather_s3_tft/main/display.h:1):

```c
/* CHAR_SCALE=1 variant (6x8 cells, 40 cols x 16 rows). */
esp_err_t display_draw_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg);

/* Convenience: centered text using the regular (12x16) font. */
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg);
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg);

/* Inverse-video helper used by selected menu items.
 * Draws a filled rect of the bg color sized to the text, then text on top. */
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg);
```

Implementation notes:
- The existing [`display_draw_text()`](../firmware/feather_s3_tft/main/display.c:204) (line approximate, locate via search) hardcodes `CHAR_SCALE = 2`. Refactor it so the scale is parameterized in an internal helper, then `display_draw_text` and `display_draw_text_small` are thin wrappers.
- Centered helpers compute `x = (DISPLAY_WIDTH - char_w * strlen) / 2` and call the underlying drawer.

Add a dim color constant for grayed-out menu items:
```c
#define RGB565_DIM_GRAY 0x4208   /* ~25% white */
```

### 5.4 main.c refactor

Sketch of the new `app_main()`:

```c
void app_main(void) {
    display_init();
    buttons_init();
    /* draw splash, main menu, etc. */
    ui_run_until_working(s_mnemonic);
    /* now s_mnemonic holds 12 words, derive keys */
    if (apply_mnemonic_and_enter_working(s_mnemonic) != 0) {
        /* show error, halt; future plan adds "back to menu" */
    }
    /* zero s_mnemonic immediately after key derivation, same as today */
    secure_memzero(s_mnemonic, sizeof(s_mnemonic));
    transport_init();
    /* existing USB/sign loop */
    run_signing_loop();
}
```

Where `apply_mnemonic_and_enter_working()` does what lines 800–836 of [`main.c`](../firmware/feather_s3_tft/main/main.c:800) currently do (mnemonic→seed→privkey→pubkey→npub→idle screen draw), refactored into one function.

The big `while (1)` USB loop body (currently lines 852–1004) is moved into `run_signing_loop()` for readability. No behavior change.

The existing global statics (`s_pubkey_hex`, `s_privkey`, `s_npub`, `s_npub_short`, `s_mnemonic_short`, `s_frame_buf`, `s_response_buf`, `s_signed_event_buf`) keep their current scope.

## 6. Helpers / library additions

### 6.1 Small font drawing

Already covered in §5.3. The existing `font5x7.h` is 5×7 inside a 6×8 cell, so `display_draw_text_small()` literally just drops the `CHAR_SCALE` multiplier. The existing scale-2 path is preserved.

### 6.2 BIP-39 letter index

Add to [`mnemonic.h`](../firmware/feather_s3_tft/main/mnemonic.h:1):

```c
/* Returns the number of BIP-39 words starting with `letter` (a-z, case insensitive),
 * and writes the start index in the global wordlist into *out_start. */
size_t mnemonic_words_for_letter(char letter, size_t *out_start);

/* Returns pointer to the i-th BIP-39 word (0..2047) in the global list. */
const char *mnemonic_word_at(size_t index);
```

Implementation: iterate the wordlist once on first call, cache 26 `(start, count)` pairs in a `static` array.

### 6.3 Mnemonic checksum validation

The current [`mnemonic.c`](../firmware/feather_s3_tft/main/mnemonic.c:1) provides `generate_mnemonic_12` and `mnemonic_to_seed`. For Enter mode we need to validate user-typed input independently of seed derivation (so we can show a checksum error before committing keys).

Add to [`mnemonic.h`](../firmware/feather_s3_tft/main/mnemonic.h:1):

```c
/* Validate a space-separated 12-word mnemonic against BIP-39 checksum.
 * Returns 0 on valid, negative on word-not-in-list / wrong-count / bad-checksum. */
int mnemonic_validate(const char *mnemonic);
```

If `mnemonic_to_seed()` already returns nonzero on bad checksum, `mnemonic_validate()` can be a thin wrapper that runs the checksum half but skips PBKDF2. Confirm this during implementation by reading [`mnemonic.c`](../firmware/feather_s3_tft/main/mnemonic.c:1) — if the existing function only does PBKDF2 and trusts the input, we add the proper BIP-39 checksum check (entropy bytes from word indices + last 4 bits = SHA256(entropy)[0] >> 4).

### 6.4 Secure memzero

Add a tiny `secure_memzero()` to a new [`firmware/feather_s3_tft/main/secure_mem.h`](../firmware/feather_s3_tft/main/secure_mem.h:1) using `volatile` writes, modeled on the existing [`src/secure_mem.c`](../src/secure_mem.c:1). Not strictly new functionality (we already do `memset(buf,0,sizeof)` in many places) but the in-progress mnemonic buffer in `ui_state_t` and word stage buffers should use it for clarity.

## 7. CMakeLists updates

[`firmware/feather_s3_tft/main/CMakeLists.txt`](../firmware/feather_s3_tft/main/CMakeLists.txt:1) adds the new sources:

- `buttons.c`
- `ui.c`
- `secure_mem.c`

`SRCS` list grows by these three files. No new component dependencies.

## 8. Out of scope (tracked TODOs)

These are intentionally deferred and listed here so they don't get lost:

1. **Persistence (PIN-protected NVS storage of the seed).** Will be a separate plan, [`plans/feather_signer_persistence.md`](feather_signer_persistence.md:1) — to be authored after this UI plan lands. It will reuse the same letter/digit scroll UI for PIN entry.
2. **"Back to menu" hotkey from the working page.** Today, once you arrive at working, you're stuck until reboot. A long-press gesture (e.g., D0+D2 simultaneously, or D0 long-press during idle screen only) could re-enter the menu and lock the loaded mnemonic.
3. **Lock screen / re-auth.** Without PIN, the device is fully unlocked once a mnemonic is entered. The persistence plan will add a "lock" hotkey that wipes RAM keys and returns to the splash/PIN-entry screen.
4. **Display blanking / screensaver.** ST7789 doesn't burn-in dramatically but reducing the backlight after idle would be polite.
5. **Test plan additions.** The current firmware has no automated tests on-device; we rely on host-side tests in [`tests/`](../tests/). UI logic in `ui.c` should be written so that the screen-transition function `ui_next_screen(state, event)` is **pure** (no display/button calls inside) and can be unit-tested with a host-side harness. This is a soft target — recommend doing it but not blocking on it.

## 9. Acceptance criteria

- [ ] Power-on shows the red `N_SIGNER` splash; pressing any of D0/D1/D2 advances to the main menu.
- [ ] Main menu lists `Generate`, `Enter`, `View`. `View` is dimmed and unselectable when no mnemonic is loaded.
- [ ] In the main menu, D2 moves selection up with wrap, D1 moves down with wrap, D0 commits. Selected row is shown in inverse video.
- [ ] Selecting `Generate` produces a fresh random 12-word BIP-39 mnemonic, displays it on the View page in a 3×4 grid, and any button press transitions into the working page (key derivation + USB/sign loop).
- [ ] Selecting `View` (after a mnemonic is loaded) shows the same View page, and any button press returns to main menu.
- [ ] Selecting `Enter`:
  - Stage A presents the alphabet with a movable highlight; D2/D1 navigate, D0 picks.
  - Stage B presents all BIP-39 words for the chosen letter, scrollable; D2/D1 navigate, D0 picks.
  - Stage C shows the running phrase preview as a 3×4 grid; D2 continues to next word, D1 deletes last word, D0 saves (only when 12 words are present).
  - Long-press D0 cancels and returns to main menu, wiping the partial buffer.
  - Save with valid checksum transitions to working page; save with invalid checksum shows `"BAD CHECKSUM"` for 2s and stays on Stage C.
- [ ] Working-page button semantics (D0=deny, D1=once, D2=always) remain identical to today.
- [ ] No mnemonic persistence: a reboot starts at the splash with no loaded mnemonic.
- [ ] All in-progress buffers (`ui_state_t.words[][]`, the working `s_mnemonic`) are wiped via `secure_memzero` on transition out of the screens that use them.

## 10. Mermaid: button semantics by screen

```mermaid
flowchart LR
    subgraph Splash
      Sp[D0 D1 D2 advance]
    end
    subgraph MainMenu
      M2[D2 up] --- M1[D1 down] --- M0[D0 enter]
    end
    subgraph EnterLetter
      EL2[D2 prev letter] --- EL1[D1 next letter] --- EL0[D0 select / long=cancel]
    end
    subgraph EnterWord
      EW2[D2 prev word] --- EW1[D1 next word] --- EW0[D0 select / long=cancel]
    end
    subgraph EnterPreview
      EP2[D2 continue] --- EP1[D1 delete last] --- EP0[D0 save / long=cancel]
    end
    subgraph View
      V[any button = back]
    end
    subgraph Working
      W2[D2 always allow] --- W1[D1 approve once] --- W0[D0 deny]
    end
```
