> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cfosilvia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MODULUS CLEANUP PLAN

# Legacy UI removal — data and plan

Audit run against `origin/staging` @ `a22abb5b16`, 2026-09-18. The counts below
are a snapshot of that commit; re-measure before acting on any one of them
rather than trusting a number that is weeks old.

**Ground rule: `components/modulus/**` is the new system and is not touched by
any of this.** That includes the ten components built but not yet adopted
(§3b) and `components/modulus/foundations/blocks.tsx`, which the reachability
sweep flags and which is excluded on purpose. This project retires OLD
components only.

**Scope rule, set 2026-09-22: UI only.** This cleanup deletes UI components,
pages, and the stories and tests that exist only for them. It does **not**
delete logic — no hooks, `lib/`, `app/api/`, email templates, messaging senders,
chat tools, or cookie/flag helpers, even when nothing imports them. A logic
file orphaned by a UI deletion stays where it is. The original PR 7 (dead lib,
hooks and API) is dropped for this reason.

Progress: **PRs 1–3 are done.** Everything else in §4 is still proposed.

### Re-audit, 2026-09-22 (against `staging` @ `304bd8792a`, 136 commits later)

The plan was re-checked against the repo rather than re-read, with the blind
spots of the first pass closed: `.mdx` files are now scanned, `studio/` and the
vitest configs are seeded as entry points, and every "nothing found" result is
paired with a control that must find something. What it changed:

