# Dev A Kickoff Prompt — v0.5.1 Stream A (Fullscreen + Popup Layout)

> **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.

Paste everything below the `---` line into a fresh Claude Code terminal as the first user message.

---

You are a **senior developer** owning Stream A for the Relicario v0.5.1 release. Stream A is the fullscreen vault tab layout overhaul + popup polish: 3-column layout (sidebar category nav, full-width list, slide-in drawer), bottom sheet for new-item type picker, popup type-picker polish, per-type glyph icons, and a shared toast system.

**Goal:** Replace the current 2-column vault tab (sidebar + single pane) with a responsive 3-column layout. All emoji type icons become Unicode monochrome glyphs. A shared toast module replaces ad-hoc status divs.

**Architecture:** All changes are in the extension. `vault.ts` is a full layout rewrite. `item-list.ts` and `item-form.ts` get glyph replacements and polish. A new `toast.ts` provides the shared notification API. CSS lives in `vault.css` (fullscreen) and `popup/styles.css` (popup).

**Tech Stack:** TypeScript, vitest, webpack/bun.

---

## Setup (do this first)

```bash
cd /home/alee/Sources/relicario
git fetch
git checkout main
git pull
git worktree add ../relicario.v0.5.1-stream-a -b feature/v0.5.1-stream-a-layout
cd ../relicario.v0.5.1-stream-a
pwd  # should print /home/alee/Sources/relicario.v0.5.1-stream-a
```

**ALL subsequent work happens in `/home/alee/Sources/relicario.v0.5.1-stream-a`**. Every subagent prompt MUST begin with `cd /home/alee/Sources/relicario.v0.5.1-stream-a`.

Today: 2026-05-03. Project rules in `CLAUDE.md` apply.

## Required reading

1. `CLAUDE.md` — project rules
2. `docs/superpowers/specs/2026-05-03-v0.5.x-ux-polish-and-recovery-qr-design.md` — spec sections A1–A7
3. `extension/src/vault/vault.ts` — current implementation (read fully before editing)
4. `extension/src/vault/vault.css` — current styles
5. `extension/src/popup/components/item-list.ts` — popup item list
6. `extension/src/popup/components/item-form.ts` — type-picker
7. `extension/src/shared/glyphs.ts` — existing glyph constants

## Execution mode

Use **subagent-driven-development**. Fresh subagent per task, two-stage review between tasks. Every subagent prompt MUST start with `cd /home/alee/Sources/relicario.v0.5.1-stream-a`.

## Interface contract with DEV-B (settings component)

DEV-B will deliver a settings component with these exports. **You must use exactly this signature** in `vault.ts` when wiring the settings view:

```ts
// extension/src/popup/components/settings.ts
export async function renderSettings(container: HTMLElement): Promise<void>;
export function teardownSettings(): void;
```

Your vault.ts should call `renderSettings(pane)` when the `#settings` route is active, and `teardownSettings()` when navigating away. You can proceed with a stub import while DEV-B's branch is in progress; it will be reconciled at merge time.

## Scope and boundaries

**In scope:** A1–A7 (layout, drawer, bottom sheet, type picker, glyphs, empty states, toast).

**Out of scope:** Stream B and C work. If you hit a bug outside your scope, file a `## QUESTION TO PM` block.

**Hard rules:**
- No emoji anywhere in `extension/src/`. If you see one while editing, replace it with the monochrome glyph.
- `glyphs.ts` is the single source of truth. No inline Unicode literals at call sites.
- Don't merge to main. The PM owns merges.

## Relay server

A message-bus MCP server is running on `localhost:7331`. You have three native tools:

- `post_message(from, to, kind, body)` — push a message; your `from` is always `"dev-a"`
- `read_messages(for)` — drain your inbox; call with `for="dev-a"` before each task
- `list_pending(for)` — check inbox count without consuming

Recipients: `pm`, `dev-a`, `dev-b`, `dev-c`. Use these instead of asking the user to copy-paste. Before starting each task: `read_messages(for="dev-a")`. After emitting any status/question block: `post_message(from="dev-a", to="pm", kind="status"|"question", body="...")`.

## Coordination protocol

Before starting each task, call `read_messages(for="dev-a")` to drain your inbox.

When posting a status update, call `post_message(from="dev-a", to="pm", kind="status", body="...")` with the body:

```
## STATUS UPDATE — DEV-A
Time: <iso8601>
Task: <N of 14>
Status: IN-PROGRESS | BLOCKED | REVIEW-READY
Summary: <one line>
Next: <next task or "waiting for PM">
```

---

## Files

**Create:**
- `extension/src/shared/toast.ts` — shared notification module

**Modify:**
- `extension/src/shared/glyphs.ts` — add GLYPH_VAULT_TAB + 7 per-type constants
- `extension/src/shared/__tests__/glyphs.test.ts` — add new constants to the test
- `extension/src/popup/components/item-list.ts` — glyph vault-btn, type icons, empty states, toast
- `extension/src/popup/components/item-form.ts` — TYPE_OPTIONS glyphs, polished type picker
- `extension/src/vault/vault.ts` — full layout rewrite
- `extension/src/vault/vault.css` — 3-column layout, drawer, bottom sheet, responsive
- `extension/src/popup/styles.css` — toast styles, type-icon pill, empty-state styles

---

### Task 1: Add `GLYPH_VAULT_TAB` and per-type glyph constants

