diff --git a/.codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md b/.codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md
new file mode 100644
index 0000000..d3b347a
--- /dev/null
+++ b/.codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md
@@ -0,0 +1,482 @@
+# Full M.D.C. Combat Exchange 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:** Deliver issue #51 by extending the persisted combat exchange through the complete S.D.C./M.D.C. routing matrix, including deterministic fatal overflow and a terminal dead-dossier state, while preserving legacy exchange history.
+
+**Architecture:** Extend the page-stamped rules boundary and existing pure combat resolver rather than creating a second M.D.C. path. Every newly resolved hit emits a version-2 evidence route containing native damage, conversion, protection/body deltas, final-blast absorption, and life-state outcome. Convex remains the sole dice and persistence authority and applies the route atomically. SolidJS exposes legal M.D. weapons, explicit units/history, and a readable terminal state within the existing Ley Terminal design.
+
+**Tech Stack:** TypeScript, Zod, JSON rules content, Convex 1.42, SolidJS 1.9, Tailwind CSS 4, Vite+ (`vp`), Vite+ Test, pnpm 11.
+
+## Global Constraints
+
+- Work directly in `D:\Projects\riftforge` on `feat/mdc-combat-exchange`; do not create a worktree.
+- Treat `.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md` as the approved contract and rendered RUE pp. 287, 288, and 354-359 as rules authority.
+- Keep optional pp. 358-359 injury/survival work in issue #54 and the generic effects pipeline in issue #53; neither belongs in #51.
+- Use the existing persisted exchange, not a parallel M.D.C. resolver. Keep `characters.applyDamage` S.D.C.-only; hostile M.D. writes occur only through exchanges.
+- Multiply critical damage in the native tier before conversion. `totalDamage` remains the native completed weapon total.
+- Apply A.R. to S.D.C. armor for both tiers. M.D.C. armor has no A.R. The final M.D.C. absorbs the full destroying blast with no spill.
+- A depleted M.D.C. shell stops S.D.C. strike totals 1-7, admits 8+, and does not stop M.D.
+- Raw H.P. below `-P.E.` is fatal; exactly `-P.E.` is coma. Fatal persistence is S.D.C. `0`, H.P. at the floor, `current.lifeState = "dead"`.
+- Rules remain pure/deterministic. Convex owns dice and state. Clients never choose damage tier, route, conversion, or after-values.
+- Preserve exact legacy route validation/history without migration. Old/racing pending exchanges stale safely.
+- Preserve route-epoch and exchange-ID ownership guards because parameterized routes do not remount.
+- Use stable `ConvexError` codes for expected combat refusals; the web must not parse English messages.
+- Use Vite+; in `packages/backend` use `pnpm exec`, never `npx`.
+- Work red -> green -> refactor. Run package gates before root gates. Checkpoint-commit every task.
+- No AI attribution. Never commit to `main`, merge, or manually close #51; the human maintainer merges.
+- Live-browser acceptance is mandatory for this user-visible change.
+
+### Frontend direction
+
+- Continue the compact Ley Terminal command rail; add no rounded card system, decorative art, or cyan M.D.C. language.
+- Use amber for stopped/armor absorption, blood red for body/fatal harm, and green for defended/safely settled results.
+- Label every amount `S.D.C.` or `M.D.`; color is never the only signal.
+- Keep a dead dossier, equipment, inventory management, narrative, cancellation cleanup, and history readable while disabling gameplay actions with an explicit reason.
+
+---
+
+## File Map
+
+### Rules
+
+- `packages/rules/src/schema/combat-exchange.ts` and `src/content/combat/combat-exchange.json`: page-stamped M.D.C. constants and stable errors.
+- `packages/rules/src/schema/character.ts`: optional persisted terminal marker only.
+- `packages/rules/src/engine/combat.ts`: fatal-aware body damage plus legacy pool wrapper.
+- `packages/rules/src/engine/character.ts`: derived `alive | coma | dead` and terminal invariants.
+- `packages/rules/src/engine/combat-exchange.ts`: legal M.D. profiles, M.D.C. readiness, v2 tokens, complete routing matrix.
+- `packages/rules/src/index.ts`: additive exports.
+- `packages/rules/tests/{combat,character,combat-exchange}.test.ts`: pure boundaries and matrix.
+
+### Backend
+
+- `packages/backend/convex/schema.ts`: optional dead marker.
+- `packages/backend/convex/character_state.ts`: shared living guard.
+- `packages/backend/convex/characters.ts`: fatal manual S.D.C. damage and terminal guards.
+- `packages/backend/convex/combat_values.ts`: exact union of legacy and v2 routes.
+- `packages/backend/convex/combat.ts`: target readiness, legal M.D. declarations, atomic route writes.
+- `packages/backend/convex/_generated/{api,dataModel}.d.ts`: regenerate only if codegen changes them.
+- `packages/backend/tests/{characters,combat}.test.ts`: mutation, compatibility, race, atomicity tests.
+
+### Web and delivery
+
+- `apps/web/src/lib/combat-exchange.ts`: version-aware presentation and disabled reasons.
+- `apps/web/src/components/combat-exchange-panel.tsx`: M.D. selection/results and terminal mode.
+- `apps/web/src/components/sheet-view.tsx`: derived life-state presentation and gameplay disabling.
+- `apps/web/src/pages/character-sheet.tsx`: terminal command rail and navigation ownership.
+- `apps/web/tests/combat-exchange.test.ts`: selection, labels, routes, tones, ownership.
+- Create `apps/web/tests/character-sheet.test.ts`: terminal dossier behavior.
+- `README.md`, approved design, and this plan: final aligned evidence.
+
+---
+
+### Task 1: Page-Stamped M.D.C. Rules Boundary
+
+**Files:** Modify `packages/rules/src/schema/combat-exchange.ts`, `packages/rules/src/content/combat/combat-exchange.json`, `packages/rules/src/index.ts`, and `packages/rules/tests/combat-exchange.test.ts`.
+
+**Contract:**
+
+```ts
+pages: {
+ megaDamageIntro: 288;
+ megaDamageCombat: 355;
+}
+rules: {
+ sdcPerMd: 100;
+ minimumSdcToDamageMdc: 100;
+ depletedMdcArmorBypassStrike: 8;
+ finalMdcAbsorbsDestroyingBlast: true;
+}
+```
+
+- [x] Add failing tests asserting all six exact values and negative schema tests for altered literals.
+- [x] Run `vp test packages/rules/tests/combat-exchange.test.ts`; observe missing-field failure.
+- [x] Add `z.literal(...)` fields under the existing `pages`/`rules` objects and matching JSON values; retain all existing constants/exports.
+- [x] Run `vp test packages/rules/tests/combat-exchange.test.ts`, `vp run @riftforge/rules#check`, and `vp run @riftforge/rules#test`; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/rules/src/schema/combat-exchange.ts packages/rules/src/content/combat/combat-exchange.json packages/rules/src/index.ts packages/rules/tests/combat-exchange.test.ts
+git commit -m "feat(rules): stamp mega-damage exchange rules"
+```
+
+---
+
+### Task 2: Fatal-Aware Body Damage and Derived Life State
+
+**Files:** Modify `packages/rules/src/schema/character.ts`, `packages/rules/src/engine/{combat,character}.ts`, `packages/rules/src/index.ts`, and `packages/rules/tests/{combat,character}.test.ts`.
+
+**Contract:**
+
+```ts
+export type LifeState = "alive" | "coma" | "dead";
+export interface BodyDamageResult {
+ before: VitalsPool;
+ after: VitalsPool;
+ rawHitPoints: number;
+ lifeState: LifeState;
+}
+export function applyBodyDamage(
+ pool: VitalsPool,
+ damage: number,
+ comaDeathFloor: number,
+): BodyDamageResult;
+```
+
+Persist only `lifeState: z.literal("dead").optional()`; add `lifeState` to `CharacterSheet.vitals`.
+
+- [x] Add a table for `{sdc:0, hitPoints:1}`, floor `-10`: damage `1 -> coma/0`, `11 -> coma/-10`, `12 -> dead/-10` with raw `-11`. Prove S.D.C. drains first and existing `applyDamage` still returns the clamped pool.
+- [x] Add character tests: positive H.P. => alive; H.P. `0` through floor => coma; dead marker valid only with rolled vitals, S.D.C. `0`, H.P. exactly floor; contradictory/unrolled dead state rejected; legacy marker-absent documents valid.
+- [x] Run `vp test packages/rules/tests/combat.test.ts packages/rules/tests/character.test.ts`; observe failure.
+- [x] Implement:
+
+```ts
+const sdcDamage = Math.min(pool.sdc, damage);
+const rawHitPoints = pool.hitPoints - (damage - sdcDamage);
+const lifeState = rawHitPoints < comaDeathFloor ? "dead" : rawHitPoints <= 0 ? "coma" : "alive";
+const after = {
+ sdc: pool.sdc - sdcDamage,
+ hitPoints: Math.max(comaDeathFloor, rawHitPoints),
+};
+```
+
+Keep integer/nonnegative validation. Make `applyDamage(...)` return `applyBodyDamage(...).after` for compatibility.
+
+- [x] In `deriveSheet`, validate terminal invariants after pools/floor are known and derive unpersisted alive/coma from current H.P.
+- [x] Run focused tests and rules check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/rules/src/schema/character.ts packages/rules/src/engine/combat.ts packages/rules/src/engine/character.ts packages/rules/src/index.ts packages/rules/tests/combat.test.ts packages/rules/tests/character.test.ts
+git commit -m "feat(rules): derive terminal life state"
+```
+
+---
+
+### Task 3: Legal M.D. Profiles, M.D.C. Readiness, and v2 Tokens
+
+**Files:** Modify `packages/rules/src/schema/combat-exchange.ts`, `packages/rules/src/engine/combat-exchange.ts`, and `packages/rules/tests/combat-exchange.test.ts`.
+
+**Contract:** Supported `AttackProfile.damageType` is `"sdc" | "md"`. Preserve M.D.C. protection even at current `0`; worn unrolled armor derives `mdcArmor` with absent max/current. Add `combatantDead` and `armorNotReady` errors, but retain legacy unsupported error values for persisted data.
+
+- [x] Replace M.D. refusal tests with legal energy-pistol/rifle profiles, printed formulas, unchanged strike/critical data, and `damageType: "md"`.
+- [x] Test full/partial/depleted M.D.C. armor stays `mdcArmor`; unrolled armor exposes absent pools; depleted S.D.C. armor retains current no-protection behavior.
+- [x] Test attacker token changes with damage tier/selected weapon. Defender token changes with life state, tier, M.D.C. readiness/max/current. Narrative, P.P.E., unrelated inventory remain excluded.
+- [x] Run focused rules test; observe current refusal/collapse failure.
+- [x] Return the validated item tier directly in supported profiles. Preserve all worn M.D.C. armor in `deriveProtection`.
+- [x] Prefix explicit ordered token tuples `attacker-v2`/`defender-v2`; include the new relevant fields without serializing the whole sheet.
+- [x] Run focused test and rules check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/rules/src/schema/combat-exchange.ts packages/rules/src/engine/combat-exchange.ts packages/rules/tests/combat-exchange.test.ts
+git commit -m "feat(rules): authorize mega-damage attacks"
+```
+
+---
+
+### Task 4: Version-2 Tiered Routing and Resolution
+
+**Files:** Modify `packages/rules/src/engine/combat-exchange.ts`, `packages/rules/src/index.ts`, and `packages/rules/tests/combat-exchange.test.ts`.
+
+**Contract:**
+
+```ts
+export type DamageAmount = { type: "sdc" | "md"; value: number };
+export type ProtectionDamageSnapshot = {
+ kind: "sdcArmor" | "mdcArmor";
+ itemId: string;
+ name: string;
+ before: number;
+ after: number;
+};
+export type BodyDamageSnapshot = { before: VitalsPool; after: VitalsPool };
+```
+
+`TieredDamageRoute` is an exact union:
+
+- `{routingVersion:2, kind:"stopped", reason:"intactMdcImpervious"|"depletedMdcShell", nativeDamage, armor, body}`
+- `{routingVersion:2, kind:"armor", nativeDamage, convertedDamage?, armor, body, finalBlastAbsorbed}`
+- `{routingVersion:2, kind:"body", nativeDamage, convertedDamage?, armor?, body, lifeState:{before:"alive"|"coma", after:"alive"|"coma"}}`
+- `{routingVersion:2, kind:"fatal", nativeDamage, convertedDamage?, armor?, body, lifeState:{before:"alive"|"coma", after:"dead"}}`
+
+All new hits use this union. Keep legacy `SdcDamageRoute` exported only for persisted validation/presentation.
+
+- [x] Add intact-M.D.C. conversion tests for `99/100/199/200` plus printed `450/496 -> 4 M.D.C.`.
+- [x] Add final-point M.D.C. no-spill and `finalBlastAbsorbed` tests.
+- [x] Add depleted-shell tests for S.D.C. strike `7/8` and native M.D. bypass.
+- [x] Add S.D.C.-armor tests for S.D. and M.D. attacks at/below/above A.R.
+- [x] Add no-armor S.D./M.D. body tests for S.D.C.-before-H.P., exact floor, and fatal overflow.
+- [x] Add a critical M.D. resolver test proving `totalDamage = native roll * multiplier` before `convertedDamage = totalDamage * 100`.
+- [x] Assert every new hit has v2, native/converted evidence, exact armor/body snapshots, and no unsupported-M.D.C. route.
+- [x] Run focused test; observe S.D.C.-only failures.
+- [x] Implement exact helpers:
+
+```ts
+const sdcToMd = (value: number): DamageAmount => ({
+ type: "md",
+ value: Math.floor(value / combatExchangeRules.rules.sdcPerMd),
+});
+const mdToSdc = (value: number): DamageAmount => ({
+ type: "sdc",
+ value: value * combatExchangeRules.rules.sdcPerMd,
+});
+```
+
+- [x] Implement routing in this order: S.D.C. armor A.R.; intact M.D.C.; depleted M.D.C. shell; no protection; fatal-aware body. `damageArmor` clamps armor; `finalBlastAbsorbed` is true when the armor hit ends at `0`; never spill armor damage.
+- [x] Resolve using `{type: input.attack.damageType, value: totalDamage}` after critical multiplication and pass the actual tier to strike evaluation.
+- [x] Run focused tests and rules check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/rules/src/engine/combat-exchange.ts packages/rules/src/index.ts packages/rules/tests/combat-exchange.test.ts
+git commit -m "feat(rules): resolve tiered combat damage"
+```
+
+---
+
+### Task 5: Backward-Compatible Convex Values and Schema
+
+**Files:** Modify `packages/backend/convex/schema.ts`, `packages/backend/convex/combat_values.ts`, generated types if changed, and `packages/backend/tests/combat.test.ts`.
+
+**Contract:** Keep the legacy route validator exact, add separately discriminated v2 validators matching Task 4, and use `route: v.union(legacySdcDamageRouteValidator, tieredDamageRouteValidator)`. Add `lifeState: v.optional(v.literal("dead"))` to character current state.
+
+- [x] Add fixtures proving legacy armor/body routes and v2 stopped/armor/fatal routes all insert/read unchanged. Add negative fixtures for missing v2 native damage, fatal ending in coma, and extra legacy fields.
+- [x] Run `vp test packages/backend/tests/combat.test.ts`; observe validator failures.
+- [x] Add exact `DamageAmount`, protection/body snapshot, and four v2 route validators with `v.literal(2)`. Optional fields are optional only on branches allowed by the rules type.
+- [x] Preserve legacy unsupported error codes; add `combatantDead` and `armorNotReady`.
+- [x] Add the dead marker to schema. From `packages/backend`, run `pnpm exec convex codegen`; do not hand-edit or commit generated files unless they change.
+- [x] Run backend focused test and backend check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/backend/convex/schema.ts packages/backend/convex/combat_values.ts packages/backend/convex/_generated/dataModel.d.ts packages/backend/convex/_generated/api.d.ts packages/backend/tests/combat.test.ts
+git commit -m "feat(backend): validate tiered combat routes"
+```
+
+---
+
+### Task 6: Terminal Character Writes and Fatal Manual S.D.C. Damage
+
+**Files:** Modify `packages/backend/convex/character_state.ts`, `packages/backend/convex/characters.ts`, and `packages/backend/tests/characters.test.ts`.
+
+**Contract:**
+
+```ts
+export function requireLiving(character: Character, action: string): void;
+// Throws: Life signs terminated — dead characters cannot ${action}.
+```
+
+Manual damage returns `lifeState: "alive" | "coma" | "dead"` but accepts no tier argument.
+
+- [x] Add manual damage tests at P.E. 10/current `{sdc:0,hp:1}`: 11 damage stores coma at -10 without marker; 12 stores dead at -10 with marker and raw overflow is not persisted.
+- [x] Add dead-character rejections without state change for full update, roll/restore vitals, damage/healing, rest/meditation, treatment, ley draw, and casting (including cross-character healing).
+- [x] Prove narrative edits and inventory add/remove/equip remain available and preserve the marker.
+- [x] Run `vp test packages/backend/tests/characters.test.ts`; observe current floor-only/unguarded failures.
+- [x] Implement `requireLiving` and call it after authoritative load but before dice or gameplay writes. Guard both caster and healing target. Guard existing document before full replacement.
+- [x] Replace pool-only damage with:
+
+```ts
+const result = applyBodyDamage(damagePools(sheet), args.amount, sheet.vitals.comaDeathFloor);
+const current = {
+ ...character.current,
+ sdc: result.after.sdc,
+ hitPoints: result.after.hitPoints,
+ ...(result.lifeState === "dead" ? { lifeState: "dead" as const } : {}),
+};
+```
+
+Patch once; return before/after/amount/lifeState. Keep the mutation S.D.C.-only.
+
+- [x] Run focused test and backend check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/backend/convex/character_state.ts packages/backend/convex/characters.ts packages/backend/tests/characters.test.ts
+git commit -m "feat(backend): enforce terminal life state"
+```
+
+---
+
+### Task 7: M.D.C.-Ready Target Discovery and Declaration
+
+**Files:** Modify `packages/backend/convex/combat.ts` and `packages/backend/tests/combat.test.ts`.
+
+**Contract:** Target summaries add `lifeState`, full `ProtectionState`, and optional `disabledReason: "defenderNotReady" | "armorNotReady" | "combatantDead"`.
+
+- [x] Test discovery for intact/depleted rolled M.D.C. armor (enabled), unrolled worn M.D.C. (`armorNotReady`), dead target (`combatantDead` precedence), unrolled body (`defenderNotReady`), and unchanged S.D.C. target.
+- [x] Test legal energy-pistol/rifle declarations store `damageType:"md"` and roll strike exactly once. Dead attacker/defender and unready armor must insert nothing and roll nothing.
+- [x] Run backend combat test; observe current unsupported refusals.
+- [x] Reject dead sheets with `combatFailure("combatantDead", ...)`. Reject only M.D.C. protection with absent max/current using `combatFailure("armorNotReady", ...)`; do not reject legal M.D. or depleted armor.
+- [x] Persist the real attack profile/tier. Re-read both sheets, verify expected item identity/state, compute v2 tokens, roll once, and insert once. Keep current indexes/limits.
+- [x] Run focused test and backend check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/backend/convex/combat.ts packages/backend/tests/combat.test.ts
+git commit -m "feat(backend): declare mega-damage attacks"
+```
+
+---
+
+### Task 8: Atomic Tiered Resolution, Death, and Stale Races
+
+**Files:** Modify `packages/backend/convex/combat.ts` and `packages/backend/tests/combat.test.ts`.
+
+- [x] Add atomic intact-M.D.C. tests: S.D.C. 99 stopped, 100 ablates 1, and a destroying M.D. blast patches armor only.
+- [x] Add atomic depleted-shell tests: S.D.C. strike 7 stopped, strike 8 hits body, and M.D. converts into body.
+- [x] Add atomic S.D.C.-armor tests for M.D. at/below and above A.R.
+- [x] Add exact-floor coma and one-point-overflow fatal marker tests. Assert native `totalDamage` and full route evidence.
+- [x] Add stale races for death, armor readiness, and selected-weapon change after declaration.
+- [x] Add idempotency/compatibility tests: cleanup cancellation remains legal, duplicate response never double-applies, and legacy v1 tokens stale safely.
+- [x] Run backend combat test; observe S.D.C.-only write failures.
+- [x] Call `resolveCombatExchange` with stored attack tier, completed server rolls, rederived protection/body, and derived floor. Convex does no conversion.
+- [x] Apply one route patch: stopped => none; armor => armor only; body => S.D.C./H.P.; fatal => S.D.C./H.P./dead together. Patch exchange resolution in the same Convex mutation. Legacy routes are never newly generated.
+- [x] Check v2 tokens before defense/damage rolls. Any race, including newly dead, settles through stale logic with no character patch; stable dead errors are for new declarations.
+- [x] Run `vp test packages/backend/tests/combat.test.ts packages/backend/tests/characters.test.ts` and backend check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- packages/backend/convex/combat.ts packages/backend/tests/combat.test.ts
+git commit -m "feat(backend): persist tiered combat outcomes"
+```
+
+---
+
+### Task 9: Unit-Aware Web Presentation and Legal M.D. Selection
+
+**Files:** Modify `apps/web/src/lib/combat-exchange.ts`, `apps/web/src/components/combat-exchange-panel.tsx`, and `apps/web/tests/combat-exchange.test.ts`.
+
+- [x] Replace M.D. disabled tests: legal catalog M.D. weapons are enabled and labeled with formula plus `M.D.`; invalid modes retain their reason.
+- [x] Pin server-reason copy exactly: `defenderNotReady -> Roll this target's H.P. and S.D.C. first.`, `armorNotReady -> Roll this target's worn armor M.D.C. first.`, `combatantDead -> Life signs terminated; this target cannot enter combat.`
+- [x] Pin exact legacy formatting unchanged and v2 summaries for stopped S.D.C., `496 S.D.C. -> 4 M.D.C.`, native M.D. ablation, M.D.-to-body conversion, final blast, depleted shell, and fatal termination.
+- [x] Pin tones: stopped/armor `warn`, body/fatal `bad`, defended `good`, cancelled `dim`.
+- [x] Run `vp test apps/web/tests/combat-exchange.test.ts`; observe M.D. refusal/S.D.C.-only formatter failures.
+- [x] Remove unsupported-M.D. copy, map only server stable disabled reasons, and add:
+
+```ts
+function isTieredRoute(route: ExchangeRoute): route is TieredDamageRoute {
+ return "routingVersion" in route && route.routingVersion === 2;
+}
+```
+
+Keep legacy formatter branch. Build v2 text from persisted native/converted amounts, reason, before/after values, final-blast flag, and life state; never reconstruct evidence from `totalDamage`.
+
+- [x] Update panel result labels using existing primitives and rail structure; add no new card system.
+- [x] Run focused test and web check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- apps/web/src/lib/combat-exchange.ts apps/web/src/components/combat-exchange-panel.tsx apps/web/tests/combat-exchange.test.ts
+git commit -m "feat(web): present tiered combat outcomes"
+```
+
+---
+
+### Task 10: Terminal Dossier UI Without Hiding History
+
+**Files:** Modify `apps/web/src/components/sheet-view.tsx`, `apps/web/src/components/combat-exchange-panel.tsx`, `apps/web/src/pages/character-sheet.tsx`, `apps/web/tests/combat-exchange.test.ts`; create `apps/web/tests/character-sheet.test.ts`.
+
+**Contract:** Add `gameplayDisabledReason?: string` to `SheetView` props. Use `Life signs terminated; gameplay actions are unavailable.` consistently.
+
+- [x] Add a dead `SheetView` test: terminal label/reason render; identity/vitals/equipment remain; save/skill/weapon/spell/combat rows are disabled with `aria-disabled`/`title`; inventory remains usable.
+- [x] Add a dead page/rail test: narrative, telemetry, history, navigation, and cancellation remain; declaration/response/damage/restore/recovery controls are unavailable.
+- [x] Add an alive-sheet/page regression proving existing controls remain wired.
+- [x] Add route-change regression: dead A -> living B during in-flight request resets drafts, increments epoch, ignores A result, enables B actions.
+- [x] Run focused web tests; observe missing terminal behavior.
+- [x] Pass terminal reason into save/skill/combat/weapon/spell rows while retaining the actions object for inventory. Terminal reason takes precedence over spell affordability.
+- [x] In the page, derive `sheet()?.vitals.lifeState === "dead"`. Replace command-rail gameplay controls with a danger terminal alert; keep telemetry. In combat panel, keep outgoing cancellation/recent history but suppress declaration/response controls and show the alert.
+- [x] Extend the existing ID-change reset/epoch logic; do not key/remount the page or replace ownership tokens with booleans.
+- [x] Run focused tests and web check/test; expect PASS.
+- [x] Commit:
+
+```text
+git add -- apps/web/src/components/sheet-view.tsx apps/web/src/components/combat-exchange-panel.tsx apps/web/src/pages/character-sheet.tsx apps/web/tests/character-sheet.test.ts apps/web/tests/combat-exchange.test.ts
+git commit -m "feat(web): render terminal character dossiers"
+```
+
+---
+
+### Task 11: Cross-Package Hardening and Automated Verification
+
+**Files:** Modify only files already in scope when a reproduced failure requires it; update this plan's checked steps/evidence.
+
+- [x] Run focused boundary suite:
+
+```text
+vp test packages/rules/tests/combat.test.ts packages/rules/tests/character.test.ts packages/rules/tests/combat-exchange.test.ts packages/backend/tests/characters.test.ts packages/backend/tests/combat.test.ts apps/web/tests/combat-exchange.test.ts apps/web/tests/character-sheet.test.ts apps/web/tests/convex.test.ts
+```
+
+- [x] Run all package gates:
+
+```text
+vp run @riftforge/rules#check
+vp run @riftforge/rules#test
+vp run @riftforge/backend#check
+vp run @riftforge/backend#test
+vp run @riftforge/web#check
+vp run @riftforge/web#test
+```
+
+- [x] Run `vp check`, `vp test`, and `git diff --check`; expect PASS.
+- [x] Audit the bug class:
+
+```text
+rg -n "unsupportedMdWeapon|unsupportedMdcProtection|Full M\.D\.C\. combat is follow-up work|damageType: \"sdc\"" packages apps README.md
+rg -n "applyDamage\(|applyBodyDamage\(|lifeState|routingVersion" packages apps
+rg -n "TODO|TBD|FIXME" packages/rules/src packages/backend/convex apps/web/src .codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md
+```
+
+Legacy validator/enum hits are expected; active refusals, hardcoded S.D.C. resolution, unguarded writes, and incomplete implementation markers are not.
+
+- [x] Compare rules type, Convex validator, and formatter branch-by-branch. Confirm client non-authority, one atomic character patch, exact-floor distinction, and no final-blast spill.
+- [x] Use `superpowers:requesting-code-review`. Reproduce findings, add regression tests for real bugs, fix root causes, rerun affected gates.
+- [x] If hardening changes code, commit `fix(combat): harden mega-damage invariants`; do not create an empty commit.
+
+---
+
+### Task 12: Live Convex and Browser Acceptance
+
+**Files:** None expected; for a live defect modify the narrow source/test responsible.
+
+- [x] Confirm port 3210 has no stale owner. Start `pnpm exec convex dev` in `packages/backend` and `vp dev` in `apps/web`.
+- [x] Seed two reproducible dossiers with `pnpm exec convex run characters:create`: an alive attacker with S.D./M.D. weapons and an alive defender with worn rolled M.D.C. armor. Record returned IDs.
+- [x] At `http://localhost:5173`, verify every legal-catalog live route: legal M.D. selection, sub-100 stop, native ablation, final-blast no spill, depleted-shell `7/8`, fatal terminal marker, and legacy/v2 history together. Verify the unavailable 100+ S.D.C.-vs-M.D.C. and M.D.-vs-S.D.C.-armor rows through exact rules/backend tests; the production catalog has no legal browser path for either row.
+- [x] On the dead dossier, verify explicit inaccessible gameplay actions while identity, pools, equipment, inventory, narrative, cleanup cancellation, and history remain. Navigate to the living ID without reload; drafts reset/actions re-enable.
+- [x] Check desktop and narrow viewport, keyboard/accessibility text, layout clipping, and console errors/warnings.
+- [x] For defects: add failing automated test, fix, rerun package gates, repeat scenario.
+- [x] Stop web, Convex CLI, and orphaned backend; confirm ports 5173/3210 are closed.
+
+---
+
+### Task 13: Documentation, Issue Evidence, and Draft PR
+
+**Files:** Modify `README.md`, approved design spec, and this plan.
+
+- [x] Replace README's stale “full M.D.C. interaction remains future work” boundary with completed conversion/armor/shell/final-blast/death behavior; defer optional survival to #54.
+- [x] Append exact date/time, package/root command outputs and fresh counts, live IDs/scenarios, viewports, and console result to the design; check all completed plan boxes. Never reuse older counts.
+- [x] Re-run `vp check`, `vp test`, `git diff --check`, and `git status --short --branch`.
+- [x] Commit:
+
+```text
+git add -- README.md .codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md .codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md
+git commit -m "docs: record mega-damage combat delivery"
+```
+
+- [x] Push `feat/mdc-combat-exchange` and open draft PR [#55](https://github.com/StreamDemon/RiftForge/pull/55) to `main` summarizing architecture, compatibility, fatal boundary, automation, and live evidence. Link `Closes #51`, `Follow-up #54`, and `Roadmap #53`; no AI attribution; never merge.
+- [x] Comment on #51 with verified time-scoped evidence/PR link: [issue comment 5037268203](https://github.com/StreamDemon/RiftForge/issues/51#issuecomment-5037268203). Do not close it manually.
+- [x] Use CodeRabbit as the temporary review gate while Cubic is quota-blocked until 2026-08-01. CodeRabbit approved the implementation, then its full `3067c99..4f5f0bb` re-review found one valid trivial nit: missing source-contract files could pass negative assertions vacuously. Fix the whole duplicated-helper class TDD, rerun affected and root gates, and stop ready for human merge.
+
+---
+
+## Final Acceptance Checklist
+
+- [x] Rendered/page-stamped authority covers every mechanic.
+- [x] `99/100/199/200`, printed `450/496`, critical-before-conversion, final-blast, depleted-shell `7/8`, and M.D.-vs-S.D.C.-A.R. are pinned.
+- [x] Exact `-P.E.` is coma; below is dead; all gameplay mutations reject dead while inventory/narrative preserve the marker.
+- [x] Legacy routes read without migration; pending legacy/racing exchanges stale and never double-apply.
+- [x] New history exposes native tier, conversion, reason, and before/after evidence.
+- [x] Dead dossiers remain readable/accessibly terminal; route navigation owns async results.
+- [x] Rules/backend/web package gates and root gates pass with fresh evidence.
+- [x] Live desktop/narrow acceptance passes with clean console.
+- [x] README, #51, #54, #53, design, plan, and ready PR agree; human retains merge authority.
diff --git a/.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md b/.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md
new file mode 100644
index 0000000..da082b9
--- /dev/null
+++ b/.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md
@@ -0,0 +1,691 @@
+# Full M.D.C. Combat Exchange Design
+
+**Date:** 2026-07-21
+**Status:** PR #55 ready; CodeRabbit nit resolved; human merge pending
+**Branch:** `feat/mdc-combat-exchange`
+**Primary issue:** [#51 — Full M.D.C. combat](https://github.com/StreamDemon/RiftForge/issues/51)
+
+## Goal
+
+Extend the persisted hostile-combat vertical slice from Issue #44 through the
+complete core S.D.C./M.D.C. damage-tier boundary without inventing optional
+injury rules or introducing a generic effect interpreter.
+
+This slice will:
+
+- support owned S.D.C. and M.D. weapon attacks through one exchange protocol;
+- apply the printed S.D.C.-to-M.D.C. conversion and imperviousness rules;
+- resolve S.D.C. and M.D. attacks against S.D.C. and M.D.C. body armor;
+- preserve the final-blast absorption rule for destroyed armor;
+- preserve the limited S.D.C. protection of depleted M.D.C. body armor;
+- resolve M.D. against an unprotected mortal and persist deterministic death;
+- keep all dice, conversion, routing, and writes authoritative on the server;
+- keep existing Issue #44 exchange history readable without a data rewrite; and
+- present the complete route in the live SolidJS combat rail.
+
+The existing exchange remains the authority boundary. Issue #51 does not add
+M.D. input to the sheet's manual damage control.
+
+## Delivered outcome
+
+The local implementation now delivers the complete core boundary described here:
+page-stamped tier constants, critical-before-conversion routing, intact and
+depleted M.D.C. armor behavior, S.D.C. armor A.R. for either attack tier,
+final-blast absorption without spill, versioned immutable evidence, atomic Convex
+writes, and persisted terminal death. It extends the existing exchange and keeps
+legacy history readable. The optional RUE pp.358-359 near-fatal injury/survival
+procedure remains in issue #54, and the generic effects pipeline remains roadmap
+issue #53.
+
+## Approved decisions
+
+1. **One exchange protocol.** Extend the existing typed exchange model. Do not
+ build a parallel M.D.C. ledger or resolver.
+2. **Core outcomes only.** M.D. against an unprotected mortal follows the core
+ damage and death rules. The optional near-fatal injury procedure on RUE
+ pp.358-359 is deferred to Issue #54.
+3. **Exchange-only M.D. writes.** M.D. can enter persistent character state only
+ through the server-rolled exchange. The manual damage control remains S.D.C.
+4. **No generic effects layer.** Issue #51 remains feature-specific. A future
+ page-stamped effect pipeline is tracked in Issue #53 and has explicit entry
+ criteria.
+5. **Backward-compatible history.** Existing resolved, cancelled, stale, and
+ pending Issue #44 records remain accepted. Legacy pending records may safely
+ stale when their page-stamped rules token no longer matches.
+6. **Terminal death is explicit.** Fatal overflow cannot be represented as a
+ survivable character clamped to the coma floor. A persisted terminal marker
+ distinguishes death from a character exactly at the legal negative H.P.
+ floor.
+7. **No client-authored tier.** Weapon content determines S.D.C. versus M.D.; the
+ UI never submits a damage tier or conversion.
+
+## Rendered rules evidence
+
+The scanned local Rifts Ultimate Edition has no usable text layer. The following
+pages were rendered from PDF indexes `printed page + 2` at 2.2x and inspected
+visually on 2026-07-21.
+
+| Printed page | Rule used in this design |
+| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 287 | Artificial S.D.C. armor routes every completed strike by A.R.: at or below A.R. the armor absorbs the attack; above A.R. the attack reaches the wearer. Depleted S.D.C. armor affords no future protection. |
+| 288 | One M.D. equals 100 S.D.C. M.D.C. armor is impervious to S.D.C./Hit Point damage below 100. S.D.C. totals of 100 or more can harm M.D.C.; divide by 100 and round down. M.D.C. armor has no A.R. The last M.D.C. absorbs the complete destroying blast. |
+| 354-355 | Zero or negative H.P. is the coma band down to `-P.E.` inclusive. Damage beyond that floor is death with no hope of recovery. The final M.D.C. of armor absorbs the complete destroying blast; subsequent M.D. attacks reach the unprotected wearer. |
+| 355 | At zero M.D.C., body armor is scrap but still stops S.D.C. strike totals 1-7; totals of 8 or higher reach the body inside. Ordinary S.D.C. weapons do not harm M.D.C. beings except for printed vulnerabilities. |
+| 356-357 | Context only: M.D.C. technology, equipment scarcity, and GM guidance. No new deterministic damage constant is introduced from these pages. |
+| 358 | Core text describes M.D. against an S.D.C. body as normally lethal. The near-fatal survival procedure begins as an optional guideline requiring GM agreement and medical intervention. |
+| 359 | The optional survival path introduces hit locations, called-shot eligibility, limb/internal injury, immediate care, surgery, and trauma. Those mechanics are deliberately outside Issue #51 and tracked in Issue #54. |
+
+Existing Issue #44 sources remain authoritative for declaration, defense,
+critical multiplication, ranged combat, S.D.C. armor A.R., and S.D.C.-before-H.P.
+routing.
+
+## Scope
+
+### In scope
+
+- Owned S.D.C. knife, axe, handgun, and submachine-gun attacks already supported
+ by Issue #44.
+- Owned M.D. energy-pistol and energy-rifle attacks from the current catalog.
+- Existing melee/ranged declaration context, defense authorization, critical
+ rules, and server-owned dice.
+- S.D.C. attacks against intact and depleted M.D.C. body armor.
+- M.D. attacks against S.D.C. armor, intact M.D.C. armor, depleted M.D.C.
+ armor, and an unprotected S.D.C./Hit Point body.
+- Final-blast armor absorption without spill into the wearer.
+- Explicit stopped, armor, body, and fatal route evidence.
+- Persisted terminal death state and derived alive/coma/dead presentation.
+- Consistent fatal-threshold handling for the existing manual S.D.C. damage
+ mutation, without adding an M.D. input mode.
+- Backward-compatible Convex validators and history formatting.
+- Live two-dossier combat acceptance and parameter-route ownership checks.
+
+### Out of scope
+
+- Optional near-fatal M.D. survival, hit locations, limb loss, medical checks,
+ surgery, trauma, and bionic reconstruction (Issue #54).
+- A generic spell/psionic/combat effect pipeline (Issue #53).
+- General called shots, aimed locations, bursts, payload tracking, thrown weapon
+ modes, initiative, rounds, or action-budget enforcement.
+- M.D.C. creatures, supernatural bodies, dragons, vehicles, robot vehicles,
+ power-armor operation, force fields, and printed vulnerability exceptions.
+- Explosions, blast radii, impact damage, knockdown, cover geometry, and VTT
+ positioning.
+- Authentication, ownership, GM permissions, resurrection, or character deletion
+ policy.
+- Manual M.D. damage entry.
+
+## Architecture
+
+The existing authority flow remains intact:
+
+```text
+page-stamped tier constants + pure exchange resolver
+ |
+ v
+Convex exchange ledger + atomic character write
+ |
+ v
+SolidJS declaration / response / history rail
+```
+
+Issue #51 expands the types and route calculation inside each boundary. It does
+not add a fourth orchestration layer.
+
+### Rules package
+
+The pure rules layer owns:
+
+- damage-tier classification from weapon content;
+- page-stamped S.D.C./M.D.C. conversion constants;
+- protection classification, including depleted M.D.C. armor identity;
+- critical multiplication before tier conversion;
+- stopped, armor, body, and fatal route calculation;
+- life-state derivation and terminal-state invariants; and
+- stable combat-state tokens over every route-relevant input.
+
+Random rolls, elapsed time, completed strike/defense rolls, and current pools are
+inputs. The rules layer never reads Convex or mutates a character.
+
+### Backend
+
+Convex continues to own exchange identity, synchronization, random dice,
+immutable history, current-state rederivation, and the exactly-once character
+write. It persists the rules layer's authorized route rather than reproducing
+tier math.
+
+### Web
+
+SolidJS surfaces server-derived weapon/target choices, declaration context,
+authorized defenses, and immutable route evidence. It never converts S.D.C. to
+M.D.C., decides death, or calculates armor damage.
+
+## Page-stamped combat content
+
+The combat exchange content and Zod schema gain explicit printed references and
+constants for:
+
+- M.D.C. introduction: p.288;
+- M.D.C. combat: p.355;
+- S.D.C. per M.D.: `100`;
+- minimum S.D.C. total that can harm intact M.D.C.: `100`;
+- depleted M.D.C. body-armor bypass strike total: `8`; and
+- final-blast absorption: enabled for the last positive armor point.
+
+The schema pins literal printed values and rejects contradictory shapes at import.
+Tests assert the complete content object so page or constant drift fails loudly.
+
+## Pure rules model
+
+### Attack profiles
+
+`AttackProfile` expands its supported branch from `damageType: "sdc"` to
+`damageType: "sdc" | "md"`.
+
+| Weapon category | Attack kind | Tier |
+| ----------------------------- | ----------- | ------ |
+| `knife`, `axe` | melee | S.D.C. |
+| `handgun`, `submachineGun` | ranged | S.D.C. |
+| `energyPistol`, `energyRifle` | ranged | M.D. |
+
+Existing strike bonuses, ranged minimums, defense options, critical thresholds,
+and firearm restrictions apply unchanged. M.D. weapons do not receive invented
+bonuses or special defense behavior.
+
+The attack snapshot and attacker state token include the damage tier and all
+page-stamped conversion constants. Any weapon/content change invalidates a pending
+exchange before additional dice or writes.
+
+### Protection classification
+
+Protection remains derived from the worn physical item:
+
+- `none`;
+- `sdcArmor` with A.R., maximum, and current S.D.C.; or
+- `mdcArmor` with maximum and current M.D.C.
+
+Unlike Issue #44, a worn M.D.C. suit at zero remains `mdcArmor`. Printed p.355
+gives depleted body armor limited S.D.C. protection, so treating it as `none`
+would lose a real mechanic and its identity.
+
+A dice-capacity M.D.C. suit whose per-instance maximum has not been rolled is
+classified as unready. It can be displayed, but no attack may be declared against
+it until its maximum/current protection is known.
+
+### Resolution order
+
+1. Validate the attack snapshot and combat context.
+2. Validate the completed strike roll and declaration minimum.
+3. Resolve an authorized defense with the existing `resolveStrike` primitive.
+4. Determine critical state and multiplier.
+5. Validate and total the native weapon damage roll.
+6. Apply the critical multiplier in the weapon's native tier.
+7. Classify current protection, including a depleted M.D.C. shell.
+8. Convert tiers only when the route requires conversion.
+9. Apply the route without spill from a destroying armor hit.
+10. Return immutable before/after evidence and terminal outcome.
+
+Conversion never happens before the critical multiplier. This makes the value
+being converted the actual completed damage total.
+
+### Routing matrix
+
+| Attack | Protection | Pure result |
+| ------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| S.D.C. | None | Apply to personal S.D.C., then H.P.; classify survivable versus fatal overflow. |
+| S.D.C. | S.D.C. armor | Existing strike-vs-A.R. route: at/below A.R. ablates armor; above A.R. reaches body. Destroying armor absorbs the full hit. |
+| S.D.C. | Intact M.D.C. armor | Under 100 is stopped with no pool change. At 100+, apply `floor(total / 100)` M.D. to armor. Destroying armor absorbs the full hit. |
+| S.D.C. | Depleted M.D.C. armor | Completed strike totals 1-7 are stopped. Totals 8+ reach personal S.D.C./H.P. as S.D.C. |
+| M.D. | None | Convert completed M.D. to `M.D. * 100` S.D.C./H.P. damage and classify fatal overflow. |
+| M.D. | S.D.C. armor | Apply the existing A.R. comparison. At/below A.R., convert to S.D.C. and ablate armor with no destroying-hit spill. Above A.R., convert the full hit to personal S.D.C./H.P. damage and classify fatal overflow without changing armor. |
+| M.D. | Intact M.D.C. armor | Apply native M.D. to armor. The destroying hit does not spill into the body. |
+| M.D. | Depleted M.D.C. armor | Convert the full completed M.D. total to S.D.C./H.P. damage and classify fatal overflow. |
+
+An intact M.D.C. hit by 99 S.D.C. yields a persisted stopped route. A hit by 450
+or 496 S.D.C. applies 4 M.D., matching the printed round-down examples. A hit by
+21 M.D. against 3 remaining M.D.C. changes armor from 3 to 0 and leaves body
+pools untouched. A subsequent M.D. hit reaches the wearer.
+
+### Versioned route evidence
+
+Existing Issue #44 routes remain valid in their original shape. New routes carry
+a required routing version so the validator and UI can distinguish legacy and
+tier-aware evidence without guessing from optional fields.
+
+A tier-aware route records:
+
+- native completed damage value and tier;
+- converted damage value and tier when conversion occurred;
+- route kind: `stopped`, `armor`, `body`, or `fatal`;
+- stable stopped reason, when applicable;
+- armor identity and before/after pool, when applicable;
+- body S.D.C./H.P. before and after, when applicable;
+- whether final-blast absorption prevented spill; and
+- life state before and after for fatal routes.
+
+`totalDamage` remains the completed native weapon total. Converted/application
+values live in route evidence so old S.D.C. records do not change meaning.
+
+### Life state and fatal overflow
+
+Character storage gains `current.lifeState?: "dead"`. Absence means the
+character is not terminally dead and remains backward-compatible with every
+existing document.
+
+The derived sheet exposes:
+
+- `alive` when current H.P. is above zero;
+- `coma` when current H.P. is zero through `-P.E.` inclusive and no terminal
+ marker exists; and
+- `dead` when the terminal marker exists.
+
+Body damage calculates the raw H.P. result before enforcing the storable numeric
+floor:
+
+- raw H.P. at or above `-P.E.` is survivable and stores the actual value;
+- raw H.P. below `-P.E.` is fatal, stores H.P. at `-P.E.`, stores personal S.D.C.
+ at zero, and sets `lifeState: "dead"`.
+
+The terminal marker is valid only when rolled vitals exist, current personal
+S.D.C. is zero, and current H.P. equals the derived `-P.E.` floor. These
+contradictions are rejected by `deriveSheet`, so no mutation can store a dead
+character with healthy pools.
+
+Death is terminal in current product scope. Existing damage, healing, treatment,
+rest, inventory, and combat mutations must preserve the marker or reject the
+operation. Only a separately designed resurrection mechanic may clear it.
+
+The existing manual damage mutation remains S.D.C.-only but uses the same raw
+overflow classifier. This fixes the existing case where arbitrarily large S.D.C.
+damage could clamp to the coma floor and remain survivable.
+
+## Persisted exchange model
+
+The existing discriminated exchange variants remain:
+
+- `pendingDefense`;
+- `resolved`;
+- `cancelled`; and
+- `stale`.
+
+The base attack snapshot expands `damageType` to the tier union. The resolved
+result validator accepts both the legacy Issue #44 route and the new versioned
+tier-aware route.
+
+No historical rewrite is required:
+
+- old characters lack `current.lifeState` and derive as nonterminal;
+- old attack snapshots already contain `damageType: "sdc"`;
+- old resolved routes remain accepted by their legacy validator branch; and
+- old pending exchanges whose state tokens no longer match resolve safely to
+ `stale`, never through legacy rules.
+
+## Combat-state tokens
+
+Attacker tokens continue to fingerprint the selected weapon and derived attack.
+They additionally cover damage tier and the page-stamped M.D.C. constants.
+
+Defender tokens continue to fingerprint rolled/current body pools and complete
+worn-protection identity. They additionally cover:
+
+- derived life state;
+- the terminal marker;
+- depleted M.D.C. armor identity and zero pool; and
+- armor readiness for a dice-capacity suit.
+
+Narrative, P.P.E., and unrelated inventory entries remain excluded so irrelevant
+edits do not stale combat.
+
+## Backend operations
+
+### Target query
+
+The bounded target query continues to exclude self and report readiness. It now
+also reports:
+
+- derived life state;
+- protection tier and current/max pool;
+- whether a dice-capacity M.D.C. suit still needs its roll; and
+- a stable disabled reason for dead or protection-unready targets.
+
+M.D.C. protection is no longer itself a disabled reason.
+
+### Declare attack
+
+Declaration continues to:
+
+1. load and rederive attacker and defender;
+2. reject self, missing, unready, or dead combatants;
+3. derive the selected owned weapon profile;
+4. derive current protection and require any per-suit armor roll;
+5. parse explicit combat context;
+6. roll the strike on the server;
+7. persist an immediate miss or a pending defense; and
+8. return only stable exchange/result data.
+
+M.D. weapon modes are no longer refused. No damage die is rolled for an immediate
+miss.
+
+### Respond to attack
+
+Response retains the exactly-once transaction:
+
+1. load a pending exchange;
+2. rederive both combatants;
+3. compare weapon, attack, context, defense options, state tokens, life state, and
+ current protection;
+4. parse and authorize the selected response;
+5. roll defense when required;
+6. roll native damage only for a hit;
+7. resolve the tier-aware route;
+8. atomically patch armor/body/life state when changed; and
+9. replace pending history with the immutable resolved result.
+
+Any changed route-relevant state finalizes the exchange as stale before defense or
+damage dice and before character writes.
+
+### Existing character mutations
+
+Every mutation that can change resources or combat readiness is audited against
+the terminal-state invariant. Dead characters cannot:
+
+- take additional manual damage;
+- heal or receive battle-injury treatment;
+- rest or restore resources;
+- declare, receive, or respond to a live combat action as an active combatant.
+
+Read queries and immutable combat history remain available. A pre-existing
+pending exchange involving a newly dead combatant may still be cancelled as
+ledger cleanup; any attempted resolution finalizes it as stale before dice or
+character writes. Inventory management does not clear or bypass the terminal
+marker.
+
+## Web experience
+
+### Weapon and target selection
+
+Energy pistols and rifles become selectable and keep explicit `M.D.` units in
+their labels. Tier is informational and cannot be edited.
+
+M.D.C.-armored targets become legal. A target with an unrolled dice-capacity suit
+is disabled with a precise instruction to roll the suit's M.D.C. first. Dead
+characters are disabled with terminal-state text.
+
+### Declaration and response
+
+The existing GM-context and defense forms remain unchanged. M.D. attacks use the
+same ranged awareness, dodge, automatic-dodge, modifier, and response authority as
+their weapon category requires.
+
+No new conversion, armor, or fatality controls appear. These are outputs.
+
+### Result presentation
+
+The recent-history formatter renders explicit route evidence, including examples
+such as:
+
+```text
+96 S.D.C. -> M.D.C. ARMOR IMPERVIOUS - NO EFFECT
+250 S.D.C. -> 2 M.D. :: ARMOR 70 -> 68
+21 M.D. :: ARMOR 3 -> 0 :: FINAL BLAST ABSORBED
+1 M.D. -> 100 S.D.C. :: UNPROTECTED BODY :: FATAL
+DEPLETED SHELL STOPPED STRIKE 7
+DEPLETED SHELL BYPASSED :: BODY S.D.C. 12 -> 4
+```
+
+Legacy Issue #44 results continue through their existing formatter branch.
+
+Fatal outcomes use the existing blood-red signal and explicit text such as
+`LIFE SIGNS TERMINATED`. M.D.C. is technological, not magical, so it never uses
+ley cyan. Stopped/no-effect armor results use the machine's amber voice; defended
+results retain confirmed green.
+
+### Terminal dossier state
+
+The dossier remains navigable and readable after death. Vitals show the terminal
+state without relying on color alone. Damage, healing, treatment, rest, and combat
+controls are unavailable with a concise explanation. Historical exchanges remain
+visible.
+
+### Navigation and asynchronous ownership
+
+The Issue #44 ownership model remains mandatory:
+
+- parameterized routes do not remount;
+- route ID changes reset weapon/target/context drafts, incoming/outgoing/recent
+ feeds, notices, telemetry, and row-local state;
+- in-flight declaration, response, and cancellation results carry route ID plus
+ monotonic epoch; and
+- results are ignored when ownership no longer matches.
+
+## Error handling
+
+Stable structured failures cover:
+
+- dead attacker or defender;
+- missing per-suit M.D.C. roll;
+- changed weapon or attack tier;
+- changed armor identity, pool, or life state;
+- illegal context or defense;
+- missing combatant or exchange; and
+- non-pending exchange operations.
+
+The following are successful persisted outcomes, not errors:
+
+- S.D.C. below the intact-M.D.C. threshold;
+- depleted shell stopping a low S.D.C. strike;
+- final-blast absorption;
+- a survivable coma result; and
+- a deterministic fatal result.
+
+## Migration and compatibility
+
+Convex schema deployment adds optional character state and expanded union
+branches. No data rewrite or backfill runs.
+
+Compatibility requirements:
+
+- existing characters derive without a terminal marker;
+- existing resolved S.D.C. history remains queryable and renderable;
+- new code accepts old cancelled and stale variants;
+- legacy pending exchanges can be cancelled or safely become stale; and
+- current indexes remain sufficient because exchange identity/status ownership
+ does not change.
+
+## Testing strategy
+
+### Rules package
+
+Pin printed constants and page stamps, then test the complete matrix:
+
+- 99, 100, 199, and 200 S.D.C. against intact M.D.C.;
+- the printed 450 and 496 S.D.C. round-down examples;
+- native M.D. against intact and nearly depleted M.D.C.;
+- final-blast absorption for native and converted damage;
+- depleted-shell completed strike totals 7 and 8;
+- M.D. at/below and above S.D.C. armor A.R., including no destroying-hit spill;
+- M.D. against an unprotected body;
+- exact `-P.E.` survival versus one point beyond fatal overflow;
+- critical multiplication before conversion;
+- malformed or mismatched native/converted route evidence;
+- life-state invariants and terminal mutation inputs;
+- old S.D.C. route compatibility; and
+- state-token sensitivity for tier, rules, depleted armor, and death.
+
+### Backend
+
+Verify:
+
+- target readiness for fixed, rolled, unrolled, intact, depleted, and dead state;
+- M.D. attack declaration through the existing exchange API;
+- no defense/damage dice or writes for invalid declarations;
+- authoritative native damage rolls and conversions;
+- atomic armor/body/death updates;
+- final-blast no-spill;
+- immutable stopped and fatal history;
+- stale-state rejection before rolls/writes;
+- two concurrent responses produce one winner and one character write;
+- all existing recovery/resource mutations reject dead characters; and
+- legacy exchange documents remain readable.
+
+### Web
+
+Verify:
+
+- M.D. weapons are selectable and accurately labelled;
+- M.D.C. protection is targetable when ready;
+- unrolled armor and dead targets explain why they are disabled;
+- every tiered route format includes units, conversion, and before/after evidence;
+- fatal state disables every relevant control without color-only meaning;
+- old S.D.C. history formatting is unchanged;
+- combat colors remain semantic with no non-magic cyan;
+- async ownership rejects late results after route changes; and
+- history disclosure/accessibility contracts remain intact.
+
+## Live-browser acceptance
+
+Use at least two local dossiers and server-owned dice to verify:
+
+1. M.D. against intact M.D.C. armor.
+2. A final M.D.C. point absorbing an oversized blast with no body change.
+3. A subsequent M.D. hit reaching the now-unprotected wearer.
+4. S.D.C. below 100 producing an immutable no-effect route.
+5. S.D.C. at/above 100 converting and ablating M.D.C.
+6. Depleted-shell S.D.C. strike totals on both sides of 8.
+7. M.D. against an unprotected character producing deterministic death.
+8. The dead dossier remaining readable while damage, recovery, rest, and combat
+ controls remain unavailable.
+9. Old S.D.C. history beside new tier-aware results.
+10. A-to-B route changes with pending/completed exchanges and no stale UI state.
+11. Keyboard and screen-reader labels, narrow-rail overflow, and clean consoles.
+
+## Validation gates
+
+Before publication, run with fresh, time-scoped evidence:
+
+```text
+vp run @riftforge/rules#check
+vp run @riftforge/rules#test
+vp run @riftforge/backend#check
+vp run @riftforge/backend#test
+vp run @riftforge/web#check
+vp run @riftforge/web#test
+vp check
+vp test
+git diff --check
+```
+
+User-visible changes also require the live-browser acceptance above. Test counts
+must be quoted as observations from the final branch revision, never as timeless
+project totals.
+
+## Delivery evidence
+
+This delivery record was assembled on **2026-07-22 (Asia/Singapore)**. The live
+acceptance itself ran on **2026-07-21 (Asia/Singapore)** against source commit
+`f67236e2badbb539dcd3b866ed5fb9ace1e915b6`; the automated gates below were rerun
+fresh on the final documentation revision before the local documentation commit.
+
+### Fresh automated gates
+
+Captured **2026-07-22 01:44:28 +08:00 (Asia/Singapore)** with package task
+caching explicitly disabled. Counts are observations from this branch revision,
+not timeless project totals.
+
+| Command | Fresh result |
+| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| `vp run --no-cache @riftforge/rules#check` | exit 0; 60/60 files formatted; 46/46 files free of warnings, lint errors, and type errors |
+| `vp run --no-cache @riftforge/rules#test` | exit 0; 17/17 files; 358/358 tests |
+| `vp run --no-cache @riftforge/backend#check` | exit 0; 13/13 files formatted; 15/15 files free of warnings, lint errors, and type errors |
+| `vp run --no-cache @riftforge/backend#test` | exit 0; 3/3 files; 128/128 tests |
+| `vp run --no-cache @riftforge/web#check` | exit 0; 37/37 files formatted; 33/33 files free of warnings, lint errors, and type errors |
+| `vp run --no-cache @riftforge/web#test` | exit 0; 3/3 files; 52/52 tests |
+| `vp check` | exit 0; 130/130 files formatted; 95/95 files free of warnings, lint errors, and type errors |
+| `vp test` | exit 0; 23/23 files; 538/538 tests |
+| `git diff --check` | exit 0; no output |
+| `git status --short --branch` | `feat/mdc-combat-exchange`; only README, this design, and the implementation plan modified before the documentation commit |
+
+### Review publication
+
+- Draft PR [#55](https://github.com/StreamDemon/RiftForge/pull/55) opened against
+ `main` on **2026-07-22 01:51:21 +08:00 (Asia/Singapore)** from published head
+ `1ce2130adadc06bafe473236f3db0e52fc844265`.
+- Issue #51 received the verified delivery evidence and draft-PR link at
+ **2026-07-22 01:51:57 +08:00 (Asia/Singapore)** in
+ [issue comment 5037268203](https://github.com/StreamDemon/RiftForge/issues/51#issuecomment-5037268203).
+- Cubic exhausted its monthly review allowance and is unavailable until
+ 2026-08-01. The maintainer designated CodeRabbit as the active reviewer until
+ Cubic returns and marked PR #55 ready for review.
+- CodeRabbit reviewed the complete `3067c99..4e0afc5` change set and submitted
+ `APPROVED` at **2026-07-22 03:04:16 +08:00 (Asia/Singapore)** with zero inline
+ threads. The human maintainer retains sole merge authority.
+- A full re-review through `4f5f0bb` completed at
+ **2026-07-22 03:27:47 +08:00 (Asia/Singapore)** with one trivial nit: the two
+ source-contract test helpers returned an empty string for a missing file,
+ allowing negative assertions to pass vacuously. Both duplicated helpers now
+ throw with the missing relative path. RED failed 2/48 as expected; GREEN passed
+ 48/48, the web package passed 54/54, and root validation passed 540/540 with
+ 130/130 files formatted and 95/95 files lint/type clean.
+
+### Live Convex and browser acceptance
+
+- Attacker dossier: `j5765xjcskpg5jwrj32qx3br9d8azgwe`.
+- Defender dossier: `j57b5e6myeyxxhffcqwy09kg8n8ayxw9`, with 39 rolled
+ M.D.C. staged at 3 current M.D.C. for the ablation sequence.
+- A legal Wilk's `1D6 M.D.` profile dealt 1 M.D., reducing armor from 3 to 2.
+- An NG-P7 10 M.D. hit reduced armor from 2 to 0, rendered
+ `FINAL BLAST ABSORBED`, and left body pools at 120 S.D.C./18 H.P.
+- With the same depleted shell still worn, exact completed strike 7 was stopped;
+ exact strike 8 bypassed and reduced body S.D.C. from 120 to 115.
+- A 6 S.D.C. hit against intact 39 M.D.C. persisted an impervious/no-change
+ result. A later 5 M.D. hit converted to 500 S.D.C., stored body pools at
+ 0/-14, and persisted `DEAD`.
+- Six version-2 resolved rows rendered beside legacy/null resolved history.
+ Cancellation cleanup remained available and recorded `CANCELLED` after death.
+- The dead dossier kept identity, vitals, equipment, inventory, narrative, and
+ history readable while gameplay actions exposed the terminal reason.
+- Direct `/characters/:id` navigation from the dead defender to the living
+ attacker stayed in the same document: the window marker,
+ `performance.timeOrigin`, and sole Navigation Timing entry were unchanged.
+ Drafts reset, living controls re-enabled, and the dead target remained disabled.
+- Desktop acceptance ran at 1440px. Narrow acceptance ran at 390x844; browser
+ chrome yielded a 375 CSS-pixel document, and document/body scroll width equaled
+ client width with no overflowing elements.
+- `SHOW20` exposed `aria-controls`; keyboard activation updated `aria-expanded`
+ and rendered 20 rows. Fatal/body, stopped/armor, defended, and dim history
+ states retained explicit text in addition to color.
+- A fresh Chrome session reported zero console errors and zero warnings.
+- Web, Convex CLI, the orphan-prone local backend, and temporary logs were stopped
+ or removed; ports 5173 and 3210 were closed at handoff.
+
+The legal production catalog has no S.D.C. armor and no S.D.C. attack profile
+capable of reaching 100+ completed damage. Consequently, the 100+ S.D.C.-against-
+M.D.C. conversion rows and M.D.-against-S.D.C.-armor A.R. rows were verified by
+exact pure-rules and atomic-backend tests, not by the browser UI. No temporary or
+invented rules content was introduced to manufacture those scenarios.
+
+## Tracker and documentation delivery
+
+- Keep Issue #51 aligned with the implemented core scope and rendered evidence.
+- Keep optional near-fatal survival in Issue #54.
+- Keep the generic page-stamped effect pipeline in Issue #53.
+- Update README/current-status wording once M.D.C. exchanges are live.
+- Record explicit exclusions and final validation evidence in the PR and Issue
+ #51.
+- Follow branch -> PR -> active automated review -> human merge. CodeRabbit is
+ the temporary review gate while Cubic is quota-blocked. Never merge the PR from
+ the agent workflow.
+
+## Success criteria
+
+Issue #51 is complete when:
+
+- printed M.D.C. constants are page-stamped and load-validated;
+- one pure resolver covers the approved S.D.C./M.D.C. routing matrix;
+- existing history remains valid without a rewrite;
+- Convex persists server-authorized armor/body/death outcomes atomically and
+ exactly once;
+- terminal death cannot be bypassed by existing mutations;
+- the SolidJS rail presents all new route evidence and terminal state;
+- package, root, diff, and live-browser gates pass; and
+- the branch is published as a PR, the active automated reviewer approves it,
+ and the human maintainer retains merge authority.
diff --git a/README.md b/README.md
index ad3fa5f..57a76de 100644
--- a/README.md
+++ b/README.md
@@ -29,13 +29,16 @@ apps/web/
The engine is isomorphic: the Convex backend derives sheets at query time, and the
client imports the same package for ephemeral rolls. The runtime never parses the
-source book. Combat APIs now support persisted, server-rolled S.D.C. weapon
-exchanges with engine-authorized defense choices, immutable history, and atomic
-body S.D.C.-then-Hit-Point routing. Artificial S.D.C. armor A.R. and ablation
-routing are pure-tested and ready for page-stamped production content. Natural
-A.R. is intentionally absent per RUE p.339; full M.D.C. interaction remains
-follow-up work. Spell APIs expose load-validated structured damage, derivation,
-and injected-RNG rolling.
+source book. Combat APIs support persisted, server-rolled S.D.C. and M.D. weapon
+exchanges through one engine-authorized protocol. The delivered core covers
+critical-before-conversion damage, S.D.C./M.D.C. cross-tier routing, S.D.C. armor
+A.R. and ablation, intact and depleted M.D.C. armor, final-blast absorption with
+no spill, immutable route evidence, atomic body damage, and explicit terminal
+death. Legacy S.D.C. exchange history remains readable without migration.
+Natural A.R. is intentionally absent per RUE p.339. The optional near-fatal M.D.
+injury and survival procedure remains deferred to issue #54, and a generic
+page-stamped effects pipeline remains roadmap work in issue #53. Spell APIs expose
+load-validated structured damage, derivation, and injected-RNG rolling.
### Current status
diff --git a/apps/web/index.html b/apps/web/index.html
index ac8e5a6..3b2d74e 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -4,6 +4,7 @@
{(alignment) => (
@@ -387,7 +397,7 @@ export function SheetView(props: { sheet: CharacterSheet; actions?: SheetActions
// to show — hide the block unless at least one row has a value.
when={APPEARANCE_ROWS.some(([field]) => s().narrative?.appearance?.[field])}
>
-