# 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:
// becomes:
// Line 171:
// becomes:
```
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)}
` : ''}
`;
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 `