**Files:**
- Modify: `extension/src/shared/glyphs.ts`
- Modify: `extension/src/shared/__tests__/glyphs.test.ts`

- [ ] **Step 1: Update the failing test first**

In `extension/src/shared/__tests__/glyphs.test.ts`, add to the existing test:

```ts
it('exports GLYPH_VAULT_TAB as U+29C9', () => {
  expect(glyphs.GLYPH_VAULT_TAB).toBe('⧉');
});

it('exports per-type glyph constants', () => {
  expect(glyphs.GLYPH_TYPE_LOGIN).toBe('◉');
  expect(glyphs.GLYPH_TYPE_SECURE_NOTE).toBe('◫');
  expect(glyphs.GLYPH_TYPE_TOTP).toBe('⊡');
  expect(glyphs.GLYPH_TYPE_CARD).toBe('▭');
  expect(glyphs.GLYPH_TYPE_IDENTITY).toBe('⌬');
  expect(glyphs.GLYPH_TYPE_KEY).toBe('⊹');
  expect(glyphs.GLYPH_TYPE_DOCUMENT).toBe('≡');
});

it('per-type glyphs are single codepoints (no emoji)', () => {
  const typeGlyphs = [
    glyphs.GLYPH_TYPE_LOGIN, glyphs.GLYPH_TYPE_SECURE_NOTE, glyphs.GLYPH_TYPE_TOTP,
    glyphs.GLYPH_TYPE_CARD, glyphs.GLYPH_TYPE_IDENTITY, glyphs.GLYPH_TYPE_KEY,
    glyphs.GLYPH_TYPE_DOCUMENT,
  ];
  for (const g of typeGlyphs) {
    expect([...g].length).toBe(1);
  }
});
```

- [ ] **Step 2: Run tests to confirm they fail**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | grep -E "FAIL|✗|×"
```

Expected: the new test cases fail (constants not exported yet).

- [ ] **Step 3: Add constants to glyphs.ts**

In `extension/src/shared/glyphs.ts`, add after the existing `GLYPH_NEXT` line:

```ts
export const GLYPH_VAULT_TAB      = '⧉';   // U+29C9 pop-out to fullscreen vault tab

export const GLYPH_TYPE_LOGIN       = '◉';  // login
export const GLYPH_TYPE_SECURE_NOTE = '◫';  // secure note
export const GLYPH_TYPE_TOTP        = '⊡';  // totp / 2FA
export const GLYPH_TYPE_CARD        = '▭';  // card
export const GLYPH_TYPE_IDENTITY    = '⌬';  // identity
export const GLYPH_TYPE_KEY         = '⊹';  // SSH / API key
export const GLYPH_TYPE_DOCUMENT    = '≡';  // document
```

- [ ] **Step 4: Run tests to confirm they pass**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | grep -E "PASS|✓|Tests"
```

Expected: all glyph tests pass.

- [ ] **Step 5: Commit**

```bash
git add extension/src/shared/glyphs.ts extension/src/shared/__tests__/glyphs.test.ts
git commit -m "feat(ext/glyphs): add GLYPH_VAULT_TAB and per-type icon constants"
```

---

### Task 2: Replace `&#x2934;` vault button in `item-list.ts`

**Files:**
- Modify: `extension/src/popup/components/item-list.ts`

- [ ] **Step 1: Update the import**

In `item-list.ts`, find the imports section and add `GLYPH_VAULT_TAB` to the glyphs import:

```ts
import { GLYPH_VAULT_TAB } from '../../shared/glyphs';
```

- [ ] **Step 2: Replace the inline HTML entity**

In `item-list.ts:69`, change:

```ts
// Old:
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">&#x2934;</button>

// New:
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">${GLYPH_VAULT_TAB}</button>
```

