diff --git a/CLAUDE.md b/CLAUDE.md
index 06f22ed..fd2d8df 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -115,11 +115,33 @@ server/
calls.js ← CRUD for market calls + GET /api/calls/calendar (earnings/dividend events)
ui/ ← SvelteKit dashboard (lives inside this repo, not a separate repo)
- src/routes/
- +page.svelte ← main screener UI
- calls/ ← market calls list + detail views
- portfolio/ ← portfolio advice view
- safe-buys/ ← filtered strong-buy view
+ src/
+ styles/ ← global SCSS design-token system (Phase 4)
+ app.scss ← root file — @use all partials
+ _tokens.scss ← CSS custom properties generated from SCSS maps ($bg, $text, $blues, $signals…)
+ _reset.scss ← box-sizing reset + body base
+ _layout.scss ← shell, nav, .links, main, nav-progress, .loading-area
+ _section.scss ← .section, .section-header, .count, .mode-tabs, .error-banner
+ _table.scss ← table, thead/tbody, .table-wrap, .col-ticker, .ticker, .num, .tag
+ _buttons.scss ← button base, .btn-primary, .btn-catalyst, .btn-ghost, .btn-screen, .btn-analyze
+ _badges.scss ← .verdict-pill, .sentiment-pill, .text-* helpers (SCSS @each maps)
+ lib/
+ api.js ← typed fetch wrappers for all API routes
+ utils.ts ← shared pure functions: sigOrd, sorted, verdictShort, vClass, fmtPE, fmt…
+ MarketContext.svelte ← collapsible card-grid context (used in portfolio + safe-buys)
+ MarketContextStrip.svelte ← horizontal chip strip (used in screener — Phase 5)
+ AssetTable.svelte ← STOCK/ETF/BOND section: mode tabs + Analyze + table (Phase 5)
+ AnalysisSidebar.svelte ← LLM analysis slide-over panel (Phase 5)
+ VerdictPill.svelte ← verdict-pill span; props: label (Phase 5)
+ SignalBadge.svelte ← signal emoji + label badge
+ Spinner.svelte ← sm: dot-pulse | md/lg: chart-line animation
+ routes/
+ +page.js ← SvelteKit load (ssr:false) — fetches catalysts + screens on mount (Phase 5)
+ +page.svelte ← main screener UI (~230 lines after Phase 5 decomposition)
+ +layout.svelte ← shell, nav, nav-progress bar, nav-overlay with Spinner
+ calls/ ← market calls list + detail views
+ portfolio/ ← portfolio advice view
+ safe-buys/ ← filtered strong-buy view
market-calls.json ← persisted market thesis calls (written by MarketCallStore)
portfolio.json ← user's holdings: ticker, shares, costBasis, source, type
@@ -358,6 +380,12 @@ Test output: silent on pass, shows only failures + one summary line (`scripts/su
**Key unit:** `ytm` in `Bond.metrics` is stored as a percentage (e.g. `6.5` = 6.5%). `BondScorer._sanitize` divides by 100 before spread calculation.
+**Coverage gaps (known):**
+- `MarketCallStore.js` — no tests; CRUD against `market-calls.json` is untested
+- `LLMAnalyst.test.js` — tests a local copy of the fence-stripping regex rather than importing from source; will silently drift if the regex changes
+- API routes (`server/server/routes/`) — no integration tests; covered implicitly by manual testing only
+- UI components — not tested at the unit level (Phases 4–5 are pure UI; no server logic changed)
+
---
## Conventions
@@ -412,28 +440,31 @@ All items completed. Additional features delivered alongside cleanup:
- Renamed `src/` to `server/` — `src/server/` is now `server/server/`
- Updated all import paths in `bin/`, `tests/`, and `CLAUDE.md`
-### Phase 4 — SCSS Migration
-Replace per-component `
diff --git a/ui/src/lib/AssetTable.svelte b/ui/src/lib/AssetTable.svelte
new file mode 100644
index 0000000..a36f361
--- /dev/null
+++ b/ui/src/lib/AssetTable.svelte
@@ -0,0 +1,108 @@
+
+
+{type}S
+ {rows.length}
+
+
+
+
+
+
+
+
+ {#each sorted(rows) as r}
+ {@const m = r.asset.displayMetrics ?? {}}
+ {@const v = r[mode]}
+ Ticker
+ Price
+ Verdict
+ Score
+ {#if type === 'STOCK'}
+ Sector
+ P/E PEG ROE%
+ OpMgn% FCF% D/E
+ Flags
+ {:else if type === 'ETF'}
+ Expense Yield AUM 5Y Ret
+ {:else}
+ YTM Duration Rating
+ {/if}
+
+
+ {/each}
+
+ {r.asset.ticker}
+ {m.Price ?? '—'}
+
+ {v.scoreSummary}
+ {#if type === 'STOCK'}
+ {m.Sector ?? '—'}
+ {m['P/E'] ?? '—'}
+ {m['PEG'] ?? '—'}
+ {m['ROE%'] ?? '—'}
+ {m['OpMgn%'] ?? '—'}
+ {m['FCF Yld%'] ?? '—'}
+ {m['D/E'] ?? '—'}
+
+ {#each v.audit?.riskFlags ?? [] as flag}
+ ⚠ {flag}
+ {/each}
+
+ {:else if type === 'ETF'}
+ {m['Exp Ratio%'] ?? '—'}
+ {m['Yield%'] ?? '—'}
+ {m['AUM'] ?? '—'}
+ {m['5Y Return%'] ?? '—'}
+ {:else}
+ {m['YTM%'] ?? '—'}
+ {m['Duration'] ?? '—'}
+ {m['Rating'] ?? '—'}
+ {/if}
+