* **Two files the plan called dead are deliberate.** `MoveHoldingsDialog` is the
  last unwired piece of a Storybook-first feature (`b6782fea53`: *"before any
  data wiring lands"*); its siblings from that commit have all since been wired.
  It stays, and so does its review harness. `ReferralProgress` was parked by
  Robert (`6d284925af`: *"kept for future re-enablement"*); Felipe decided on
  2026-09-22 to delete it anyway, so the component remains scheduled for
  deletion in PR 5. Its data hook, `use-referral-stats`, is **not** deleted
  (scope rule).
* **A fresh sweep would delete a live experiment arm.** `components/landing-v5/`
  is Shain's Landing V5, created 2026-09-21. On the re-audit commit it was not
  routed on `staging` yet; it has been since (`4a5935e8c8`), and it is **live in
  production as `test-b`** of the three-arm flag `landing-page-conversion-v02`.
  Even routed, 51 of its 103 files still *read* as unreferenced: the
  Framer-generated tree under `landing-v5/framer/` imports itself with `.js`
  specifiers that resolve to `.jsx`/`.ts` files, which the reachability scan did
  not map (corrected 2026-09-23, see §5). **Nothing under `landing-v5/` is ever
  a deletion candidate.**
* **Five of Shain's open PRs edit files on the old delete list.** With PR 7
  dropped, only two UI files are still affected: `footer.tsx` (#2450, #1764,
  both months stale) and `security-faq.tsx` (#3562, the MX epic). Both are held
  in PR 6 until he confirms.
* **PR 8 conflicts with the PR 1 rule** — see PR 8.
* **Independent confirmation of PR 3:** `tsconfig.json` already excludes its
  seven landing files with the note *"delete in a follow-up cleanup"*; they use
  removed Clerk v6 APIs and no longer compile.

Recency alone is a noisy signal here: most "recent" touches on dead files come
from repo-wide sweeps (#4119's lint grandfathering stamped 216 files, the
rebrand, the email-redaction fix). What matters is the last *substantive*
commit, and whether its message says the file was parked on purpose.

***

## 1. The four facts that shape everything

**The app shell is unconditionally Modulus.** `app/layout-with-sidebar-client.tsx:139`
stamps `data-theme-variant='modulus'` with no flag and no condition. So inside
`app/(app)/**` there is no control arm to protect, and legacy components there are
dead weight rather than a live variant.

**But the root layout's `themeVariant` is a *different*, still-live experiment.**
`app/layout.tsx:137` writes `control` / `dark-variant-b` from `resolveThemeVariant`.
That is the dark-theme test, not the Modulus one. Do not conflate them; removing
legacy components must not touch `app/themes.css`, which those rules key on.

**Modulus is already fully independent of `components/ui`.** Zero files under
`components/modulus/` import anything from `components/ui/` (verified with a control
search that does return `@/components/chat/` ×3, so the empty result is real). The
only mention is prose in `components/modulus/CLAUDE.md`. Nothing about deleting
legacy UI can break Modulus itself.

**`components/ui/**` used to be declared read-only, and PR 1 narrowed that.**
`CLAUDE.md` now separates "Frozen. Do not edit" (`themes.css`, `globals.css`,
the two `tailwind.config.ts` keys) from "Being retired. Deleting is allowed,
editing still is not" (`components/ui/**` and the non-Modulus stories). The
rule was rewritten rather than ignored, because a rule that is quietly worked
around gets re-frozen by the next reader.

Deleting is allowed; **restyling or extending one of these components is still
a stop-and-ask**, and that half of the freeze is not a formality: 19 files
under `app/(public)` still import `components/ui`, only 12 of its 64 modules
have a Modulus counterpart, and the landing A/B is still running.

***

## 2. Which components are genuinely duplicated

Counts are **production importers only** (stories and tests excluded).

| Modulus | in use | Legacy counterpart | in use | Migrated |
| - | - | - | - | - |
| `ModulusIconButton` | 45 | `edit-icon-button` + `delete-icon-button` | 7 | \~87% |
| `ModulusSegmented` | 15 | `segmented-tabs` | 2 | \~88% |
| `ModulusEmptyState` | 4 | `empty-state-grid` | 2 | \~67% |
| `ModulusDrawer` | 2 | `drawer` | 1 | \~67% |
| `ModulusButton` | 91 | `button` | **205** | \~31% |
| `ModulusTag` / `ModulusChip` | 9 | `badge` | 20 | \~31% |
| `ModulusCheckbox` | 2 | `checkbox` | 17 | \~11% |
| `ModulusCard` | 7 | `card` | **77** | \~8% |
| `ModulusTextTab` | 1 | `tabs` | 14 | \~7% |
| `ModulusTextField` | 6 | `input` + `textarea` | **91** | \~6% |
| `ModulusOptionList` | 1 | `select` | 43 | \~2% |
| `ModulusModalPicker` | 1 | `dialog` | **107** | \~1% |

**The honest headline: only 12 of 64 `components/ui` modules have a Modulus
counterpart at all.** The other 52 are Radix primitives and product widgets that
Modulus never set out to replace — `label` (59), `skeleton` (48), `table` (27),
`sensitive-text` (23), `dropdown-menu` (19), `tooltip` (17), `alert-dialog` (17),
`popover` (16), and so on. Those are not deletion targets; they are simply the
rest of the design system.

Two of the pairs are also not like-for-like, and should not be treated as a rename:

* `ModulusOptionList` is a menu list, not a form `<select>` with a native
  value/validation contract.
* `ModulusModalPicker` is a picker. `dialog` is the generic Radix modal behind 107
  call sites. **Modulus has no general-purpose dialog**, so "delete `ui/dialog`" is
  blocked on designing one, not on migration effort.

`ui/drawer` looks nearly dead (1 importer) but is not independently removable: its
only consumer is `components/ui/responsive-dialog.tsx`, which has 4 of its own.

***

## 3. Dead files across the codebase

Reachability run from 702 real entry points (Next.js route conventions, middleware,
instrumentation, configs, `.storybook/`, `scripts/`, `supabase/`): **3376 files
scanned, 2240 reachable.**

| Bucket | Count | Meaning |
| - | - | - |
| Fully unreachable | **116** | no production, story or test path reaches it |
| Alive only via a story | **35** | product-dead; only a Storybook entry holds it up |
| Alive only via a test | **10** | product-dead; only its own test holds it up |

By directory: `components` 52, `app` 26, `lib` 17, `hooks` 13, rest 8.

Clear clusters in the unreachable set:

* **Superseded landing pages** — `landing-page-client.tsx`, `-client-old`,
  `-original`, `-variant`, `-variant2`, `app/(home)/old-page.tsx`, `old2-page.tsx`
* **Pre-revamp radar and vault** — `app/(app)/radar/components/{create-form,
  radar-header,radar-landing,radar-sidebar,schedule-card,schedule-list,
  template-gallery}`, `app/(app)/vault/components/{batch-upload-document,
  document-list,folder-list,upload-document}`
* **Retired chat SQL tooling** — `app/api/chat/tools/{index,sql-query-tool,
  text-to-sql-tool,transaction-query-tool}`, `app/api/chat/utils/chat-logger`
* **Dashboard cards and banners** — `net-worth-card`, `smart-alerts-card`,
  `portfolio-brief-card`, `office-hours-banner`, `pfs-announcement-banner`,
  `product-updates-banner`, `referral-popup`, `pwa-install-prompt`
* **Orphaned hooks** — 13 under `hooks/mutations` and `hooks/queries` whose only
  consumers were the components above

### This list is NOT safe to bulk-delete, and I can prove it

My first pass reported 119 unreachable files. It was **wrong about 14 of them**,
because the regex only matched `from '…'`, `import('…')` and `require('…')` — and
missed bare side-effect imports. `lib/generative-ui/renderer.tsx:9` and
`lib/email/gen-ui-to-html.ts:40` both do:

```ts theme={null}
import '@/components/chat/generative/register'
```

That one line keeps every generative-UI component alive. Deleting on the first
pass would have removed live chat and email rendering code.

The regex is fixed, and two further known false positives remain in the 116 by
construction: `vitest.setup.ts` and `vitest.financial-literacy.config.ts` (loaded
by config, never imported) and `types/global.d.ts` (ambient, picked up by
`tsconfig`, never imported). Two more need a human call rather than a tool:
`emails/scenario-complete-email.tsx` sits in the outbound-messaging area, and
`hooks/use-experiment.ts` + `lib/experiments/registry.ts` are test-only right now
simply because no experiment is currently wired — `docs/EXPERIMENTS.md` mandates
that hook for future tests, so **unused here does not mean delete**.

***

## 3a. The 35 "story-only" files split three ways — only 18 are deletable

"Held up only by a story" is not one category. Resolving *which* story holds each
file up separates them cleanly.

### DELETE — 18 files, superseded product code

> **Corrected 2026-09-22:** `move-holdings-dialog` is not dead (unwired feature)
> and stays; `use-referral-stats` stays under the UI-only scope rule. See the
> re-audit at the top and PR 5.
> Each is held by exactly one story, and each has a live replacement in production:

| Cluster | Files | Replaced by |
| - | - | - |
| Radar (pre-revamp) | `create-radar-dialog`, `delete-radar-dialog`, `radar-empty-state`, `low-data-warning-card`, `radar-info-card`, `silvia-scanning-animation` | `RadarDayZero` (`radar-page-client.tsx:43`) |
| Investment / property dialogs | `add-investment-dialog`, `delete-investment-dialog`, `delete-property-dialog`, `disconnect-property-dialog`, `move-holdings-dialog` | current asset flows |
| Misc | `empty-state` | `ModulusEmptyState` (4 live uses) |
| | `feedback-modal` | `support-feedback-modal` (3 live uses) |
| | `net-worth-trend-chart`, `onboarding-step-progress`, `referral-progress`, `stock-chart-example` | — |
| Hook | `use-referral-stats` | falls out with `referral-progress` |

The radar four are one tranche: `radar-empty-state.stories.tsx` alone holds
`low-data-warning-card`, `radar-info-card` and `silvia-scanning-animation`, so that
single story file is the whole cluster's anchor.

Only **one** of the 18 has a test: `components/__tests__/onboarding-step-progress.test.tsx`.

### KEEP — 10 Modulus components (design-system inventory)

`account-row`, `info-panel`, `logo-icon`, `logo-wordmark`, `mobile-app-bar`,
`modal-showcase`, `nav-item`, `screen-shell`, `sidebar`, `thread`.

Eight of the ten are held by **several** stories, not one: `logo-icon` and `nav-item`
by 6, `sidebar`, `account-row` and `mobile-app-bar` by 5, `screen-shell` by 4. They
are the pieces that **compose the full-screen design references** — `chat.stories`,
`radar-page.stories`, `research.stories`. Deleting them breaks the Storybook screens
that this project treats as the source of truth.

### KEEP — 7 story fixtures and harnesses

`portfolio-story-fixtures` (5 holders), `silvia-feed.mocks` (5),
`public-investments-{prod,story}-fixture`, `investment-accounts/stories/{fixtures,
flow-harness,mock-securities}`.

These are story *support* files. Being story-only is their job, not a symptom, and
most serve stories of components that are very much alive (`portfolio-section`,
`public-investments-detail-screen`, `insights-widget`).

One exception worth a look: `investment-account-flow.stories.tsx` imports only
`./flow-harness` and `./fixtures` and no production component at all, so that story
plus its harness is a closed loop documenting a mock rather than the product. Worth
a decision, not an automatic delete.

***

## 3b. Duplication also runs the other way: 10 Modulus components are unadopted

These exist in `components/modulus/` with stories, and are reached by **nothing in
production**:

`account-row`, `info-panel`, `logo-icon`, `logo-wordmark`, `mobile-app-bar`,
`modal-showcase`, `nav-item`, `screen-shell`, `sidebar`, `thread`.

The important one is **`ModulusSidebar`**: it is story-only, while the sidebar the
app actually renders is `components/sidebar.tsx`. Same for `ModulusThread` and
`ModulusMobileAppBar` against their shipped equivalents. So for these the duplicate
pair is real but inverted — the legacy file is the live one and the Modulus file is
the shelf-ware.

**Do not delete these as "dead code".** They are design-system inventory and the
intended replacements. They belong in the Tranche 4 conversation (adopt them) rather
than the Tranche 3 one (delete unreachable files). Flagging them because a naive
reachability sweep puts them in the same bucket as `old2-page.tsx`, and they are not
the same thing at all.

***

## 4. The plan, as PRs

Ground rule agreed 2026-09-18: **anything under `components/modulus/` is new and is
not touched.** That includes the 10 unadopted components and
`components/modulus/foundations/blocks.tsx`, which shows up in the orphan sweep and
is excluded on purpose. This work only retires OLD components.

Ordering matters in two places: PR 1 unblocks everything, and the delete PRs come
before the migration PRs because deletes remove call sites the migrations would
otherwise have to do by hand.

***

### PR 1 — Narrow the two read-only rules (docs only) — **DONE**

* Split the one "Read-only. Do not edit these files" list into **Frozen. Do not
  edit** (`themes.css`, `globals.css`, the two `tailwind.config.ts` keys) and
  **Being retired. Deleting is allowed, editing still is not**
  (`components/ui/**`, non-Modulus `*.stories.tsx`).
* Deletion rights only. Restyling or extending a legacy component is still a
  stop-and-ask, because 19 `app/(public)` files still import `components/ui`,
  52 of its 64 modules have no Modulus counterpart, and the landing A/B is
  live. The freeze was protecting live surfaces, and most of them are still
  live.
* Added this plan to the repo and to the `AGENTS.md` docs index, so the rule
  points at something real.
* **Why first:** without it every later PR contradicts the repo's own
  instructions.

### PR 2 — Delete 6 unused demo components — **DONE**

* `components/ui/`: `aurora-background`, `credit-card-icon`, `dollar-icon`,
  `gooey-text-morphing`, `hero-highlight`, `lamp`.
* Verified on the commit it branched from: 0 absolute and 0 relative importers
  (controls: `button` 216 / `./button` 15), 0 stories, 0 tests, no config or doc
  references.
* **The one thing that looked like a user:** `app/(app)/subscriptions/page.tsx`
  contains `<DollarIcon />` twice and `<CreditCardIcon />` once. All three sit
  inside `{/* <Card> … </Card> */}` blocks (lines 245–386) — commented-out cards,
  so neither rendered nor type-checked. Whoever uncomments them will need an
  icon source; `lucide-react` has `CreditCard` and `DollarSign`.
* tsc error set identical before and after; full vitest suite green.
* **Size:** 6 files deleted, nothing edited. **Risk:** none.

### PR 3 — Delete superseded landing generations — **DONE**

* Deleted: `components/landing-page-{client,client-old,original,variant,variant2}.tsx`
  and `app/(home)/{old-page,old2-page,net-worth-chart,total-cash}.tsx`.
* **Stays (scope rule):** `hooks/use-new-landing-experiment.ts` (reads a PostHog
  flag) and `components/variant-persistence.tsx` (renders nothing; writes the
  `lp_experiment_variant` cookie). Neither had an importer even before this PR,
  and neither imports anything that was deleted.
* **Live path untouched:** `app/(home)/page.tsx` -> `home-landing-router` ->
  `LandingPageV3` / `LandingPageV4` / `LandingPageV5` — the three arms of
  `landing-page-conversion-v02`, which is live. Nothing deleted here is on any
  of those paths, by the resolver and by plain text search.
* **Deliberately NOT changed, because the experiment is live:** two comments in
  `components/landing-page-v3.tsx` (the control arm) and
  `hooks/use-landing-engagement.ts` still name `landing-page-variant2.tsx` as the
  deprecated predecessor. They are prose, not imports, and still read correctly
  as history. Editing the control arm mid-test for a comment is not worth it.
* **`tsconfig.json`: left for later.** Its seven `exclude` entries now name
  files that no longer exist, which is a no-op. They were meant to go in this
  PR, but open PR #4057 (technoplato) edits exactly those lines — it appends to
  the same array after `landing-page-variant2.tsx` — so removing them now would
  give one of the two PRs a merge conflict. **Remove them after #4057 lands.**
* `docs/AB-TEST-VARIANT-TRACKING-FIX.md` gains a status note: historical, the
  components it names are gone, and where the live experiment lives now.
* **Name trap still stands:** `use-landing-conversion-experiment` is LIVE;
  `use-new-landing-experiment` is the dead one.

### PR 4 — Delete pre-revamp radar and vault

* Radar orphans: `create-form`, `radar-header`, `radar-landing`, `radar-sidebar`,
  `schedule-card`, `schedule-list`, `template-gallery`
* Radar story-only + their stories: `create-radar-dialog`, `delete-radar-dialog`,
  `radar-empty-state`, `low-data-warning-card`, `radar-info-card`,
  `silvia-scanning-animation`
* Vault orphans: `batch-upload-document`, `document-list`, `folder-list`,
  `upload-document`
* All superseded by `RadarDayZero` (`radar-page-client.tsx:43`) and the current vault.
* **Bonus:** removes one of the three `segmented-tabs` call sites, shrinking PR 8.
* **Size:** \~23 files. **Risk:** low.

### PR 5 — Delete superseded dialogs and misc story-only

* `add-investment-dialog`, `delete-investment-dialog`, `delete-property-dialog`,
  `disconnect-property-dialog`. Live equivalents confirmed on 2026-09-22 —
  residential property delete lives in `add-property-dialog.tsx`
  (`deleteResidentialProperty`), commercial in `commercial-property-dialog.tsx`.
* `components/empty-state` (-> `ModulusEmptyState`), `components/feedback-modal`
  (-> `support-feedback-modal`)
* `net-worth-trend-chart`, `onboarding-step-progress`, `stock-chart-example`
* `referral-progress` — **deleted by decision** (Felipe, 2026-09-22), overriding
  Robert's parking in `6d284925af`. Say so in the PR so it is not a surprise;
  the component is recoverable from that commit. `hooks/queries/use-referral-stats`
  **stays** (scope rule), as does its `.storybook/main.ts` mock alias.
* Each takes its own `.stories.tsx`. One test goes too:
  `components/__tests__/onboarding-step-progress.test.tsx`.
* **REMOVED from this PR:** `investment-accounts/move-holdings-dialog` and the
  `investment-accounts/stories/` harness (`flow-harness`, `fixtures`,
  `mock-securities`, `investment-account-flow.stories.tsx`). Not dead — the
  unwired half of the holdings↔account feature.
* **Size:** \~10 components + \~10 stories + 1 test. **Risk:** low.

### PR 6 — Delete dead dashboard cards, banners and tables

* \~33 files under `components/`: `net-worth-card`, `smart-alerts-card`,
  `portfolio-brief-card`, `dashboard-investment-card`, `dashboard-silvia-feed`,
  `net-worth-breakdown-card`, `cash-accounts-{cards,table}`, `crypto-holdings-cards`,
  `loan-accounts-cards`, `public-investments-cards`, `office-hours-banner`,
  `pfs-announcement-banner`, `product-updates-banner`, `alert-banner`,
  `referral-popup`, `pwa-install-prompt`, `auth-dialog`, `app-header`, `footer`,
  `security-faq`, `silvia-showcase`, `testimonial-to-embed`, `sql-results`,
  `chart-renderer`, `section-skeleton`, `table-skeleton`, `sync-status-indicator`,
  `privacy-mode-toggle`, `investment-button`, `mobile-clickable-card`,
  `mobile-desktop-icon`, `transactions-sidebar-item`, `vault-sidebar-item`,
  `upload-thing-uploader`, `research/analysis-report`, `research/index`
* **Chains:** once these go, `components/ui/dynamic-chart` and
  `components/ui/skeleton-card` lose their last consumers and can go in the same PR.
* **Held until Shain confirms:** `footer.tsx` (edited by his open #2450 and
  \#1764, both untouched for months — likely abandoned, but his call) and
  `security-faq.tsx` (edited by #3562, the MX epic draft). Deleting either first
  hands him a modify/delete conflict.
* **Open:** `components/research/index.ts` only re-exports its sibling
  `analysis-report`; default is to delete it with that component.
* Hooks these components used (`use-portfolio-brief`, `use-smart-alerts`,
  `use-pwa-prompt-status`, …) **stay** under the scope rule.
* **Size:** \~33 files. Mechanical but the largest delete. **Risk:** low individually,
  worth splitting in two if review is unwieldy.

### PR 7 — **DROPPED** (2026-09-22)

Was: delete dead `lib/`, `hooks/`, `app/api/` and email/messaging files. Out of
scope under the UI-only rule — logic files are not deleted even when nothing
imports them. Nothing from it is scheduled anywhere else.

Two facts from its audit are still worth recording, because they are true
whether or not anything is deleted:

* **`AGENTS.md` says the project uses "Neo4j for graph relationships".** In this
  repo `lib/neo4j.ts` has never been imported by anything, no file mentions
  Neo4j or Cypher, and `neo4j-driver` is an unused dependency. Either that line
  describes a different service or it is stale — worth confirming.
* `lib/blog/freshness.ts` is documented in `CLAUDE.md`'s blog file map and in
  `docs/blog/seo-infrastructure.md` as live ("content staleness detection"),
  but nothing calls it.

***

Everything above deletes. Nothing above changes a rendered pixel.
Below this line the work becomes migration, and each PR edits live UI.

***

### PR 8 — `segmented-tabs` -> `ModulusSegmented` — **blocked on a rule decision**

* 2 live call sites after PR 4: `app/(app)/product-updates/page.tsx` and
  `detail-section.stories.tsx`. Then delete the module.
* **Conflicts with PR 1's rule.** `app/(app)/assets-liabilities/_components/
  detail/detail-section.stories.tsx` is outside `components/modulus/`, so it is
  still edit-frozen, and migrating it is an edit. Needs either a narrow
  exception in `CLAUDE.md` ("updating a story only to follow its component's
  migration is allowed") or a different approach.
* **Size:** 2 sites. **Risk:** low. Modulus is already at 15 uses vs 2.

### PR 9 — `empty-state-grid` -> `ModulusEmptyState`

* `components/assets-section.tsx`, `components/liabilities-section.tsx`, plus
  `components/__tests__/assets-section.test.tsx`. Then delete the module.
* **Size:** 3 files. **Risk:** low.

### PR 10 — `edit-icon-button` / `delete-icon-button` -> `ModulusIconButton`

* `app/(app)/real-estate/page.tsx`, `components/precious-metals-table.tsx`,
  `components/physical-assets-table.tsx` (relative import). Then delete both modules.
* **Size:** 3 sites. **Risk:** low. Modulus already at 45 uses vs 7.

### PR 11 — `drawer` + `responsive-dialog` (needs a design answer first)

* `ui/drawer`'s only consumer is `ui/responsive-dialog`, which has 4 live consumers
  in connect-account, sam and dialogs.
* `responsive-dialog` is "dialog on desktop, drawer on mobile". Modulus has
  `ModulusDrawer` and `ModulusPane` but no single responsive equivalent, so this
  needs a decision before code.
* **Size:** 4 sites + 2 modules. **Risk:** medium, blocked on design.

### PR 12-14 — the mid-size swaps, one per PR

* `badge` -> `ModulusTag` / `ModulusChip` (20 sites)
* `checkbox` -> `ModulusCheckbox` (17 sites)
* `tabs` -> `ModulusTextTab` (14 sites)

### PR 15-17 — the large swaps, each split across several PRs by directory

* `card` -> `ModulusCard` (77 sites)
* `input` + `textarea` -> `ModulusTextField` (91 sites)
* `button` -> `ModulusButton` (205 sites, of which 149 are shared `components/*.tsx`)

### Blocked, not scheduled

* `dialog` (107 sites) — Modulus has no general-purpose dialog. Needs design.
* `select` (43 sites) — `ModulusOptionList` is a menu, not a form select with a
  value and validation contract. Needs design.

## 5. Constraints that must hold in every tranche

**Check the scope before moving anything in `app/(public)/**`.** A Modulus class
outside a `data-theme-variant='modulus'` subtree resolves to nothing and renders
unstyled, with no error. Some public routes do carry the scope in their own layout
(`blog`, `careers`, the quizzes, calculators, financial-literacy) and some do not.
This is per-route, not global.

**Leave the Landing V4 surfaces alone** while that experiment is running.

**`*.stories.tsx` outside `components/modulus/`** may be deleted with their
component (PR 1) but not edited. Migrations that need to edit one are blocked
until that rule is settled — see PR 8.

**Before any delete PR, re-check every file on the commit you branch from.**
"Nothing imports it" is necessary but not sufficient. Each of these caught a
real problem on 2026-09-22:

1. **Reachability, with the blind spots closed.** Scan `.ts`, `.tsx`, `.js`,
   `.mjs` **and `.mdx`**; seed Next route files, middleware, instrumentation,
   every `*.config.*`, `vitest.setup.ts`, `.storybook/`, `scripts/`,
   `supabase/`, `studio/`; match bare side-effect imports (`import 'x'`); and
   resolve `.js` specifiers to `.ts`/`.tsx`/`.jsx` the way the bundler does
   (`import x from './foo.js'` is `foo.tsx`). Missing that last one made 51 live
   Landing V5 files look dead. **Because the resolver can have gaps, also run a
   plain text search for each file's name across all code** — that check does
   not depend on resolution at all.
2. **Recency and intent.** Find the last *substantive* commit (skip repo-wide
   sweeps) and read its message. Anything parked on purpose — "kept for",
   "re-enable", "before any data wiring" — is not dead. Anything created in the
   last few weeks is presumed in progress until its author says otherwise.
3. **Open PRs.** Intersect every open PR's changed files with the delete list;
   a hit is a modify/delete conflict for someone else.
4. **Non-import references.** `tsconfig.json`, `.storybook/main.ts` aliases,
   `package.json`, `.github/`, `vercel.json`, tests that read source files from
   disk, and docs — each deletion must take its stale reference with it.
5. **Name matches in live code.** If a deleted export's name appears in a live
   file, find out why before deleting (on PR 2 it was commented-out JSX).

**Every empty result gets a control that must fire.** This is not a formality:
on 2026-09-22 a zsh glob emptied a file list, a zsh subscript mangled a count,
BSD `sed` ignored `\L`, and an unsplit variable turned a six-name loop into one
nonsense search — all of which printed a clean-looking "nothing found".

**Per-PR verification:** tsc *error set* identical before and after (not a
count), eslint compared as a warning set against `staging`, the full `vitest`
suite, and a Storybook or app render for anything visual. Note that
`tsconfig.json` has `"incremental": true`, so run tsc with `--incremental false`
when a result looks surprising, and that a worktree sharing another worktree's
`node_modules` can lack newly added packages (on 2026-09-22 `unframer`, for
`landing-v5`) — compare against a baseline from the same setup.

***

## 6. Rough size

Re-cut on 2026-09-22 for the UI-only scope (PR 7 dropped, the two deliberate
files out of PR 5, two PR 6 files held for Shain). Counts are approximate until
each PR re-measures on its own branch point.

| PR | What | Files removed | Call sites touched | Risk |
| - | - | - | - | - |
| 1 | rules | 0 | 0 | none — **done** |
| 2 | unused demo components | 6 | 0 | none — **done** |
| 3 | old landing generations | 9 (+ 1 doc note; tsconfig after #4057) | 0 | low — **done** |
| 4 | pre-revamp radar + vault | \~23 | 0 | low |
| 5 | superseded dialogs, misc | \~10 + stories + 1 test | 0 | low |
| 6 | dead cards, banners, tables | \~33 (2 held) | 0 | low |
| 7 | — | dropped | — | — |
| 8–11 | near-done migrations | \~5 modules | \~12 | low; 8 blocked on a rule, 11 on design |
| 12–17 | large migrations | \~6 modules | \~424 | medium, spread over several PRs |
| — | `dialog`, `select` | 2 | \~150 | blocked on design |

PRs 2–6 remove roughly 80 UI files without changing a rendered pixel, and none
of it depends on the migrations.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.