- [ ] **Step 3: Build and check for TS errors**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/popup/components/item-list.ts
git commit -m "fix(ext/popup): replace inline &#x2934; vault-tab button with GLYPH_VAULT_TAB"
```

---

### Task 3: Replace emoji `typeIcon()` in `item-list.ts` with glyph function

**Files:**
- Modify: `extension/src/popup/components/item-list.ts`

- [ ] **Step 1: Add glyph imports to item-list.ts**

```ts
import {
  GLYPH_VAULT_TAB,
  GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
  GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
} from '../../shared/glyphs';
```

- [ ] **Step 2: Replace `typeIcon()` in item-list.ts**

The existing `typeIcon()` function (lines ~16–26) uses emoji. Replace entirely:

```ts
function typeIcon(t: ItemType): string {
  switch (t) {
    case 'login':       return GLYPH_TYPE_LOGIN;
    case 'secure_note': return GLYPH_TYPE_SECURE_NOTE;
    case 'identity':    return GLYPH_TYPE_IDENTITY;
    case 'card':        return GLYPH_TYPE_CARD;
    case 'key':         return GLYPH_TYPE_KEY;
    case 'document':    return GLYPH_TYPE_DOCUMENT;
    case 'totp':        return GLYPH_TYPE_TOTP;
  }
}
```

Also replace the 📎 paperclip emoji in `buildRowsHtml()`:

```ts
// Old:
${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">📎</span>' : ''}

// New:
${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">⊕</span>' : ''}
```

- [ ] **Step 3: Build and confirm no TS errors**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/popup/components/item-list.ts
git commit -m "fix(ext/popup): replace emoji typeIcon with glyph constants in item-list"
```

---

### Task 4: Empty states in `item-list.ts`

**Files:**
- Modify: `extension/src/popup/components/item-list.ts`
- Modify: `extension/src/popup/styles.css`

- [ ] **Step 1: Update `buildRowsHtml()` to use two distinct empty states**

Current empty state is `'<div class="empty">no items</div>'`. Split into:

```ts
function buildRowsHtml(): string {
  const state = getState();
  const filtered = getFilteredEntries();

  if (filtered.length === 0) {
    if (state.searchQuery) {
      return `
        <div class="empty-state">
          <span class="empty-state__icon" aria-hidden="true">⊘</span>
          <div class="empty-state__title">No results for "${escapeHtml(state.searchQuery)}"</div>
          <div class="empty-state__hint">Try a shorter search term.</div>
        </div>
      `;
    }
    return `
      <div class="empty-state">
        <span class="empty-state__icon" aria-hidden="true">◈</span>
        <div class="empty-state__title">No items yet</div>
        <div class="empty-state__hint">Press <kbd>+</kbd> to add your first item.</div>
      </div>
    `;
  }

  return filtered.map(([id, e], i) => `
    <div class="entry-row ${i === state.selectedIndex ? 'selected' : ''}" data-id="${escapeHtml(id)}" data-index="${i}">
      <span class="entry-name"><span class="type-icon" aria-hidden="true">${typeIcon(e.type)}</span> ${escapeHtml(e.title)}${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">⊕</span>' : ''}</span>
      <span class="entry-meta">${escapeHtml(metaLine(e))}</span>
    </div>
  `).join('');
}
```

- [ ] **Step 2: Add empty-state CSS to styles.css**

In `extension/src/popup/styles.css`, add:

```css
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  text-align: center;
}

.empty-state__icon {
  font-size: 28px;
  color: var(--text-muted, #8b949e);
  margin-bottom: 12px;
  display: block;
}

.empty-state__title {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 4px;
}

.empty-state__hint {
  font-size: 11px;
  color: var(--text-muted, #8b949e);
}
```

- [ ] **Step 3: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/popup/components/item-list.ts extension/src/popup/styles.css
git commit -m "feat(ext/popup): empty states with glyph icons in item-list"
```

---

### Task 5: Polished type-picker in `item-form.ts`

**Files:**
- Modify: `extension/src/popup/components/item-form.ts`

- [ ] **Step 1: Replace emoji in TYPE_OPTIONS**

Current `TYPE_OPTIONS` uses emoji icons. Replace:

```ts
import {
  GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
  GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
} from '../../shared/glyphs';

const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string; description: string }> = [
  { type: 'login',       icon: GLYPH_TYPE_LOGIN,       label: 'Login',       description: 'Username + password' },
  { type: 'secure_note', icon: GLYPH_TYPE_SECURE_NOTE, label: 'Secure Note', description: 'Encrypted text note' },
  { type: 'identity',    icon: GLYPH_TYPE_IDENTITY,    label: 'Identity',    description: 'Personal details' },
  { type: 'card',        icon: GLYPH_TYPE_CARD,        label: 'Card',        description: 'Credit / debit card' },
  { type: 'key',         icon: GLYPH_TYPE_KEY,         label: 'SSH / API Key', description: 'Keys and tokens' },
  { type: 'document',    icon: GLYPH_TYPE_DOCUMENT,    label: 'Document',    description: 'File attachment' },
  { type: 'totp',        icon: GLYPH_TYPE_TOTP,        label: 'TOTP',        description: '2FA authenticator' },
];
```

- [ ] **Step 2: Upgrade `renderTypeSelection` to a 2-column card grid**

Replace the current `renderTypeSelection` function's HTML:

```ts
function renderTypeSelection(app: HTMLElement): void {
  app.innerHTML = `
    <div class="pad">
      <div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
        <button class="btn" id="back-btn">◂ back</button>
        <span style="font-size:14px; font-weight:600;">New item</span>
        <span style="flex:1;"></span>
        ${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⧉</button>'}
      </div>
      <div class="type-card-grid">
        ${TYPE_OPTIONS.map((opt) => `
          <button class="type-card" data-type="${opt.type}">
            <span class="type-card__icon" aria-hidden="true">${opt.icon}</span>
            <span class="type-card__label">${escapeHtml(opt.label)}</span>
            <span class="type-card__desc">${escapeHtml(opt.description)}</span>
          </button>
        `).join('')}
      </div>
      <div class="keyhints"><span><kbd>Esc</kbd> back</span></div>
    </div>
  `;

  document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
  document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') navigate('list');
  }, { once: true });

  document.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const type = btn.dataset.type as ItemType;
      setState({ newType: type });
      renderItemForm(app, 'add');
    });
  });
}
```

- [ ] **Step 3: Add type-card CSS to styles.css**

In `extension/src/popup/styles.css`:

```css
.type-card-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 12px;
}

