diff --git a/docs/superpowers/plans/2026-05-02-phase-2b-polish-and-form-layout.md b/docs/superpowers/plans/2026-05-02-phase-2b-polish-and-form-layout.md
new file mode 100644
index 0000000..8b8e8a2
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-02-phase-2b-polish-and-form-layout.md
@@ -0,0 +1,1257 @@
+# Phase 2B: Polish Foundation + Form Layout — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Land the patina palette, polish vocabulary (backdrop, glass cards, button hierarchy, arrow glyph), and the two-column login form layout across popup, setup wizard, and fullscreen vault.
+
+**Architecture:** Foundation CSS tokens + shared classes go into `popup/styles.css` and `vault/vault.css` first. Each surface (login, setup, vault) is then updated to consume the new classes. The two-column login form gets a `surface: 'popup' | 'fullscreen'` flag on `renderForm()` so the same component renders single-column in popup and two-column in fullscreen.
+
+**Tech Stack:** TypeScript, vanilla DOM, vitest + happy-dom, plain CSS (no preprocessor).
+
+**Spec:** `docs/superpowers/specs/2026-05-02-phase-2b-form-layout-design.md`
+
+---
+
+## File Structure
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `extension/src/shared/glyphs.ts` | Modify | Add `GLYPH_NEXT = '▸'` |
+| `extension/src/popup/styles.css` | Modify | Patina tokens, `.surface-backdrop`, `.glass`, `.btn-primary/secondary` |
+| `extension/src/vault/vault.css` | Modify | Same tokens + form-grid + sticky save bar + header treatment |
+| `extension/src/popup/components/unlock.ts` | Modify | Logo lockup, glass card, primary unlock button |
+| `extension/src/popup/components/settings-vault.ts:164,171` | Modify | Replace `→` with `▸` |
+| `extension/src/setup/setup.ts` | Modify | Backdrop wrapper, glass step cards, `▸` on next buttons |
+| `extension/src/vault/vault.ts` | Modify | Backdrop wrapper, surface flag passed to login renderer, dirty subtitle |
+| `extension/src/popup/components/types/login.ts` | Modify | `surface` param on renderForm; column wrapping for fullscreen |
+| `extension/src/popup/components/__tests__/unlock.test.ts` | Create | Unlock view structure tests |
+| `extension/src/setup/__tests__/setup.test.ts` | Create | Setup wizard structure tests |
+| `extension/src/popup/components/types/__tests__/login.test.ts` | Modify | Surface flag + two-column rendering tests |
+
+---
+
+## Task 1: Add patina color tokens to popup/styles.css
+
+**Files:**
+- Modify: `extension/src/popup/styles.css:3-28`
+
+- [ ] **Step 1: Read the current `:root` block in styles.css**
+
+The existing tokens are at lines 3-28. We're adding patina tokens alongside, keeping the `--accent` alias for backwards compatibility.
+
+- [ ] **Step 2: Replace the `:root` block with patina tokens**
+
+In `extension/src/popup/styles.css`, replace lines 3-28 with:
+
+```css
+:root {
+ /* Patina gold (Phase 2B) */
+ --gold-base: #a88a4a;
+ --gold-mid: #cdb47a;
+ --gold-shadow: #5a3f12;
+ --gold-text: #c9a868;
+ --gold-soft: rgba(184, 149, 86, 0.14);
+ --gold-ring: rgba(184, 149, 86, 0.18);
+ --gold-stroke: #b89556;
+ --gold-hi-end: #dac8a0;
+
+ /* Brand alias (kept for backwards compatibility) */
+ --accent: var(--gold-base);
+ --accent-soft: var(--gold-soft);
+ --accent-strong: var(--gold-shadow);
+
+ /* Surfaces */
+ --bg-page: #0a0e14;
+ --bg-pane: #11161e;
+ --bg-elevated: #1c2330;
+ --bg-card: rgba(22, 27, 34, 0.55);
+ --bg-input: #0a0e14;
+ --border-soft: rgba(255, 255, 255, 0.05);
+ --border-mid: #262d36;
+ --border-subtle: var(--border-mid);
+
+ /* Text */
+ --text: #c9d1d9;
+ --text-muted: #8b949e;
+ --text-dim: #6b7888;
+
+ /* Status */
+ --danger: #ab2b20;
+ --danger-bg: #791111;
+ --success: #6cb37a;
+
+ /* Focus */
+ --focus-ring: 0 0 0 2px var(--gold-ring);
+}
+```
+
+- [ ] **Step 3: Update `body` background to use new token**
+
+Find the `body` rule (around line 36) and replace `background: #0d1117;` with `background: var(--bg-page);`.
+
+- [ ] **Step 4: Update `.brand` color to use `--gold-text`**
+
+Find `.brand` (around line 62) and replace `color: #d2ab43;` with `color: var(--gold-text);`.
+
+- [ ] **Step 5: Build extension to verify no CSS errors**
+
+Run from the `extension/` directory:
+```bash
+npm run build 2>&1 | tail -5
+```
+
+Expected: webpack compiles successfully.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add extension/src/popup/styles.css
+git commit -m "$(cat <<'EOF'
+style(ext/popup): add patina palette tokens
+
+Replaces bright amber #d2ab43 with patina gold #a88a4a as the new base.
+Keeps --accent as alias for backwards compatibility. Adds --bg-card
+and --border-soft for upcoming glass card class.
+EOF
+)"
+```
+
+---
+
+## Task 2: Add patina tokens to vault.css
+
+**Files:**
+- Modify: `extension/src/vault/vault.css:3-28`
+
+- [ ] **Step 1: Apply the same token block to vault.css**
+
+In `extension/src/vault/vault.css`, replace lines 3-28 with the same `:root` block from Task 1 Step 2.
+
+- [ ] **Step 2: Update `body` background and `.brand` color**
+
+Same updates as Task 1 Steps 3-4 but in `vault.css`.
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+cd extension && npm run build 2>&1 | tail -5
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add extension/src/vault/vault.css
+git commit -m "$(cat <<'EOF'
+style(ext/vault): add patina palette tokens
+
+Mirrors popup/styles.css token block so the two surfaces share a
+consistent color vocabulary.
+EOF
+)"
+```
+
+---
+
+## Task 3: Add `.surface-backdrop` class to both stylesheets
+
+**Files:**
+- Modify: `extension/src/popup/styles.css` (append)
+- Modify: `extension/src/vault/vault.css` (append)
+
+- [ ] **Step 1: Append `.surface-backdrop` to popup/styles.css**
+
+Add at the end of `extension/src/popup/styles.css`:
+
+```css
+/* Phase 2B: surface backdrop — subtle radial top-glow + grid texture.
+ Apply to body or a top-level wrapper. Children must sit above the ::before. */
+.surface-backdrop {
+ position: relative;
+ background:
+ radial-gradient(ellipse 700px 240px at 50% -40px, rgba(184, 149, 86, 0.05), transparent 65%),
+ linear-gradient(180deg, #11161e 0%, #0a0e14 100%);
+}
+.surface-backdrop::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background-image:
+ linear-gradient(rgba(255, 255, 255, 0.012) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.012) 1px, transparent 1px);
+ background-size: 18px 18px;
+ pointer-events: none;
+ z-index: 0;
+}
+.surface-backdrop > * {
+ position: relative;
+ z-index: 1;
+}
+```
+
+- [ ] **Step 2: Append the same block to vault.css**
+
+Append the identical block to `extension/src/vault/vault.css`.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add extension/src/popup/styles.css extension/src/vault/vault.css
+git commit -m "$(cat <<'EOF'
+style(ext): add .surface-backdrop class
+
+Subtle radial top-glow + 18px grid texture. Used as the backdrop for
+the login popup, setup wizard, and fullscreen vault shell.
+EOF
+)"
+```
+
+---
+
+## Task 4: Add `.glass` card class to both stylesheets
+
+**Files:**
+- Modify: `extension/src/popup/styles.css` (append)
+- Modify: `extension/src/vault/vault.css` (append)
+
+- [ ] **Step 1: Append `.glass` to both stylesheets**
+
+Add at the end of both `extension/src/popup/styles.css` and `extension/src/vault/vault.css`:
+
+```css
+/* Phase 2B: glass card. Translucent panel with backdrop blur for the
+ unlock card, setup step card, and form section panels. Falls back
+ gracefully on browsers without backdrop-filter (just stays translucent). */
+.glass {
+ background: var(--bg-card);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border: 1px solid var(--border-soft);
+ border-radius: 10px;
+ box-shadow:
+ 0 1px 0 rgba(255, 255, 255, 0.03) inset,
+ 0 6px 18px rgba(0, 0, 0, 0.35);
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add extension/src/popup/styles.css extension/src/vault/vault.css
+git commit -m "$(cat <<'EOF'
+style(ext): add .glass card class
+
+Translucent fill, soft border, inner highlight, drop shadow. Used for
+the unlock card, setup step cards, and form section panels.
+EOF
+)"
+```
+
+---
+
+## Task 5: Add `.btn-primary` / `.btn-secondary` classes
+
+**Files:**
+- Modify: `extension/src/popup/styles.css` (append)
+- Modify: `extension/src/vault/vault.css` (append)
+
+- [ ] **Step 1: Append button hierarchy to both stylesheets**
+
+```css
+/* Phase 2B: button hierarchy. Existing .btn class kept for backwards
+ compatibility; .btn-primary and .btn-secondary express clearer intent
+ and are used in updated views. */
+.btn-primary {
+ background: var(--gold-base);
+ color: var(--bg-page);
+ border: none;
+ padding: 9px 14px;
+ font-size: 12px;
+ font-weight: 600;
+ border-radius: 6px;
+ font-family: inherit;
+ cursor: pointer;
+ letter-spacing: 0.3px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ transition: background-color 0.15s;
+}
+.btn-primary:hover { background: var(--gold-stroke); }
+.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
+.btn-primary:focus-visible {
+ outline: none;
+ box-shadow: var(--focus-ring);
+}
+
+.btn-secondary {
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ color: var(--text-muted);
+ padding: 6px 12px;
+ font-size: 11px;
+ border-radius: 5px;
+ font-family: inherit;
+ cursor: pointer;
+}
+.btn-secondary:hover { border-color: rgba(255, 255, 255, 0.12); color: var(--text); }
+.btn-secondary:focus-visible {
+ outline: none;
+ box-shadow: var(--focus-ring);
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add extension/src/popup/styles.css extension/src/vault/vault.css
+git commit -m "$(cat <<'EOF'
+style(ext): add .btn-primary and .btn-secondary classes
+
+Two-tier button hierarchy. .btn-primary uses patina gold fill; .btn-secondary
+is a ghost button with muted border. Existing .btn class kept for
+backwards compatibility.
+EOF
+)"
+```
+
+---
+
+## Task 6: Add `GLYPH_NEXT` and apply to existing arrow uses
+
+**Files:**
+- Modify: `extension/src/shared/glyphs.ts`
+- Modify: `extension/src/popup/components/settings-vault.ts:164,171`
+- Test: `extension/src/shared/__tests__/glyphs.test.ts` (create or extend)
+
+- [ ] **Step 1: Add `GLYPH_NEXT` constant**
+
+In `extension/src/shared/glyphs.ts`, add after `GLYPH_LOCK`:
+
+```typescript
+export const GLYPH_NEXT = '▸'; // forward / next button (matches ▾/▸ disclosure family)
+```
+
+- [ ] **Step 2: Write a snapshot test for the constants**
+
+Check whether `extension/src/shared/__tests__/glyphs.test.ts` exists. If not, create it:
+
+```typescript
+import { describe, it, expect } from 'vitest';
+import {
+ GLYPH_REVEAL, GLYPH_HIDE, GLYPH_GENERATE, GLYPH_FILL_FROM_TAB,
+ GLYPH_QR, GLYPH_MONO, GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS,
+ GLYPH_LOCK, GLYPH_NEXT,
+} from '../glyphs';
+
+describe('glyph constants', () => {
+ it('uses single unicode codepoints (no emoji multi-codepoint)', () => {
+ const all = [
+ GLYPH_REVEAL, GLYPH_HIDE, GLYPH_GENERATE, GLYPH_FILL_FROM_TAB,
+ GLYPH_QR, GLYPH_MONO, GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS,
+ GLYPH_LOCK, GLYPH_NEXT,
+ ];
+ for (const g of all) {
+ expect([...g].length).toBe(1);
+ }
+ });
+
+ it('GLYPH_NEXT is the small right triangle (U+25B8)', () => {
+ expect(GLYPH_NEXT).toBe('▸');
+ });
+});
+```
+
+- [ ] **Step 3: Run the test**
+
+```bash
+cd extension && npx vitest run src/shared/__tests__/glyphs.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 4: Replace `→` in settings-vault.ts**
+
+In `extension/src/popup/components/settings-vault.ts`, change:
+
+```typescript
+// Line 164:
+Backup & restore →
+// becomes:
+Backup & restore ${GLYPH_NEXT}
+
+// Line 171:
+LastPass CSV →
+// becomes:
+LastPass CSV ${GLYPH_NEXT}
+```
+
+Add the import at the top of the file:
+
+```typescript
+import { GLYPH_NEXT } from '../../shared/glyphs';
+```
+
+- [ ] **Step 5: Run vitest to confirm nothing broke**
+
+```bash
+cd extension && npx vitest run
+```
+
+Expected: all existing tests pass.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add extension/src/shared/glyphs.ts extension/src/shared/__tests__/glyphs.test.ts extension/src/popup/components/settings-vault.ts
+git commit -m "$(cat <<'EOF'
+feat(ext): add GLYPH_NEXT and replace ASCII arrows with ▸
+
+Replaces the ASCII rightwards arrow → with U+25B8 ▸ in settings-vault
+buttons. Matches the existing ▾/▸ disclosure-glyph family.
+EOF
+)"
+```
+
+---
+
+## Task 7: Restructure unlock view with logo lockup, glass card, primary button
+
+**Files:**
+- Modify: `extension/src/popup/components/unlock.ts`
+- Modify: `extension/src/popup/index.html` (body wrapper class)
+- Test: `extension/src/popup/components/__tests__/unlock.test.ts` (create)
+
+- [ ] **Step 1: Apply `.surface-backdrop` to popup body**
+
+In `extension/src/popup/index.html`, change the `
` tag to:
+
+```html
+
+```
+
+- [ ] **Step 2: Write the unlock view structure test**
+
+Create `extension/src/popup/components/__tests__/unlock.test.ts`:
+
+```typescript
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderUnlock } from '../unlock';
+
+vi.mock('../../../shared/state', () => ({
+ getState: () => ({ loading: false, error: null }),
+ setState: vi.fn(),
+ sendMessage: vi.fn(),
+ navigate: vi.fn(),
+ escapeHtml: (s: string) => s,
+ openVaultTab: vi.fn(),
+}));
+
+describe('renderUnlock', () => {
+ let app: HTMLElement;
+ beforeEach(() => {
+ document.body.innerHTML = '
';
+ app = document.getElementById('app')!;
+ });
+
+ it('renders the logo lockup (logo + brand + tagline)', () => {
+ renderUnlock(app);
+ expect(app.querySelector('.brand-logo')).toBeTruthy();
+ expect(app.querySelector('.brand')?.textContent).toBe('Relicario');
+ expect(app.querySelector('.tagline')?.textContent).toContain('two-factor');
+ });
+
+ it('renders the unlock form inside a .glass card', () => {
+ renderUnlock(app);
+ const glass = app.querySelector('.glass');
+ expect(glass).toBeTruthy();
+ expect(glass!.querySelector('#passphrase-input')).toBeTruthy();
+ expect(glass!.querySelector('.btn-primary')).toBeTruthy();
+ });
+
+ it('renders open-vault and settings as secondary buttons outside the card', () => {
+ renderUnlock(app);
+ const vaultBtn = app.querySelector('#vault-btn');
+ const settingsBtn = app.querySelector('#settings-btn');
+ expect(vaultBtn?.classList.contains('btn-secondary')).toBe(true);
+ expect(settingsBtn?.classList.contains('btn-secondary')).toBe(true);
+ // They should NOT be inside the .glass card
+ const glass = app.querySelector('.glass');
+ expect(glass!.contains(vaultBtn!)).toBe(false);
+ });
+});
+```
+
+- [ ] **Step 3: Run tests to verify they fail**
+
+```bash
+cd extension && npx vitest run src/popup/components/__tests__/unlock.test.ts
+```
+
+Expected: FAIL (current unlock view doesn't have these classes / structure).
+
+- [ ] **Step 4: Rewrite renderUnlock**
+
+Replace the entire `renderUnlock` function in `extension/src/popup/components/unlock.ts`:
+
+```typescript
+/// Unlock view — passphrase input with ENTER to submit.
+
+import { getState, setState, sendMessage, navigate, escapeHtml, openVaultTab } from '../../shared/state';
+import type { ItemId, ManifestEntry } from '../../shared/types';
+
+export function renderUnlock(app: HTMLElement): void {
+ const state = getState();
+
+ app.innerHTML = `
+
+
+
+
Relicario
+
two-factor vault
+
+
+
+
unlock
+
+
+
+ ${state.loading ? '
' : ''}
+ ${state.error ? `
${escapeHtml(state.error)}
` : ''}
+
unlock vault
+
+
+
+ open vault
+ settings
+
+
+ `;
+
+ const input = document.getElementById('passphrase-input') as HTMLInputElement;
+ const unlockBtn = document.getElementById('unlock-btn') as HTMLButtonElement | null;
+
+ const submit = async () => {
+ const passphrase = input.value;
+ if (!passphrase) return;
+ setState({ loading: true, error: null });
+ const resp = await sendMessage({ type: 'unlock', passphrase });
+ if (resp.ok) {
+ const listResp = await sendMessage({ type: 'list_items' });
+ if (listResp.ok) {
+ const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> };
+ navigate('list', { entries: data.items });
+ } else {
+ setState({ loading: false, error: listResp.error });
+ }
+ } else {
+ setState({ loading: false, error: resp.error });
+ }
+ };
+
+ if (input && !state.loading) {
+ input.focus();
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
+ }
+ unlockBtn?.addEventListener('click', submit);
+
+ document.getElementById('vault-btn')?.addEventListener('click', () => openVaultTab());
+ document.getElementById('settings-btn')?.addEventListener('click', () => navigate('settings'));
+}
+```
+
+- [ ] **Step 5: Add `.tagline` and `.logo-lockup` CSS to popup/styles.css**
+
+Append to `extension/src/popup/styles.css`:
+
+```css
+.logo-lockup .brand-logo { width: 42px; height: 42px; margin: 0 auto 10px; }
+.logo-lockup .brand { font-size: 17px; font-weight: 600; color: var(--gold-text); letter-spacing: 0.5px; }
+.tagline { color: var(--text-dim); font-size: 11px; margin-top: 4px; letter-spacing: 0.3px; }
+```
+
+- [ ] **Step 6: Run tests to verify they pass**
+
+```bash
+cd extension && npx vitest run src/popup/components/__tests__/unlock.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add extension/src/popup/index.html extension/src/popup/components/unlock.ts extension/src/popup/components/__tests__/unlock.test.ts extension/src/popup/styles.css
+git commit -m "$(cat <<'EOF'
+feat(ext/popup): polish unlock view with logo lockup + glass card
+
+Restructures the unlock screen so the form sits in a glass card with
+a primary 'unlock vault' button. Logo, brand, and tagline are grouped
+as a lockup. Open-vault and settings are demoted to secondary buttons.
+Body gets the .surface-backdrop wrapper.
+EOF
+)"
+```
+
+---
+
+## Task 8: Apply backdrop + glass cards to setup wizard
+
+**Files:**
+- Modify: `extension/src/setup/setup.ts`
+
+- [ ] **Step 1: Find the body wrapper / outer container in setup.ts**
+
+The `app.innerHTML = ...` block around line 191-199 wraps content in a `.pad` div. That's where we'll apply the backdrop.
+
+- [ ] **Step 2: Wrap setup content in `.surface-backdrop`**
+
+In `extension/src/setup/setup.ts`, locate the `render()` function. Change the outer wrapper from:
+
+```typescript
+app.innerHTML = `
+
+ ...
+
+`;
+```
+
+to:
+
+```typescript
+app.innerHTML = `
+
+`;
+```
+
+- [ ] **Step 3: Wrap each step body in a `.glass` card**
+
+Each `renderStepN()` function returns a string starting with `...`. Update each to:
+
+```typescript
+return `
+
+
...
+ ...
+
+`;
+```
+
+Apply this to `renderStep0`, `renderStep1`, `renderStep2`, `renderStep3New`, `renderStep3Attach`, `renderStep4`, `renderStep5`. The `wizard-step glass` combination preserves any existing `.wizard-step` rules while adding the glass treatment.
+
+- [ ] **Step 4: Update mode-card style to use glass class**
+
+In `renderStep0`, the mode cards use `
`. Add `glass` to the class list:
+
+```typescript
+
+```
+
+- [ ] **Step 5: Add `▸` glyph to all "next" buttons**
+
+Import `GLYPH_NEXT` at the top of `setup.ts`:
+
+```typescript
+import { GLYPH_NEXT } from '../shared/glyphs';
+```
+
+Update each "next" button (lines 239, 445, 546, 921) to include the glyph after the label. Use `btn-primary` instead of `btn btn-primary`:
+
+```typescript
+// Line 239:
+next ${GLYPH_NEXT}
+
+// Line 445:
+next ${GLYPH_NEXT}
+
+// Line 546:
+next ${GLYPH_NEXT}
+
+// Line 921 (continue button — also gets the glyph):
+continue ${GLYPH_NEXT}
+```
+
+For `attach-btn` (~line 301) and `create-btn` (~line 696), keep them as `btn-primary` but no arrow glyph (those are commit-action buttons, not "next"-style).
+
+- [ ] **Step 6: Build and visually verify in Chrome**
+
+```bash
+cd extension && npm run build 2>&1 | tail -3
+```
+
+Open the extension's setup page (or load the dist into Chrome) to confirm rendering. This is a manual visual check — there isn't an existing test harness for setup.ts views.
+
+- [ ] **Step 7: Run vitest to confirm nothing regressed**
+
+```bash
+cd extension && npx vitest run
+```
+
+Expected: all existing tests pass.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add extension/src/setup/setup.ts
+git commit -m "$(cat <<'EOF'
+feat(ext/setup): apply polish vocabulary to setup wizard
+
+- Wraps setup content in .surface-backdrop
+- Each wizard step gets a .glass card
+- Mode-picker cards become glass cards
+- 'next' / 'continue' buttons get the ▸ glyph
+- Migrate from .btn .btn-primary to the new .btn-primary class
+EOF
+)"
+```
+
+---
+
+## Task 9: Apply backdrop to fullscreen vault shell
+
+**Files:**
+- Modify: `extension/src/vault/vault.html`
+- Modify: `extension/src/vault/vault.ts`
+
+- [ ] **Step 1: Apply `.surface-backdrop` to body in vault.html**
+
+In `extension/src/vault/vault.html`, change `` to:
+
+```html
+
+```
+
+- [ ] **Step 2: Build and check the existing layout still works**
+
+```bash
+cd extension && npm run build 2>&1 | tail -3
+```
+
+The existing vault layout has its own panes / sidebar; the backdrop sits behind everything via the `::before` pseudo-element.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add extension/src/vault/vault.html
+git commit -m "$(cat <<'EOF'
+style(ext/vault): apply .surface-backdrop to fullscreen body
+
+Subtle radial top-glow + grid texture behind the existing vault shell.
+No layout changes — existing panes sit above the backdrop's ::before.
+EOF
+)"
+```
+
+---
+
+## Task 10: Add `surface` flag to renderForm and per-surface column wrapping
+
+**Files:**
+- Modify: `extension/src/popup/components/types/login.ts:238`
+- Modify: `extension/src/popup/popup.ts` (callers of renderForm)
+- Modify: `extension/src/vault/vault.ts` (callers of renderForm)
+- Test: `extension/src/popup/components/types/__tests__/login.test.ts`
+
+- [ ] **Step 1: Read the current renderForm signature**
+
+```bash
+grep -n "renderForm" extension/src/popup/components/types/login.ts | head -10
+```
+
+The current signature at line 238 is:
+
+```typescript
+export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null): void
+```
+
+- [ ] **Step 2: Write the surface-flag test**
+
+In `extension/src/popup/components/types/__tests__/login.test.ts`, add a test:
+
+```typescript
+describe('renderForm surface flag', () => {
+ let app: HTMLElement;
+ beforeEach(() => {
+ document.body.innerHTML = '
';
+ app = document.getElementById('app')!;
+ // ... existing test setup mocks should already be in place above
+ });
+
+ it('renders single-column when surface is "popup" (default)', () => {
+ renderForm(app, 'add', null);
+ expect(app.querySelector('.form-grid')).toBeNull();
+ });
+
+ it('renders two-column .form-grid wrapper when surface is "fullscreen"', () => {
+ renderForm(app, 'add', null, { surface: 'fullscreen' });
+ const grid = app.querySelector('.form-grid');
+ expect(grid).toBeTruthy();
+ expect(grid!.querySelector('[data-form-section="identity"]')).toBeTruthy();
+ expect(grid!.querySelector('[data-form-section="credentials"]')).toBeTruthy();
+ });
+});
+```
+
+- [ ] **Step 3: Run test to verify it fails**
+
+```bash
+cd extension && npx vitest run src/popup/components/types/__tests__/login.test.ts -t "surface flag"
+```
+
+Expected: FAIL — type error on `{ surface }` arg.
+
+- [ ] **Step 4: Update renderForm signature with optional surface flag**
+
+In `extension/src/popup/components/types/login.ts:238`, change the signature:
+
+```typescript
+export interface RenderFormOptions {
+ surface?: 'popup' | 'fullscreen';
+}
+
+export function renderForm(
+ app: HTMLElement,
+ mode: 'add' | 'edit',
+ existing: Item | null,
+ opts: RenderFormOptions = {}
+): void {
+ const surface = opts.surface ?? 'popup';
+ // ... existing function body, with section wrapping below
+```
+
+- [ ] **Step 5: Wrap Identity / Credentials sections**
+
+Inside `renderForm`, locate the existing form-fields render block (search for the ` ` / ` ` blocks). The current implementation builds the form HTML by concatenating field strings or calling `renderRow(...)` per field. Identify the title / url / group fields (Identity) and username / password / totp fields (Credentials), then wrap them in column containers that only become a grid in fullscreen.
+
+Below, `<>` etc. are placeholders for whatever the existing code generates for that field — leave that code unchanged, just relocate it inside the new wrappers:
+
+```typescript
+const identityHtml = `
+
+ ${surface === 'fullscreen' ? '' : ''}
+ <
>
+ <>
+ <>
+
+`;
+
+const credentialsHtml = `
+
+`;
+
+const sectionsHtml = surface === 'fullscreen'
+ ? `${identityHtml}${credentialsHtml}
`
+ : `${identityHtml}${credentialsHtml}`;
+```
+
+The notes / custom-sections / attachments blocks must remain *outside* the grid — they should sit below the column wrapper as full-width siblings, regardless of surface.
+
+- [ ] **Step 6: Add `.form-grid`, `.form-col`, `.col-header` to vault.css**
+
+Append to `extension/src/vault/vault.css`:
+
+```css
+.form-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+ max-width: 960px;
+ margin: 0 auto;
+}
+@media (max-width: 720px) {
+ .form-grid { grid-template-columns: 1fr; }
+}
+.form-col {
+ padding: 14px 16px;
+}
+.col-header {
+ text-transform: uppercase;
+ letter-spacing: 1.2px;
+ font-weight: 500;
+ color: var(--text-muted);
+ font-size: 10px;
+ border-bottom: 1px solid var(--border-mid);
+ padding-bottom: 6px;
+ margin-bottom: 12px;
+}
+```
+
+- [ ] **Step 7: Update the vault.ts caller to pass `surface: 'fullscreen'`**
+
+```bash
+grep -n "renderForm" extension/src/vault/vault.ts
+```
+
+For each call site in vault.ts, add the surface flag:
+
+```typescript
+renderForm(app, mode, existing, { surface: 'fullscreen' });
+```
+
+The popup.ts callers stay unchanged (default `'popup'`).
+
+- [ ] **Step 8: Run tests**
+
+```bash
+cd extension && npx vitest run src/popup/components/types/__tests__/login.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add extension/src/popup/components/types/login.ts extension/src/popup/components/types/__tests__/login.test.ts extension/src/vault/vault.ts extension/src/vault/vault.css
+git commit -m "$(cat <<'EOF'
+feat(ext/login): add surface flag for two-column fullscreen form
+
+renderForm() takes an optional { surface: 'popup' | 'fullscreen' }
+parameter. When 'fullscreen', the Identity and Credentials field
+groups render as glass cards inside a .form-grid (two columns,
+stacks at <=720px). Popup keeps its single-column layout.
+EOF
+)"
+```
+
+---
+
+## Task 11: Add sticky save bar in fullscreen forms
+
+**Files:**
+- Modify: `extension/src/vault/vault.css` (append)
+- Modify: `extension/src/vault/vault.ts` (form rendering wrapper)
+
+- [ ] **Step 1: Append sticky save bar CSS to vault.css**
+
+```css
+.form-pane {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+}
+.form-scroll {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px 24px;
+}
+.sticky-save-bar {
+ position: sticky;
+ bottom: 0;
+ background: rgba(17, 22, 30, 0.7);
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
+ border-top: 1px solid var(--border-mid);
+ padding: 12px 24px;
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ z-index: 10;
+}
+.sticky-save-bar::before {
+ content: '';
+ position: absolute;
+ top: -24px;
+ left: 0;
+ right: 0;
+ height: 24px;
+ background: linear-gradient(to top, rgba(17, 22, 30, 0.7), transparent);
+ pointer-events: none;
+}
+```
+
+- [ ] **Step 2: Wrap fullscreen form rendering with `.form-pane` + `.sticky-save-bar`**
+
+In `extension/src/vault/vault.ts`, wherever the form is rendered (look for the call to `renderForm`), wrap with the pane structure:
+
+```typescript
+function renderFormWrapped(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null) {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'form-pane';
+ wrapper.innerHTML = `
+
+
+ cancel
+ save
+
+ `;
+ app.replaceChildren(wrapper);
+
+ const scrollEl = wrapper.querySelector('#form-scroll') as HTMLElement;
+ renderForm(scrollEl, mode, existing, { surface: 'fullscreen' });
+
+ wrapper.querySelector('#form-cancel')?.addEventListener('click', () => {
+ // dispatch existing cancel handler — wire up to existing flow
+ document.getElementById('form-cancel-existing')?.click();
+ });
+ wrapper.querySelector('#form-save')?.addEventListener('click', () => {
+ document.getElementById('form-save-existing')?.click();
+ });
+}
+```
+
+The exact wiring depends on how the existing form save/cancel buttons are structured. The save bar buttons should trigger the same handlers — easiest path is to make the existing form's save/cancel buttons hidden when `surface === 'fullscreen'` and let the sticky bar trigger them.
+
+A cleaner alternative: have `renderForm` accept a flag to skip rendering its own action buttons when the wrapper is providing them. Add to `RenderFormOptions`:
+
+```typescript
+export interface RenderFormOptions {
+ surface?: 'popup' | 'fullscreen';
+ /** When true, renderForm skips its own save/cancel buttons (caller provides them in a sticky bar). */
+ externalActions?: boolean;
+}
+```
+
+And inside renderForm, gate the existing save/cancel button render on `!opts.externalActions`. Then the sticky bar buttons can call the existing save/cancel functions directly (export them from `login.ts` if necessary).
+
+- [ ] **Step 3: Build to verify CSS / TS compile**
+
+```bash
+cd extension && npm run build 2>&1 | tail -3
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add extension/src/vault/vault.css extension/src/vault/vault.ts extension/src/popup/components/types/login.ts
+git commit -m "$(cat <<'EOF'
+feat(ext/vault): sticky save bar in fullscreen forms
+
+The form pane gets a flex column layout: scrollable content above,
+sticky save bar at bottom. Bar uses translucent fill with backdrop-blur
+and a 24px gradient fade so content scrolls under it. Save / cancel
+buttons reuse the form's existing handlers via externalActions flag.
+EOF
+)"
+```
+
+---
+
+## Task 12: Header treatment with dirty-state subtitle
+
+**Files:**
+- Modify: `extension/src/vault/vault.css` (append)
+- Modify: `extension/src/vault/vault.ts`
+
+- [ ] **Step 1: Append form header styles to vault.css**
+
+```css
+.fullscreen-form-header {
+ padding: 14px 24px;
+ border-bottom: 1px solid var(--border-mid);
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+}
+.fullscreen-form-header .title {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--text);
+}
+.fullscreen-form-header .sub {
+ font-size: 11px;
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+.fullscreen-form-header .hint {
+ font-size: 11px;
+ color: var(--text-dim);
+}
+```
+
+- [ ] **Step 2: Detect platform for keyboard hint label**
+
+Add a small helper near the top of `vault.ts`:
+
+```typescript
+const isMac = navigator.platform.toLowerCase().includes('mac');
+const SAVE_HINT = isMac ? '⌘+S to save' : 'Ctrl+S to save';
+```
+
+- [ ] **Step 3: Render the header above `.form-pane`**
+
+Update `renderFormWrapped` from Task 11 to include the header:
+
+```typescript
+function renderFormWrapped(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null) {
+ const titleText = mode === 'add' ? 'new login' : 'edit login';
+ const wrapper = document.createElement('div');
+ wrapper.className = 'form-pane';
+ wrapper.innerHTML = `
+
+
+
+ cancel
+ save
+
+ `;
+ app.replaceChildren(wrapper);
+ // ... rest of wiring from Task 11
+}
+```
+
+- [ ] **Step 4: Wire dirty-state subscription**
+
+Add a small dirty-tracker that listens for `input`/`change` events on the form scroll element. When any input fires, set the subtitle to "unsaved · esc to cancel"; on save/cancel, reset to "no changes":
+
+```typescript
+const scrollEl = wrapper.querySelector('#form-scroll') as HTMLElement;
+const subEl = wrapper.querySelector('#form-dirty-sub') as HTMLElement;
+let isDirty = false;
+const markDirty = () => {
+ if (isDirty) return;
+ isDirty = true;
+ subEl.textContent = 'unsaved · esc to cancel';
+};
+const markClean = () => {
+ isDirty = false;
+ subEl.textContent = 'no changes';
+};
+scrollEl.addEventListener('input', markDirty, true);
+scrollEl.addEventListener('change', markDirty, true);
+
+wrapper.querySelector('#form-cancel')?.addEventListener('click', () => {
+ markClean();
+ // ... existing cancel logic
+});
+wrapper.querySelector('#form-save')?.addEventListener('click', () => {
+ markClean();
+ // ... existing save logic
+});
+```
+
+- [ ] **Step 5: Write a happy-dom test for the dirty subtitle**
+
+In `extension/src/vault/__tests__/`, create or extend a test file:
+
+```typescript
+import { describe, it, expect, beforeEach } from 'vitest';
+
+// Pseudo-test — the actual test mounts renderFormWrapped with real wiring.
+// If renderFormWrapped is unexported, export it from vault.ts for testing.
+
+describe('fullscreen form dirty subtitle', () => {
+ let app: HTMLElement;
+ beforeEach(() => {
+ document.body.innerHTML = '
';
+ app = document.getElementById('app')!;
+ });
+
+ it('starts pristine and switches to dirty on first input', () => {
+ // Mount renderFormWrapped(app, 'add', null);
+ // const sub = app.querySelector('#form-dirty-sub')!;
+ // expect(sub.textContent).toBe('no changes');
+ // const titleInput = app.querySelector('input[type=text]') as HTMLInputElement;
+ // titleInput.value = 'x';
+ // titleInput.dispatchEvent(new Event('input', { bubbles: true }));
+ // expect(sub.textContent).toContain('unsaved');
+ });
+});
+```
+
+If `renderFormWrapped` is internal, expose it via `__test__` export gated on test env:
+
+```typescript
+// vault.ts
+export const __test__ = { renderFormWrapped };
+```
+
+- [ ] **Step 6: Build to verify**
+
+```bash
+cd extension && npm run build 2>&1 | tail -3
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add extension/src/vault/vault.css extension/src/vault/vault.ts extension/src/vault/__tests__/
+git commit -m "$(cat <<'EOF'
+feat(ext/vault): fullscreen form header with dirty-state subtitle
+
+Title left ('new login' / 'edit login'), subtitle below cycles between
+'no changes' and 'unsaved · esc to cancel' on input events. Right side
+shows the platform-aware save hint ('⌘+S to save' / 'Ctrl+S to save').
+The actual ⌘+S keymap arrives in Phase 3 — this is a visual hint only.
+EOF
+)"
+```
+
+---
+
+## Task 13: Final verification
+
+- [ ] **Step 1: Run full test suite**
+
+```bash
+cd extension && npx vitest run
+```
+
+Expected: all tests pass. If any test fails, fix and recommit before proceeding.
+
+- [ ] **Step 2: Build for production (Chrome + Firefox)**
+
+```bash
+cd extension && npm run build:all
+```
+
+Expected: webpack compiles both targets with no errors (only the existing 4MB WASM warning).
+
+- [ ] **Step 3: Verify rebuilt extension manifest**
+
+```bash
+grep '"name"' extension/dist/manifest.json extension/dist-firefox/manifest.json
+```
+
+Expected: both show `"name": "Relicario"`.
+
+- [ ] **Step 4: Manual smoke test (load unpacked dist into Chrome)**
+
+Open `chrome://extensions`, enable Developer Mode, "Load unpacked" → `extension/dist/`. Verify:
+
+- Click the extension icon: popup opens, shows logo lockup, glass card with passphrase input, primary "unlock vault" button, secondary buttons below.
+- Open the setup wizard (e.g., via `extension/dist/setup.html`): each step renders inside a glass card, "next" buttons show `▸` glyph.
+- Open the fullscreen vault (extension menu → "Open Relicario vault"): backdrop visible behind the existing pane layout.
+- Add a new login item: form renders two-column with Identity and Credentials cards, sticky save bar at bottom, header subtitle changes from "no changes" to "unsaved · esc to cancel" on first input.
+
+- [ ] **Step 5: Commit verification log if anything was missed**
+
+If the smoke test reveals follow-up tweaks, fix them and commit with message `style(ext): polish smoke-test fixes`. Otherwise, no commit.
+
+---
+
+## Completion Checklist
+
+- [ ] Task 1: Patina tokens in popup/styles.css
+- [ ] Task 2: Patina tokens in vault.css
+- [ ] Task 3: `.surface-backdrop` class
+- [ ] Task 4: `.glass` card class
+- [ ] Task 5: `.btn-primary` / `.btn-secondary`
+- [ ] Task 6: `GLYPH_NEXT` + arrow replacements
+- [ ] Task 7: Unlock view restructure
+- [ ] Task 8: Setup wizard polish
+- [ ] Task 9: Vault shell backdrop
+- [ ] Task 10: Surface flag + two-column form
+- [ ] Task 11: Sticky save bar
+- [ ] Task 12: Header dirty subtitle
+- [ ] Task 13: Final verification