diff --git a/web/plans/260426-2343-tan-tan-3-section-board-and-settings/phase-01-three-section-player-board.md b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/phase-01-three-section-player-board.md
new file mode 100644
index 0000000..910fa6e
--- /dev/null
+++ b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/phase-01-three-section-player-board.md
@@ -0,0 +1,156 @@
+# Phase 1 — 3-Section Visual Split for PlayerBoard
+
+## Context
+
+- Plan: [plan.md](plan.md)
+- Reference image: physical Minh Tân lô tô card (3 stacked 3×9 mini-cards on
+ one sheet, brown empty cells, decorative cross-hatch separators).
+- Touches: `src/lib/PlayerBoard.svelte`, `src/app.css`.
+
+## Overview
+
+- **Priority**: P1 (visual identity / authenticity)
+- **Status**: TODO
+- **Effort**: ~30 min
+- **Description**: Render the existing 9×9 grid as 3 visually distinct 3-row
+ sections. Data, generator, click handlers, win detection: unchanged.
+
+## Key Insights
+
+- The 9×9 → 3×(3×9) split is **purely visual**. Don't touch
+ `src/lib/game-logic.js`. Don't change the grid shape, the storage shape,
+ or `isRowComplete` — they all keep operating on a 9×9 array.
+- Tân Tân section labels (top to bottom): **Minh Tân** / **Loại đặc biệt**
+ / **Tấn tài tấn lộc**. Use these verbatim.
+- The decorative cross-hatch border (✚✚✚✚) on the physical card is
+ ornamental. CSS approximation: a thin orange/red dotted or repeating
+ cross-pattern border between sections is enough — don't pixel-copy.
+- Card serial number badge (e.g., "25" on the physical card) is **out of
+ scope** per plan.
+
+## Requirements
+
+### Functional
+- Player card renders as 3 stacked sub-grids: rows 0–2, rows 3–5, rows 6–8.
+- A label sits between each pair of sub-grids (and above the first):
+ `Minh Tân`, `Loại đặc biệt`, `Tấn tài tấn lộc`.
+- All click / cross / waiting / Kinh behavior keeps working unchanged.
+- Cell crossed style, row-complete style, the `Chờ N` toast, and the Kinh
+ modal all behave exactly as before.
+
+### Non-functional
+- No layout shift or visual jitter.
+- Mobile responsive (stays within `max-w-2xl` container).
+- Dark mode parity: separator and labels visible on both themes.
+- A11y: each sub-grid keeps `aria-label="Bảng lô tô"` (or section-specific
+ label like `Bảng lô tô — phần 1`).
+
+## Architecture
+
+### Render strategy
+
+Replace the single `
` with three sibling
+`
` blocks, each iterating only its 3 rows. Use
+`{#each [0, 3, 6] as startRow, sectionIdx}` and within each section iterate
+`grid.slice(startRow, startRow + 3)`.
+
+The `crossed` lookup must use the **absolute row index** (`startRow + r`),
+not the slice-relative index. Same for click handlers and `rowCompleteness`.
+
+### Section labels
+
+Plain centered text (`text-center text-xs uppercase tracking-widest text-slate-500`)
+between sections, sandwiched by horizontal cross-hatch lines. Use a
+repeating linear-gradient or a `.section-divider` utility in `app.css`.
+
+### Border continuity
+
+Currently each cell has `border-r border-b`. Within a section this is fine.
+Across sections we **don't** want the bottom row of section 1 to look
+adjacent to the top row of section 2 — the separator label / divider sits
+between them, so the visual gap naturally breaks the grid.
+
+The outer wrapper (`rounded-2xl overflow-hidden shadow-xl border …`)
+currently wraps the whole grid. Two choices:
+- **A**: keep one outer wrapper, separators are inside it.
+- **B**: each section has its own rounded card.
+
+**Pick A** — one outer wrapper, internal separators. Closer to the
+physical sheet which is one continuous piece of paper.
+
+## Related Code Files
+
+### Modify
+- `src/lib/PlayerBoard.svelte`
+ - Replace the single `{#each grid.flat() …}` loop with 3 sectioned loops
+ (lines ~157–191 in current state).
+ - Each section iterates `[0, 3, 6][i] .. + 3` and uses absolute row idx
+ when reading `crossed[row]`, calling `handleCellClick(row, col)`,
+ reading `rowCompleteness[row]`.
+ - Add the section header text between sections.
+- `src/app.css`
+ - Add `.section-divider` utility (cross-hatch / dotted horizontal rule).
+ - Optionally add `.section-label` if Tailwind utility classes don't
+ cover the styling cleanly.
+
+### Don't touch
+- `src/lib/game-logic.js`
+- `src/routes/master/+page.svelte` (master grid stays 11×9)
+- `src/routes/+page.svelte`
+
+## Implementation Steps
+
+1. In `PlayerBoard.svelte`, extract a `SECTIONS = [0, 3, 6]` constant and a
+ parallel `SECTION_LABELS = ['Minh Tân', 'Loại đặc biệt', 'Tấn tài tấn lộc']`.
+2. Wrap the existing grid block with a flex column. Inside, loop sections:
+ for each `startRow`, render the label, then a `loto-grid` containing
+ that section's 3 rows × 9 cols.
+3. Inside the inner loop, compute `row = startRow + r`. Pass `row` to
+ `handleCellClick`, look up `crossed[row]?.[col]`, `rowCompleteness[row]`.
+4. Add the `Chờ` toast container *outside* the sectioned grid (it
+ currently uses `absolute inset-0` of the outer relative wrapper —
+ keep that wrapper around all 3 sections).
+5. In `app.css`, define `.section-divider` (e.g.
+ `background: repeating-linear-gradient(90deg, transparent 0 8px, theme(colors.orange.400) 8px 10px); height: 6px;`).
+6. Run `npx svelte-check`, then test in dev: generate card, mark cells,
+ trigger Kinh, trigger Chờ. All should work.
+
+## Todo
+
+- [ ] Add `SECTIONS` + `SECTION_LABELS` constants to PlayerBoard.svelte
+- [ ] Replace flat grid render with 3 sectioned `loto-grid` blocks
+- [ ] Verify absolute-row-index math (click, crossed lookup, rowCompleteness)
+- [ ] Add label markup between sections
+- [ ] Add `.section-divider` style in `app.css`
+- [ ] Verify Chờ toast still appears centered over the whole card
+- [ ] Verify Kinh modal still triggers
+- [ ] Verify dark-mode contrast for labels and dividers
+- [ ] Mobile breakpoint check (sm + base)
+- [ ] `npx svelte-check` clean
+
+## Success Criteria
+
+- Player page shows 3 visually separated 3×9 mini-cards with the 3 labels.
+- Generating a new card, marking cells, and winning all behave identically
+ to before this change.
+- No svelte-check warnings.
+- Dark mode looks intentional (not just light mode in the dark).
+
+## Risk Assessment
+
+| Risk | Likelihood | Impact | Mitigation |
+|---|---|---|---|
+| Off-by-one in absolute row index | Medium | High (broken click handling) | Compute `row = startRow + r` once at the top of the inner each, reuse everywhere. |
+| Toast positioning breaks | Low | Low | Toast container stays at the same wrapper level it's at today. |
+| Border doubling at section seams | Low | Cosmetic | Last row of each section keeps its own `border-b`; separator visually masks it. |
+| `aspect-square` cells distort with new container | Low | Cosmetic | grid-template-columns is still `repeat(9, 1fr)` per section, same width budget per section. |
+
+## Security Considerations
+
+None — view-only change.
+
+## Next Steps
+
+After this phase: Phase 2 (settings + color picker) plugs the
+configurable color into `bg-slate-50 dark:bg-slate-900/60` empty cells
+(replaced by a CSS variable).
diff --git a/web/plans/260426-2343-tan-tan-3-section-board-and-settings/phase-02-settings-color-picker.md b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/phase-02-settings-color-picker.md
new file mode 100644
index 0000000..d7c4143
--- /dev/null
+++ b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/phase-02-settings-color-picker.md
@@ -0,0 +1,227 @@
+# Phase 2 — Settings Modal + Empty-Cell Color Picker
+
+## Context
+
+- Plan: [plan.md](plan.md)
+- Depends on Phase 1 only loosely (works either way; better when Phase 1
+ shipped because the section background also picks up the color).
+- Touches: new `settings-store.svelte.js` + `SettingsButton.svelte`,
+ edits in `PlayerBoard.svelte`, `master/+page.svelte`, `+page.svelte`,
+ `app.css`.
+
+## Overview
+
+- **Priority**: P1 (user-requested feature)
+- **Status**: TODO
+- **Effort**: ~45 min
+- **Description**: Gear icon → modal panel with a color picker. Selected
+ color paints empty/blank cells across player card *and* master tracking
+ grid. Persists in localStorage. Default: brown (matches physical Tân Tân).
+
+## Key Insights
+
+- One **global** color, not per-card. Simpler, matches physical sheet
+ (one paper color).
+- Use **CSS custom property** (`--empty-cell-bg`) on `:root` so a single
+ store update repaints every empty cell with no per-component prop drilling.
+- Settings store goes in `src/lib/settings-store.svelte.js` — the
+ `.svelte.js` extension lets it use Svelte 5 runes (`$state`) at module
+ scope. Components import the reactive `settings` object.
+- The color picker can be a **native** `` to avoid
+ pulling in a UI library. Add a few **preset swatches** (brown, slate,
+ amber, emerald, indigo, neutral white) for one-tap defaults.
+- Keep the modal a11y-clean: `role="dialog"`, `aria-modal="true"`,
+ `Escape` to close, focus trap-ish (focus the close button on open).
+
+## Requirements
+
+### Functional
+- A gear button (⚙) sits in the player page header (`/`) and master page
+ header (`/master`).
+- Clicking the gear opens a modal panel.
+- Modal contains:
+ - A native color picker (``) bound to the current
+ color.
+ - 6 preset swatches as quick-pick buttons.
+ - A "Reset to default" button.
+ - A "Close" button.
+- Color change is **live** — empty cells repaint as user picks.
+- Setting persists in localStorage under key `loto_settings`.
+- Default color: a brown matching the physical card (`#7a4a2b` or close).
+
+### Non-functional
+- No new third-party deps.
+- Settings store must support adding more keys later (extensible shape).
+- Works offline (no network).
+- Dark mode: the picker UI itself must be readable in dark mode; the
+ user-chosen color obviously stays as-is.
+
+## Architecture
+
+### Store shape
+
+```js
+// settings-store.svelte.js
+export const DEFAULT_SETTINGS = Object.freeze({
+ emptyCellColor: '#7a4a2b', // brown — matches physical Minh Tân card
+});
+
+const STORAGE_KEY = 'loto_settings';
+
+export const settings = $state({ ...DEFAULT_SETTINGS });
+
+// load + persist helpers
+export function loadSettings() { /* read localStorage, merge into settings */ }
+export function saveSettings() { /* write current settings */ }
+export function resetSettings() { /* reset to DEFAULT_SETTINGS */ }
+```
+
+A root effect (set up once in `+layout.svelte` or in each page that
+imports the store) calls `loadSettings()` on mount and persists on change.
+Simpler: do it inside the store module via a top-level `$effect.root` in
+the `.svelte.js` file — Svelte 5 supports this.
+
+### CSS variable wiring
+
+In `app.css`, define a default:
+```css
+:root { --empty-cell-bg: #7a4a2b; }
+```
+
+Bind it from the store at the page level (whichever component first
+mounts the store):
+```svelte
+
...
+```
+
+OR set it directly on `document.documentElement` in `loadSettings()` /
+on store change via an effect. The latter avoids prop drilling.
+
+### Empty-cell consumption
+
+Replace existing empty-cell backgrounds:
+
+| File | Current | New |
+|---|---|---|
+| `PlayerBoard.svelte` (empty cell) | `bg-slate-50 dark:bg-slate-900/60` | `style:background-color="var(--empty-cell-bg)"` |
+| `master/+page.svelte` (empty cell, `!hasNumber`) | `bg-slate-100 dark:bg-slate-900/60` | same |
+
+Filled cells keep their existing styles. The chosen color only repaints
+**`hasNumber === false`** cells.
+
+### Settings button placement
+
+A small floating gear button in the top-right of each page's header
+section. Reuse the same `SettingsButton.svelte` component on both pages.
+
+### Modal
+
+`SettingsButton.svelte` owns its own `open` state. When open, renders the
+modal as a sibling overlay. Same dismiss patterns as the existing Kinh
+modal in `PlayerBoard.svelte` (backdrop click + Escape).
+
+## Related Code Files
+
+### Create
+- `src/lib/settings-store.svelte.js` — rune-based reactive store +
+ load/save/reset. ~50 LOC.
+- `src/lib/SettingsButton.svelte` — gear button + modal + color picker
+ + 6 preset swatches + reset/close. ~80 LOC.
+
+### Modify
+- `src/lib/PlayerBoard.svelte` — empty-cell background uses CSS var.
+- `src/routes/master/+page.svelte` — empty-cell background uses CSS var;
+ mount `` in header.
+- `src/routes/+page.svelte` — mount `` in header.
+- `src/app.css` — add `:root { --empty-cell-bg: ...; }` default.
+
+### Don't touch
+- `src/lib/game-logic.js` (no game state changes)
+
+## Implementation Steps
+
+1. Create `src/lib/settings-store.svelte.js` with `settings` rune object,
+ `DEFAULT_SETTINGS`, `loadSettings`, `saveSettings`, `resetSettings`.
+2. In the store module, set up a root effect that:
+ - Reads localStorage on first call.
+ - Writes localStorage on change.
+ - Pushes `settings.emptyCellColor` to
+ `document.documentElement.style.setProperty('--empty-cell-bg', …)`.
+3. Add `:root { --empty-cell-bg: #7a4a2b; }` to `src/app.css` as fallback.
+4. Create `src/lib/SettingsButton.svelte`:
+ - `let { } = $props();` (no props)
+ - Local `open = $state(false)`
+ - Imports `settings` + `resetSettings` from the store
+ - Renders gear button; on click toggles `open`
+ - Modal: ``,
+ 6 preset swatches as buttons setting `settings.emptyCellColor` directly,
+ reset button calling `resetSettings()`, close button.
+ - `Escape` keydown closes modal.
+5. In `PlayerBoard.svelte`, change empty-cell background from
+ `bg-slate-50 dark:bg-slate-900/60` to inline
+ `style:background-color="var(--empty-cell-bg)"`.
+6. In `master/+page.svelte`, do the same for `!hasNumber` cells; also
+ import and mount `` in the header (next to the
+ "Về trang người chơi" link).
+7. In `+page.svelte`, mount `` in the player page header.
+8. Run `npx svelte-check`.
+9. Manual test: open gear, change color, see player card + master grid
+ empty cells update live. Reload page → color persists.
+10. Reset button → returns to brown default.
+
+## Todo
+
+- [ ] Create `settings-store.svelte.js` with rune store + persistence
+- [ ] Create `SettingsButton.svelte` with gear + modal + picker + presets
+- [ ] Add CSS var `--empty-cell-bg` default in `app.css`
+- [ ] Wire CSS var update from store via effect
+- [ ] Update `PlayerBoard.svelte` empty cells to use CSS var
+- [ ] Update `master/+page.svelte` empty cells + mount SettingsButton
+- [ ] Update `+page.svelte` to mount SettingsButton
+- [ ] Test: change color → repaints both boards live
+- [ ] Test: reload → persists
+- [ ] Test: reset → back to brown
+- [ ] Test: Escape closes modal
+- [ ] `npx svelte-check` clean
+- [ ] Update `docs/codebase-summary.md` (new files, new storage key)
+- [ ] Update `docs/project-overview-pdr.md` (note settings feature)
+
+## Success Criteria
+
+- Gear icon visible on `/` and `/master`.
+- Modal opens, picker bound to current color.
+- Picking a color via picker OR swatch repaints empty cells live on both
+ boards.
+- Reload preserves the chosen color.
+- Reset button returns to brown default.
+- No svelte-check errors.
+
+## Risk Assessment
+
+| Risk | Likelihood | Impact | Mitigation |
+|---|---|---|---|
+| `$state` at module scope misuse | Medium | High | Use `.svelte.js` extension; verify with svelte-check; reference Svelte 5 docs if unsure (`docs-seeker` skill / context7). |
+| CSS var doesn't apply across light/dark mode cleanly | Low | Cosmetic | Set var on `:root`, no media-query gating. User picked color overrides the brand-default for both modes — that's the user's call. |
+| localStorage unavailable (private mode) | Low | Setting just doesn't persist | Wrap reads/writes in try/catch (mirror existing `saveGrid`/`loadGrid` pattern). |
+| Race: store loads after first paint, brief flash of fallback color | Low | Cosmetic | Acceptable; default fallback in CSS is the same brown, so no flash for default users. |
+| New storage key collides with existing | None | – | `loto_settings` is unused. |
+
+## Security Considerations
+
+- `` returns a 7-char `#rrggbb` string, no injection
+ vector via CSS variable. Still: validate `/^#[0-9a-fA-F]{6}$/` before
+ applying, fall back to default on mismatch.
+- localStorage data is per-origin; no concerns.
+
+## Next Steps
+
+After this phase the plan is complete. Optional follow-ups (NOT in this plan):
+- Add more settings (font size, sound on Kinh, etc.) — store is already
+ shaped to allow it.
+- Persist user-defined preset swatches.
+
+## Storage Keys (added)
+
+| Key | Shape | Purpose |
+|---|---|---|
+| `loto_settings` | `{ emptyCellColor: "#rrggbb" }` | global UI settings |
diff --git a/web/plans/260426-2343-tan-tan-3-section-board-and-settings/plan.md b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/plan.md
new file mode 100644
index 0000000..0523f79
--- /dev/null
+++ b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/plan.md
@@ -0,0 +1,63 @@
+---
+slug: tan-tan-3-section-board-and-settings
+created: 2026-04-26
+status: completed
+completedAt: 2026-04-26
+mode: fast
+blockedBy: []
+blocks: []
+---
+
+# Tân Tân 3-Section Player Board + Settings
+
+Two cosmetic features that make the app look like a real Minh Tân lô tô sheet:
+
+1. **Visual split** — render the 9×9 player card as 3 stacked 3×9 mini-cards
+ with traditional Tân Tân separator labels. Data and generator unchanged.
+2. **Settings** — gear icon → modal with a color picker for empty cells.
+ Applies to all boards (player + master tracking grid). Default: brown.
+
+Reference image: physical "Minh Tân" card from dochoicholon.com — 3 mini-cards
+stacked on one sheet, brown empty cells, decorative cross-hatch separators
+labeled "Minh Tân" / "Loại đặc biệt" / "Tấn tài tấn lộc".
+
+## Decisions (locked, do not re-ask)
+
+- **Layout**: stacked on one page, not paginated.
+- **Generator**: unchanged — keeps exact 5/row + 5/col on the full 9×9.
+- **Color scope**: setting applies to player card *and* master tracking grid.
+
+## Phases
+
+| # | Phase | Status | File |
+|---|---|---|---|
+| 1 | 3-section visual split for PlayerBoard | DONE | [phase-01-three-section-player-board.md](phase-01-three-section-player-board.md) |
+| 2 | Settings modal + empty-cell color picker | DONE | [phase-02-settings-color-picker.md](phase-02-settings-color-picker.md) |
+
+## Files Touched
+
+| File | Phase | Why |
+|---|---|---|
+| `src/lib/PlayerBoard.svelte` | 1, 2 | render 3 sections; consume color setting |
+| `src/routes/master/+page.svelte` | 2 | consume color setting for empty cells |
+| `src/routes/+page.svelte` | 2 | mount settings button (player page header) |
+| `src/lib/settings-store.svelte.js` | 2 | new — rune-based settings store + persistence |
+| `src/lib/SettingsButton.svelte` | 2 | new — gear button + modal |
+| `src/app.css` | 1, 2 | section separator styles, CSS var for empty color |
+| `docs/project-overview-pdr.md` | 1, 2 | document new visual + settings |
+| `docs/codebase-summary.md` | 1, 2 | new files in component table |
+
+## Key Dependencies
+
+- Phase 2 depends on Phase 1 (so the new section background uses the
+ configurable color from day one). They can also ship independently —
+ Phase 2 will simply apply to both flat and section layouts.
+
+## Out of Scope
+
+- Per-card color override (one global color for all cards).
+- Theme preset bundles.
+- Other settings (font, sound, etc.) — settings store will be extensible
+ but only "empty-cell color" lands now.
+- Decorative bg images / paper texture.
+- Card serial number badge (the "25" badge in the physical photo).
diff --git a/web/plans/260426-2343-tan-tan-3-section-board-and-settings/reports/code-reviewer-260427-0002-session-review.md b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/reports/code-reviewer-260427-0002-session-review.md
new file mode 100644
index 0000000..604b8b0
--- /dev/null
+++ b/web/plans/260426-2343-tan-tan-3-section-board-and-settings/reports/code-reviewer-260427-0002-session-review.md
@@ -0,0 +1,193 @@
+# Code Review — Tân Tân 3-section board + Settings color picker
+
+**Date:** 2026-04-27
+**Reviewer:** code-reviewer
+**Scope:** uncommitted working tree, two features
+
+---
+
+## TL;DR
+
+Both features are essentially correct and ship-worthy. Row-index math is right. Color validation regex is safe enough for a CSS custom property sink. A handful of small bugs/papercuts — none block ship — and one **must-fix** focus/a11y issue worth 5 minutes before deploy.
+
+Verdict: **DONE_WITH_CONCERNS** — ship today, fix the must-fix list before merging the PR.
+
+---
+
+## Must-fix (block ship until done)
+
+### 1. Settings modal: `