.type-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 10px 12px;
  background: var(--bg-elevated, #161b22);
  border: 1px solid var(--border, #30363d);
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s;
}

.type-card:hover { border-color: var(--gold, #b8860b); }

.type-card__icon  { font-size: 20px; margin-bottom: 4px; }
.type-card__label { font-size: 12px; font-weight: 600; }
.type-card__desc  { font-size: 10px; color: var(--text-muted, #8b949e); margin-top: 2px; }
```

- [ ] **Step 4: Build and run tests**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | tail -10
```

- [ ] **Step 5: Commit**

```bash
git add extension/src/popup/components/item-form.ts extension/src/popup/styles.css
git commit -m "feat(ext/popup): polished 2-column type-picker with glyph icons"
```

---

### Task 6: Toast system

**Files:**
- Create: `extension/src/shared/toast.ts`
- Modify: `extension/src/popup/styles.css`
- Modify: `extension/src/vault/vault.css`

- [ ] **Step 1: Write toast.ts**

```ts
// extension/src/shared/toast.ts

export function showToast(
  message: string,
  type: 'success' | 'error' | 'info' = 'info',
  durationMs = 2500,
): void {
  let container = document.querySelector<HTMLElement>('.relicario-toast-container');
  if (!container) {
    container = document.createElement('div');
    container.className = 'relicario-toast-container';
    document.body.appendChild(container);
  }

  const toast = document.createElement('div');
  toast.className = `relicario-toast relicario-toast--${type}`;
  toast.textContent = message;
  container.appendChild(toast);

  requestAnimationFrame(() => {
    requestAnimationFrame(() => toast.classList.add('relicario-toast--visible'));
  });

  setTimeout(() => {
    toast.classList.remove('relicario-toast--visible');
    toast.addEventListener('transitionend', () => toast.remove(), { once: true });
  }, durationMs);
}
```

- [ ] **Step 2: Add toast CSS to both stylesheets**

In `extension/src/popup/styles.css` AND `extension/src/vault/vault.css`, add:

```css
/* Toast notifications */
.relicario-toast-container {
  position: fixed;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 6px;
  pointer-events: none;
  z-index: 9999;
}

/* Vault tab: position bottom-right instead */
.vault-shell .relicario-toast-container {
  left: auto;
  right: 24px;
  transform: none;
}

.relicario-toast {
  padding: 8px 16px;
  border-radius: 6px;
  font-size: 12px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s, transform 0.2s;
  pointer-events: none;
}

.relicario-toast--visible {
  opacity: 1;
  transform: translateY(0);
}

.relicario-toast--success { background: #1f4a24; color: #aff0b5; border: 1px solid #238636; }
.relicario-toast--error   { background: #4a1f1f; color: #f0afaf; border: 1px solid #ab2b20; }
.relicario-toast--info    { background: #1f2d4a; color: #afc8f0; border: 1px solid #1f6feb; }
```

- [ ] **Step 3: Replace sync-status div in item-list.ts with toast**

In `item-list.ts`, find the sync button handler and replace the manual status update with toast:

```ts
import { showToast } from '../../shared/toast';

// In the sync button click handler:
document.getElementById('sync-btn')?.addEventListener('click', async () => {
  setState({ loading: true, error: null });
  const resp = await sendMessage({ type: 'sync' });
  if (resp.ok) {
    const listResp = await sendMessage({ type: 'list_items' });
    if (listResp.ok) {
      const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> };
      setState({ entries: data.items, loading: false });
      showToast('Synced', 'success');
      return;
    }
    setState({ loading: false, error: listResp.error });
    showToast(listResp.error ?? 'Sync failed', 'error');
  } else {
    setState({ loading: false, error: resp.error });
    showToast(resp.error ?? 'Sync failed', 'error');
  }
});
```

- [ ] **Step 4: Build and run tests**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | tail -10
```

- [ ] **Step 5: Commit**

```bash
git add extension/src/shared/toast.ts extension/src/popup/styles.css extension/src/vault/vault.css extension/src/popup/components/item-list.ts
git commit -m "feat(ext): shared toast notification system"
```

---

### Task 7: `vault.ts` — replace emoji `typeIcon()` with glyphs

**Files:**
- Modify: `extension/src/vault/vault.ts`

- [ ] **Step 1: Add glyph imports to vault.ts**

Find the glyphs import line in vault.ts and add the type constants:

```ts
import {
  GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_NEXT,
  GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
  GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
} from '../shared/glyphs';
```

- [ ] **Step 2: Replace `typeIcon()` in vault.ts**

Current `typeIcon()` (~line 61) uses Unicode escape sequences for emoji. Replace:

```ts
function typeIcon(t: ItemType): string {
  switch (t) {
    case 'login':       return GLYPH_TYPE_LOGIN;
    case 'secure_note': return GLYPH_TYPE_SECURE_NOTE;
    case 'identity':    return GLYPH_TYPE_IDENTITY;
    case 'card':        return GLYPH_TYPE_CARD;
    case 'key':         return GLYPH_TYPE_KEY;
    case 'document':    return GLYPH_TYPE_DOCUMENT;
    case 'totp':        return GLYPH_TYPE_TOTP;
  }
}
```

- [ ] **Step 3: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/vault/vault.ts
git commit -m "fix(ext/vault): replace emoji typeIcon with glyph constants"
```

---

### Task 8: `vault.css` — 3-column layout rules

**Files:**
- Modify: `extension/src/vault/vault.css`

Add or replace the layout section. The existing `.vault-sidebar` and `.vault-pane` rules will be superseded.

- [ ] **Step 1: Add 3-column shell + drawer CSS**

In `vault.css`, add/replace the shell layout section:

```css
/* === 3-column shell === */
.vault-shell {
  display: flex;
  height: 100vh;
  overflow: hidden;
  background: var(--bg-page, #0d1117);
}

/* Sidebar */
.vault-sidebar {
  width: 200px;
  min-width: 200px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border, #30363d);
  background: var(--bg-sidebar, #0d1117);
  overflow-y: auto;
  flex-shrink: 0;
}

/* List pane (flex: 1, fills between sidebar and drawer) */
.vault-list-pane {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  min-width: 0;
}

/* Detail drawer */
.vault-drawer {
  width: 440px;
  min-width: 440px;
  max-width: 440px;
  border-left: 1px solid var(--border, #30363d);
  background: var(--bg-elevated, #161b22);
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform 0.2s ease;
  flex-shrink: 0;
}

.vault-drawer--open {
  transform: translateX(0);
}

/* List rows */
.vault-list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  cursor: pointer;
  border-bottom: 1px solid var(--border-subtle, #21262d);
  transition: background 0.1s;
}

.vault-list-row:hover { background: var(--bg-hover, #161b22); }

.vault-list-row--selected {
  background: var(--bg-selected, #1c2d41);
  border-left: 2px solid var(--gold, #b8860b);
}

.vault-list-row__icon {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-elevated, #161b22);
  border-radius: 6px;
  border: 1px solid var(--border, #30363d);
  font-size: 14px;
  flex-shrink: 0;
}

.vault-list-row--selected .vault-list-row__icon { border-color: var(--gold, #b8860b); }

.vault-list-row__text { flex: 1; min-width: 0; }

.vault-list-row__title {
  font-size: 13px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vault-list-row__subtitle {
  font-size: 11px;
  color: var(--text-muted, #8b949e);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 1px;
}

.vault-list-row__age {
  font-size: 10px;
  color: var(--text-dim, #6e7681);
  flex-shrink: 0;
}

/* Bottom sheet */
.vault-bottom-sheet-scrim {
  position: absolute;
  inset: 0 0 0 200px; /* exclude sidebar */
  background: rgba(0,0,0,0.5);
  opacity: 0;
  transition: opacity 0.2s;
  pointer-events: none;
  z-index: 100;
}

.vault-bottom-sheet-scrim--visible {
  opacity: 1;
  pointer-events: auto;
}

.vault-bottom-sheet {
  position: absolute;
  bottom: 0;
  left: 200px; /* exclude sidebar */
  right: 0;
  background: var(--bg-elevated, #161b22);
  border-top: 1px solid var(--border, #30363d);
  border-radius: 12px 12px 0 0;
  padding: 16px 24px 24px;
  transform: translateY(100%);
  transition: transform 0.25s ease;
  z-index: 101;
  max-height: 60vh;
  overflow-y: auto;
}

.vault-bottom-sheet--open { transform: translateY(0); }

.vault-bottom-sheet__handle {
  width: 40px;
  height: 4px;
  background: var(--border, #30363d);
  border-radius: 2px;
  margin: 0 auto 16px;
}

.vault-bottom-sheet__title {
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 16px;
  text-align: center;
}

.vault-type-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 10px;
}

.vault-type-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 12px 8px;
  background: var(--bg-page, #0d1117);
  border: 1px solid var(--border, #30363d);
  border-radius: 8px;
  cursor: pointer;
  transition: border-color 0.15s;
  gap: 6px;
}

.vault-type-card:hover { border-color: var(--gold, #b8860b); }

.vault-type-card__icon { font-size: 28px; }
.vault-type-card__name { font-size: 11px; color: var(--text-muted, #8b949e); }

/* Drawer header and body */
.vault-drawer__header {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border, #30363d);
  gap: 8px;
}

.vault-drawer__type-pill {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 2px 8px;
  background: var(--bg-page, #0d1117);
  border: 1px solid var(--border, #30363d);
  border-radius: 4px;
  color: var(--text-muted, #8b949e);
}

.vault-drawer__actions { display: flex; gap: 6px; margin-left: auto; }

.vault-drawer__close {
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 16px;
  color: var(--text-muted, #8b949e);
  padding: 4px 6px;
}

.vault-drawer__body { padding: 20px 20px 16px; }

.vault-drawer__title  { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
.vault-drawer__subtitle { font-size: 12px; color: var(--text-muted, #8b949e); margin-bottom: 16px; }

.vault-drawer__field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.vault-drawer__field-grid > .vault-drawer__field--full { grid-column: 1 / -1; }

.vault-drawer__field-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted, #8b949e);
  margin-bottom: 2px;
}

.vault-drawer__field-value {
  font-size: 13px;
  word-break: break-all;
}

/* === Responsive === */
@media (max-width: 960px) {
  .vault-drawer {
    position: absolute;
    right: 0;
    top: 0;
    height: 100%;
  }
}

@media (max-width: 720px) {
  .vault-sidebar {
    width: 48px;
    min-width: 48px;
  }
  .vault-sidebar__category-label,
  .vault-sidebar__category-count,
  .vault-sidebar__nav-label {
    display: none;
  }
  .vault-sidebar__nav-item { justify-content: center; padding: 10px 0; }
}
```

- [ ] **Step 2: Commit**

```bash
git add extension/src/vault/vault.css
git commit -m "feat(ext/vault): 3-column layout CSS — drawer, bottom sheet, list rows, responsive"
```

---

### Task 9: `vault.ts` — 3-column shell + sidebar category nav

**Files:**
- Modify: `extension/src/vault/vault.ts`

This is the largest task. Rewrite `renderShell()`, `renderSidebarList()`, and add the list pane renderer.

- [ ] **Step 1: Add `drawerOpen` and `bottomSheetOpen` to VaultState**

In the `VaultState` interface, add:

```ts
drawerOpen: boolean;
bottomSheetOpen: boolean;
```

In the initial state object, add:

```ts
drawerOpen: false,
bottomSheetOpen: false,
```

- [ ] **Step 2: Rewrite `renderShell()` for 3-column layout**

Replace the existing `renderShell()` function:

```ts
function renderShell(app: HTMLElement): void {
  if (!app.querySelector('.vault-shell')) {
    app.innerHTML = `
      <div class="vault-shell">
        <div class="vault-sidebar">
          <div class="vault-sidebar__header">
            <img class="brand-logo" src="icons/relicario-logo-16.svg" alt="">
            <span class="brand">Relicario</span>
          </div>
          <div class="vault-sidebar__search">
            <input type="text" id="vault-search" placeholder="/ search…" />
          </div>
          <nav class="vault-sidebar__categories" id="vault-categories" aria-label="Item types"></nav>
          <div class="vault-sidebar__nav">
            <button class="vault-sidebar__nav-item" data-nav="add" title="New item">+ new item</button>
            <button class="vault-sidebar__nav-item" data-nav="trash" title="Trash">${GLYPH_TRASH} <span class="vault-sidebar__nav-label">trash</span></button>
            <button class="vault-sidebar__nav-item" data-nav="settings" title="Settings">${GLYPH_SETTINGS} <span class="vault-sidebar__nav-label">settings</span></button>
            <button class="vault-sidebar__nav-item" data-nav="lock" title="Lock">${GLYPH_LOCK} <span class="vault-sidebar__nav-label">lock</span></button>
          </div>
        </div>
        <div class="vault-list-pane" id="vault-list-pane"></div>
        <div class="vault-drawer" id="vault-drawer"></div>
        <div class="vault-bottom-sheet-scrim" id="vault-sheet-scrim"></div>
        <div class="vault-bottom-sheet" id="vault-bottom-sheet"></div>
      </div>
    `;
    wireSidebar();
    wireBottomSheet();
  }

  renderSidebarCategories();
  renderListPane();
  if (state.drawerOpen && state.selectedItem) {
    renderDrawer(state.selectedItem);
  }
}
```

**Note:** The `⌬ devices` nav button is intentionally removed here. Once Stream B's Security section lands, devices are accessible via Settings → Security. Until then, users can access devices via the old popup route (still present). Confirm with PM before removing if uncertain.

- [ ] **Step 3: Rewrite `renderSidebarList()` → `renderSidebarCategories()`**

Rename the function and change it to show type sections with counts (not per-item rows):

```ts
function renderSidebarCategories(): void {
  const container = document.getElementById('vault-categories');
  if (!container) return;

  const filtered = getFilteredEntries();
  const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp'];

  const allCount = filtered.length;
  const isAllActive = !state.activeGroup && state.view === 'list';

  let html = `
    <button class="vault-category-row ${isAllActive ? 'vault-category-row--active' : ''}" data-group="">
      <span class="vault-category-row__icon">◈</span>
      <span class="vault-category-row__label">All items</span>
      <span class="vault-category-row__count">${allCount}</span>
    </button>
  `;

  for (const t of typeOrder) {
    const count = filtered.filter(([, e]) => e.type === t).length;
    if (count === 0 && allCount > 0) continue; // hide empty sections unless vault is empty
    const isActive = state.activeGroup === t;
    html += `
      <button class="vault-category-row ${isActive ? 'vault-category-row--active' : ''}" data-group="${t}">
        <span class="vault-category-row__icon">${typeIcon(t)}</span>
        <span class="vault-category-row__label vault-sidebar__category-label">${typeLabel(t)}</span>
        <span class="vault-category-row__count vault-sidebar__category-count">${count}</span>
      </button>
    `;
  }

  container.innerHTML = html;

  container.querySelectorAll<HTMLButtonElement>('.vault-category-row').forEach((btn) => {
    btn.addEventListener('click', () => {
      state.activeGroup = btn.dataset.group || null;
      state.drawerOpen = false;
      state.selectedId = null;
      state.selectedItem = null;
      renderSidebarCategories();
      renderListPane();
      closeDrawer();
    });
  });
}
```

Add to vault.css (after Task 8):

```css
.vault-category-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 12px;
  background: transparent;
  border: none;
  cursor: pointer;
  color: inherit;
  font-size: 13px;
  text-align: left;
}

.vault-category-row:hover { background: var(--bg-hover, #161b22); }
.vault-category-row--active { background: var(--bg-selected, #1c2d41); }
.vault-category-row__icon { font-size: 14px; flex-shrink: 0; }
.vault-category-row__label { flex: 1; }
.vault-category-row__count { font-size: 11px; color: var(--text-muted, #8b949e); }
```

- [ ] **Step 4: Add `renderListPane()`**

```ts
function renderListPane(): void {
  const pane = document.getElementById('vault-list-pane');
  if (!pane) return;

  const group = state.activeGroup as ItemType | null;
  let items = getFilteredEntries();
  if (group) items = items.filter(([, e]) => e.type === group);

  if (items.length === 0) {
    pane.innerHTML = `
      <div class="empty-state">
        <span class="empty-state__icon" aria-hidden="true">${state.searchQuery ? '⊘' : '◈'}</span>
        <div class="empty-state__title">${state.searchQuery ? `No results for "${escapeHtml(state.searchQuery)}"` : 'No items yet'}</div>
        <div class="empty-state__hint">${state.searchQuery ? 'Try a shorter search term.' : 'Click + new item to get started.'}</div>
      </div>
    `;
    return;
  }

  pane.innerHTML = items.map(([id, e]) => {
    const sel = id === state.selectedId ? ' vault-list-row--selected' : '';
    const subtitle = e.icon_hint ?? (e.tags.length > 0 ? e.tags.join(', ') : '');
    const modifiedAgo = e.modified ? relativeTime(e.modified) : '';
    return `
      <div class="vault-list-row${sel}" data-id="${escapeHtml(id)}">
        <div class="vault-list-row__icon" aria-hidden="true">${typeIcon(e.type)}</div>
        <div class="vault-list-row__text">
          <div class="vault-list-row__title">${escapeHtml(e.title)}</div>
          ${subtitle ? `<div class="vault-list-row__subtitle">${escapeHtml(subtitle)}</div>` : ''}
        </div>
        ${modifiedAgo ? `<div class="vault-list-row__age">${escapeHtml(modifiedAgo)}</div>` : ''}
      </div>
    `;
  }).join('');

  pane.querySelectorAll<HTMLElement>('.vault-list-row').forEach((row) => {
    row.addEventListener('click', async () => {
      await selectItemForDrawer(row.dataset.id!);
    });
  });
}

function relativeTime(unixSec: number): string {
  const diffS = Math.floor(Date.now() / 1000) - unixSec;
  if (diffS < 60) return 'just now';
  if (diffS < 3600) return `${Math.floor(diffS / 60)}m ago`;
  if (diffS < 86400) return `${Math.floor(diffS / 3600)}h ago`;
  return `${Math.floor(diffS / 86400)}d ago`;
}
```

- [ ] **Step 5: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 6: Commit**

```bash
git add extension/src/vault/vault.ts extension/src/vault/vault.css
git commit -m "feat(ext/vault): 3-column shell — sidebar category nav + list pane"
```

---

### Task 10: `vault.ts` — detail drawer

**Files:**
- Modify: `extension/src/vault/vault.ts`

- [ ] **Step 1: Add `selectItemForDrawer()` and drawer render/close functions**

```ts
async function selectItemForDrawer(id: ItemId): Promise<void> {
  const resp = await sendMessage({ type: 'get_item', id });
  if (!resp.ok) return;
  const data = resp.data as { item: Item };
  state.selectedId = id;
  state.selectedItem = data.item;
  state.drawerOpen = true;
  renderSidebarCategories();
  renderListPane();
  renderDrawer(data.item);
  openDrawer();
}

function openDrawer(): void {
  document.getElementById('vault-drawer')?.classList.add('vault-drawer--open');
}

function closeDrawer(): void {
  state.drawerOpen = false;
  state.selectedId = null;
  state.selectedItem = null;
  document.getElementById('vault-drawer')?.classList.remove('vault-drawer--open');
}

function renderDrawer(item: Item): void {
  const drawer = document.getElementById('vault-drawer');
  if (!drawer) return;

  const coreFields = getDrawerCoreFields(item);

  drawer.innerHTML = `
    <div class="vault-drawer__header">
      <span class="vault-drawer__type-pill">${item.type.replace('_', ' ').toUpperCase()}</span>
      <div class="vault-drawer__actions">
        <button class="btn" id="drawer-edit-btn" style="font-size:11px;">edit</button>
        <button class="vault-drawer__close" id="drawer-close-btn" title="Close (Esc)">✕</button>
      </div>
    </div>
    <div class="vault-drawer__body">
      <div class="vault-drawer__title">${escapeHtml(item.title)}</div>
      ${item.core && 'url' in item.core ? `<div class="vault-drawer__subtitle">${escapeHtml((item.core as { url?: string }).url ?? '')}</div>` : ''}
      <div class="vault-drawer__field-grid">
        ${coreFields.map(([label, value, full]) => `
          <div class="vault-drawer__field${full ? ' vault-drawer__field--full' : ''}">
            <div class="vault-drawer__field-label">${escapeHtml(label)}</div>
            <div class="vault-drawer__field-value">${escapeHtml(value)}</div>
          </div>
        `).join('')}
      </div>
    </div>
  `;

  document.getElementById('drawer-close-btn')?.addEventListener('click', () => {
    closeDrawer();
    renderListPane();
  });

  document.getElementById('drawer-edit-btn')?.addEventListener('click', () => {
    setHash('edit', state.selectedId!);
    renderPane();
  });
}

function getDrawerCoreFields(item: Item): Array<[string, string, boolean]> {
  // Returns [label, value, fullWidth] tuples for the core type fields
  const core = item.core as Record<string, unknown>;
  if (!core) return [];
  const fields: Array<[string, string, boolean]> = [];
  if ('username' in core) fields.push(['username', String(core.username ?? ''), false]);
  if ('password' in core) fields.push(['password', '••••••••', false]);
  if ('url' in core)      fields.push(['url', String(core.url ?? ''), true]);
  if ('number' in core)   fields.push(['number', String(core.number ?? ''), false]);
  if ('expiry' in core)   fields.push(['expiry', String(core.expiry ?? ''), false]);
  if ('cardholder' in core) fields.push(['cardholder', String(core.cardholder ?? ''), true]);
  if (item.notes)         fields.push(['notes', item.notes, true]);
  return fields;
}
```

- [ ] **Step 2: Add Esc key handler to close drawer**

In `wireSidebar()` or the shell setup, add:

```ts
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && state.drawerOpen) {
    closeDrawer();
    renderListPane();
  }
});
```

- [ ] **Step 3: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/vault/vault.ts
git commit -m "feat(ext/vault): detail drawer — open/close state + core fields display"
```

---

### Task 11: `vault.ts` — bottom sheet for new item type picker

**Files:**
- Modify: `extension/src/vault/vault.ts`

- [ ] **Step 1: Add `wireBottomSheet()` and sheet open/close functions**

```ts
const BOTTOM_SHEET_TYPES: Array<{ type: ItemType; label: string }> = [
  { type: 'login',       label: 'Login' },
  { type: 'secure_note', label: 'Secure Note' },
  { type: 'totp',        label: 'TOTP' },
  { type: 'card',        label: 'Card' },
  { type: 'identity',    label: 'Identity' },
  { type: 'key',         label: 'SSH / API Key' },
  { type: 'document',    label: 'Document' },
];

function openBottomSheet(): void {
  const sheet = document.getElementById('vault-bottom-sheet')!;
  const scrim = document.getElementById('vault-sheet-scrim')!;

  sheet.innerHTML = `
    <div class="vault-bottom-sheet__handle"></div>
    <div class="vault-bottom-sheet__title">New item — choose type</div>
    <div class="vault-type-grid">
      ${BOTTOM_SHEET_TYPES.map((t) => `
        <button class="vault-type-card" data-type="${t.type}">
          <span class="vault-type-card__icon" aria-hidden="true">${typeIcon(t.type)}</span>
          <span class="vault-type-card__name">${escapeHtml(t.label)}</span>
        </button>
      `).join('')}
    </div>
  `;

  sheet.classList.add('vault-bottom-sheet--open');
  scrim.classList.add('vault-bottom-sheet-scrim--visible');
  state.bottomSheetOpen = true;

  sheet.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const type = btn.dataset.type as ItemType;
      closeBottomSheet();
      state.newType = type;
      setHash('add', type);
      renderPane();
    });
  });
}

function closeBottomSheet(): void {
  document.getElementById('vault-bottom-sheet')?.classList.remove('vault-bottom-sheet--open');
  document.getElementById('vault-sheet-scrim')?.classList.remove('vault-bottom-sheet-scrim--visible');
  state.bottomSheetOpen = false;
}

function wireBottomSheet(): void {
  document.getElementById('vault-sheet-scrim')?.addEventListener('click', closeBottomSheet);
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && state.bottomSheetOpen) closeBottomSheet();
  });
}
```

- [ ] **Step 2: Update the `data-nav="add"` handler to open the sheet**

In `wireSidebar()`, find the `nav === 'add'` branch and change it to call `openBottomSheet()` instead of directly calling `setHash('add')`:

```ts
if (nav === 'add') {
  openBottomSheet();
  return;
}
```

- [ ] **Step 3: Build and run tests**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | tail -10
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/vault/vault.ts
git commit -m "feat(ext/vault): bottom sheet type picker for new item"
```

---

### Task 12: Wire settings component (interface contract with DEV-B)

**Files:**
- Modify: `extension/src/vault/vault.ts`

- [ ] **Step 1: Add settings import**

Add to vault.ts imports:

```ts
import { renderSettings, teardownSettings } from '../popup/components/settings';
```

- [ ] **Step 2: Wire in `renderPane()` for the settings view**

Find the `renderPane()` function and ensure the `settings` case calls `renderSettings`:

```ts
case 'settings':
  teardownSettings();
  await renderSettings(pane);
  return;
```

And ensure `teardownSettings()` is called when navigating away from settings (wherever view transitions happen).

- [ ] **Step 3: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
```

If DEV-B's settings.ts doesn't yet export `renderSettings` / `teardownSettings`, add a temporary stub to `settings.ts` on this branch to unblock compilation — note it clearly in a comment for DEV-B. This stub will be replaced at merge time.

- [ ] **Step 4: Commit**

```bash
git add extension/src/vault/vault.ts
git commit -m "feat(ext/vault): wire renderSettings / teardownSettings from settings component"
```

---

### Task 13: Full build + test pass

- [ ] **Step 1: Run all tests**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | tail -20
```

Expected: all existing tests pass. If any fail, fix before proceeding.

- [ ] **Step 2: Build both targets**

```bash
bun run build 2>&1 | tail -10
bun run build:firefox 2>&1 | tail -10
```

Expected: both build clean (only pre-existing bundle-size warnings).

- [ ] **Step 3: Grep for remaining emoji in extension/src/**

```bash
grep -rn '\U0001F\|🔑\|📝\|🪪\|💳\|🗝\|📄\|⏱️\|🖥\|🔐\|📎' /home/alee/Sources/relicario.v0.5.1-stream-a/extension/src/ 2>/dev/null
```

Expected: no output (all emoji replaced with glyphs).

- [ ] **Step 4: Commit any remaining fixes, then open PR**

```bash
gh pr create --title "feat: fullscreen 3-column layout + popup polish (Stream A)" --base main
```

- [ ] **Step 5: Post status to PM**

Call `post_message(from="dev-a", to="pm", kind="status", body="...")` with:

```
## STATUS UPDATE — DEV-A
Time: <iso8601>
Task: 13 of 13
Status: REVIEW-READY
Summary: All 13 tasks complete. PR open. Build clean. All tests pass. No emoji remaining in extension/src/.
Next: waiting for PM
```
