+ >
+ )}
>
)}
@@ -1276,6 +1301,25 @@ function TransposedTable({
});
}
+ if (data.some(y => y.waIncomeTax > 0.01 || y.waCapitalGainsTaxCredit > 0.01)) {
+ sections.push({
+ title: 'Washington Income Tax (2028+)',
+ rows: [
+ {
+ label: 'WA Income Tax',
+ contentKey: 'col-wa-income-tax',
+ cell: y => negMoney(y.waIncomeTax),
+ className: () => 'negative',
+ },
+ {
+ label: 'WA CGT Credit',
+ contentKey: 'col-wa-cgt-credit',
+ cell: y => moneyOrDash(y.waCapitalGainsTaxCredit),
+ },
+ ],
+ });
+ }
+
if (data.some(y => y.extensionFraction < 1)) {
sections.push({
title: 'Deleverage',
diff --git a/src/calculations/core.ts b/src/calculations/core.ts
index 6fc5c57..d422981 100644
--- a/src/calculations/core.ts
+++ b/src/calculations/core.ts
@@ -14,6 +14,7 @@ import {
getStateRate,
getStateTaxProfile,
computeLtcgExcise,
+ computeWashingtonYearTaxImpact,
} from '../taxData';
import {
getStrategy,
@@ -392,7 +393,7 @@ export function calculateWithOverrides(
splitDlvSt += wgt * legSt;
splitDlvLt += wgt * leg.strategy.ltGainRate;
splitDlvFin += wgt * getEffectiveFinancingCost(leg.strategy, settings);
- splitDlvExtension += wgt * 1;
+ splitDlvExtension += Number(wgt);
}
}
}
@@ -890,10 +891,10 @@ export function calculateYear(
// Benefits:
// 1. Ordinary loss reduces W2 income tax
- const ordinaryLossBenefit = safeNumber(usableOrdinaryLoss * combinedOrdinaryRate);
+ let ordinaryLossBenefit = safeNumber(usableOrdinaryLoss * combinedOrdinaryRate);
// 2. Capital loss carryforward used against ordinary income ($3k/yr limit)
- const capitalLossBenefit = safeNumber(capitalLossUsedAgainstIncome * combinedOrdinaryRate);
+ let capitalLossBenefit = safeNumber(capitalLossUsedAgainstIncome * combinedOrdinaryRate);
// 3. NOL used against taxable income. The STATE component is suppressed
// when the state suspends NOL deductions (CA SB 167: MAGI ≥ $1M through
@@ -923,13 +924,41 @@ export function calculateYear(
overrides?.stateNolAvailable !== undefined
? Math.min(nolUsed, overrides.stateNolAvailable)
: nolUsed;
- const nolUsageBenefit =
+ let nolUsageBenefit =
stateEligibleNol >= nolUsed
? safeNumber(nolAtOrdinary * (ordinaryRate + nolStateRate) + nolAtLt * ltNolRate)
: safeNumber(
nolAtOrdinary * ordinaryRate + nolAtLt * fedLtNolRate + stateEligibleNol * nolStateRate
);
+ // D-030: projection year 1 is tax year 2026. Washington's enacted income
+ // tax begins in 2028 (projection year 3) and credits capital-gains excise
+ // already paid. Compare each deduction step so the benefit attribution
+ // still reconciles to the headline without double-counting the credit.
+ const strategyLtcgExciseTax = computeLtcgExcise(taxableLt, state.ltcgExcise);
+ const totalLtcgExciseTax = computeLtcgExcise(taxableLt + eventTaxableLt, state.ltcgExcise);
+ const eventLtcgExciseTax = totalLtcgExciseTax - strategyLtcgExciseTax;
+ const waImpact =
+ inputs.stateCode === 'WA'
+ ? computeWashingtonYearTaxImpact({
+ taxYear: 2025 + year,
+ ordinaryIncome: effectiveIncome,
+ strategyStGain: taxableSt,
+ strategyLtGain: taxableLt,
+ eventStGain: eventTaxableSt,
+ eventLtGain: eventTaxableLt,
+ ordinaryDeduction: usableOrdinaryLoss,
+ capitalLossDeduction: capitalLossUsedAgainstIncome,
+ nolDeduction: nolUsed,
+ strategyCapitalGainsExcise: strategyLtcgExciseTax,
+ eventCapitalGainsExcise: eventLtcgExciseTax,
+ })
+ : null;
+
+ ordinaryLossBenefit = safeNumber(ordinaryLossBenefit + (waImpact?.ordinaryLossBenefit ?? 0));
+ capitalLossBenefit = safeNumber(capitalLossBenefit + (waImpact?.capitalLossBenefit ?? 0));
+ nolUsageBenefit = safeNumber(nolUsageBenefit + (waImpact?.nolUsageBenefit ?? 0));
+
// Costs:
// 1. LT gains are taxed at LT rates (+ WA-style excise above the annual exemption).
// Charged on TAXABLE LT gains (after carryforward and current-year ST loss
@@ -937,24 +966,28 @@ export function calculateYear(
// losses are consumed by QFAF gains), but in collateral-only mode harvested
// ST losses offset the LT gains and the cost would otherwise be overstated,
// inflating the incremental benefit of adding the QFAF. (CPA review finding E.)
- const ltcgExciseTax = computeLtcgExcise(taxableLt, state.ltcgExcise);
- const ltGainCost = safeNumber(taxableLt * combinedLtRate + ltcgExciseTax);
+ const ltcgExciseTax = strategyLtcgExciseTax;
+ const ltGainCost = safeNumber(
+ taxableLt * combinedLtRate + ltcgExciseTax + (waImpact?.strategyGainCost ?? 0)
+ );
// Planned gain event (D-012): taxed separately — it is exogenous, so it is
// NOT charged against the strategy's taxSavings. The program's help shows
// up as carryforward shelter (event-last), §461(l) absorption, and NOL
// usage. Marginal excise above the strategy's own gains is event-borne.
const gainEventAmount = eventStGain + eventLtGain;
- const totalExcise = computeLtcgExcise(taxableLt + eventTaxableLt, state.ltcgExcise);
+ const totalExcise = totalLtcgExciseTax;
const gainEventTax = safeNumber(
eventTaxableSt * combinedStRate +
eventTaxableLt * combinedLtRate +
- (totalExcise - ltcgExciseTax)
+ (totalExcise - ltcgExciseTax) +
+ (waImpact?.gainEventTax ?? 0)
);
const gainEventTaxWithoutStrategy = safeNumber(
eventStGain * combinedStRate +
eventLtGain * combinedLtRate +
- computeLtcgExcise(eventLtGain, state.ltcgExcise)
+ computeLtcgExcise(eventLtGain, state.ltcgExcise) +
+ (waImpact?.gainEventTaxWithoutStrategy ?? 0)
);
const gainEventCfShelter = safeNumber(
eventStGain - eventTaxableSt + (eventLtGain - eventTaxableLt)
@@ -1117,6 +1150,8 @@ export function calculateYear(
nolUsedThisYear: nolUsed,
capitalLossUsedAgainstIncome,
stateNolExpired: 0, // Set by the calling loop's vintage ledger (D-020)
+ waIncomeTax: safeNumber(waImpact?.incomeTax ?? 0),
+ waCapitalGainsTaxCredit: safeNumber(waImpact?.capitalGainsTaxCredit ?? 0),
effectiveStLossRate,
incomeOffsetAmount,
maxIncomeOffsetCapacity,
diff --git a/src/calculations/exitTax.ts b/src/calculations/exitTax.ts
index 96e4f54..dabfb41 100644
--- a/src/calculations/exitTax.ts
+++ b/src/calculations/exitTax.ts
@@ -1,5 +1,5 @@
import { CalculationResult } from '../types';
-import { StateTaxProfile, computeLtcgExcise } from '../taxData';
+import { StateTaxProfile, computeLtcgExcise, computeWashingtonIncomeTax } from '../taxData';
import { safeNumber } from '../utils/formatters';
/**
@@ -76,7 +76,9 @@ export function computeExitTaxAnalysis(
/** WA-style LTCG excise applied to a single-year full liquidation (D-005) */
ltcgExcise?: StateTaxProfile['ltcgExcise'],
/** Cost basis of the collateral at inception (default: equals initial value) */
- collateralCostBasis?: number
+ collateralCostBasis?: number,
+ /** D-030: context for the enacted WA income tax at the liquidation horizon. */
+ washingtonContext?: { stateCode: string; ordinaryIncome: number }
): ExitTaxAnalysis {
const years = result.years;
const initialCollateral = result.sizing.collateralValue;
@@ -118,8 +120,21 @@ export function computeExitTaxAnalysis(
const cfShelterUsed = Math.min(remainingCapitalLossCf, embeddedGain);
const taxableGainAfterShelter = Math.max(0, safeNumber(embeddedGain - cfShelterUsed));
const exciseOn = (gain: number) => computeLtcgExcise(gain, ltcgExcise);
+ const horizonTaxYear = 2025 + years.length;
+ const waIncomeTaxOnGain = (gain: number) => {
+ if (washingtonContext?.stateCode !== 'WA') return 0;
+ const baseTax = computeWashingtonIncomeTax(horizonTaxYear, washingtonContext.ordinaryIncome, 0);
+ const withGain = computeWashingtonIncomeTax(
+ horizonTaxYear,
+ washingtonContext.ordinaryIncome + gain,
+ exciseOn(gain)
+ );
+ return Math.max(0, withGain.netTax - baseTax.netTax);
+ };
const exitTax = safeNumber(
- taxableGainAfterShelter * combinedLtRate + exciseOn(taxableGainAfterShelter)
+ taxableGainAfterShelter * combinedLtRate +
+ exciseOn(taxableGainAfterShelter) +
+ waIncomeTaxOnGain(taxableGainAfterShelter)
);
// Passive baseline: buy-and-hold the same initial collateral at the same
@@ -129,7 +144,9 @@ export function computeExitTaxAnalysis(
// The passive holder carries the same pre-existing gain, so it cancels out
// of the incremental deferred tax (which stays strategy-attributable).
const passiveGain = Math.max(0, safeNumber(passiveValue - costBasis));
- const passiveExitTax = safeNumber(passiveGain * combinedLtRate + exciseOn(passiveGain));
+ const passiveExitTax = safeNumber(
+ passiveGain * combinedLtRate + exciseOn(passiveGain) + waIncomeTaxOnGain(passiveGain)
+ );
// Deliberately unclamped: with QFAF off, carryforward shelter can push the
// strategy's exit tax BELOW the passive baseline's. Clamping at 0 would
diff --git a/src/calculations/sensitivity.ts b/src/calculations/sensitivity.ts
index 75007f7..8ad01e9 100644
--- a/src/calculations/sensitivity.ts
+++ b/src/calculations/sensitivity.ts
@@ -14,6 +14,7 @@ import {
getStateRate,
getStateTaxProfile,
computeLtcgExcise,
+ computeWashingtonYearTaxImpact,
} from '../taxData';
import {
getStrategy,
@@ -526,8 +527,8 @@ function calculateYearWithSensitivity(
const combinedOrdinaryRate = ordinaryRate + stateDeductionRate;
// Benefits
- const ordinaryLossBenefit = safeNumber(usableOrdinaryLoss * combinedOrdinaryRate);
- const capitalLossBenefit = safeNumber(capitalLossUsedAgainstIncome * combinedOrdinaryRate);
+ let ordinaryLossBenefit = safeNumber(usableOrdinaryLoss * combinedOrdinaryRate);
+ let capitalLossBenefit = safeNumber(capitalLossUsedAgainstIncome * combinedOrdinaryRate);
// State NOL component suppressed under CA-style suspension (see core.ts)
const stateNolSuspended =
state.nolStateSuspension !== undefined &&
@@ -550,16 +551,39 @@ function calculateYearWithSensitivity(
overrides?.stateNolAvailable !== undefined
? Math.min(nolUsed, overrides.stateNolAvailable)
: nolUsed;
- const nolUsageBenefit =
+ let nolUsageBenefit =
stateEligibleNol >= nolUsed
? safeNumber(nolAtOrdinary * (ordinaryRate + nolStateRate) + nolAtLt * ltNolRate)
: safeNumber(
nolAtOrdinary * ordinaryRate + nolAtLt * fedLtNolRate + stateEligibleNol * nolStateRate
);
+ const strategyLtcgExciseTax = computeLtcgExcise(taxableLt, state.ltcgExcise);
+ const waImpact =
+ inputs.stateCode === 'WA'
+ ? computeWashingtonYearTaxImpact({
+ taxYear: 2025 + year,
+ ordinaryIncome: inputs.annualIncome,
+ strategyStGain: taxableSt,
+ strategyLtGain: taxableLt,
+ eventStGain: 0,
+ eventLtGain: 0,
+ ordinaryDeduction: usableOrdinaryLoss,
+ capitalLossDeduction: capitalLossUsedAgainstIncome,
+ nolDeduction: nolUsed,
+ strategyCapitalGainsExcise: strategyLtcgExciseTax,
+ eventCapitalGainsExcise: 0,
+ })
+ : null;
+ ordinaryLossBenefit = safeNumber(ordinaryLossBenefit + (waImpact?.ordinaryLossBenefit ?? 0));
+ capitalLossBenefit = safeNumber(capitalLossBenefit + (waImpact?.capitalLossBenefit ?? 0));
+ nolUsageBenefit = safeNumber(nolUsageBenefit + (waImpact?.nolUsageBenefit ?? 0));
+
// Costs: charged on taxable (post-offset) LT gains — see core.ts (CPA finding E)
- const ltcgExciseTax = computeLtcgExcise(taxableLt, state.ltcgExcise);
- const ltGainCost = safeNumber(taxableLt * combinedLtRate + ltcgExciseTax);
+ const ltcgExciseTax = strategyLtcgExciseTax;
+ const ltGainCost = safeNumber(
+ taxableLt * combinedLtRate + ltcgExciseTax + (waImpact?.strategyGainCost ?? 0)
+ );
const remainingStGainCost = safeNumber(Math.max(0, netStGainLoss) * combinedStRate);
// Net tax savings: ordinary deductions minus capital gains costs
@@ -675,6 +699,8 @@ function calculateYearWithSensitivity(
nolUsedThisYear: nolUsed,
capitalLossUsedAgainstIncome,
stateNolExpired: 0, // Set by the calling loop's vintage ledger (D-020)
+ waIncomeTax: safeNumber(waImpact?.incomeTax ?? 0),
+ waCapitalGainsTaxCredit: safeNumber(waImpact?.capitalGainsTaxCredit ?? 0),
effectiveStLossRate: adjustedStLossRate,
incomeOffsetAmount,
maxIncomeOffsetCapacity,
diff --git a/src/components/DisclaimerFooter.tsx b/src/components/DisclaimerFooter.tsx
index f7edef2..e892079 100644
--- a/src/components/DisclaimerFooter.tsx
+++ b/src/components/DisclaimerFooter.tsx
@@ -77,8 +77,10 @@ export function DisclaimerFooter() {
This calculator provides estimates for illustrative purposes only and does not constitute
investment, tax, or legal advice. Past performance does not guarantee future results.
Federal tax parameters (brackets, LTCG thresholds, §461(l) limits) verified against Rev.
- Proc. 2025-32 in June 2026. WA's 2026 capital-gains exemption was unpublished at review time
- (2025 figure used); verify state figures before relying on them.
+ Proc. 2025-32 and IRS 2026 guidance, reverified July 2026. WA's 2026 capital-gains exemption
+ remains unpublished (2025 figure used). The enacted WA income tax is modeled from 2028 with
+ its capital-gains credit; implementation details remain provisional pending final DOR
+ regulations and forms. Verify state figures before relying on them.
v{version}
diff --git a/src/components/FloatingPinnedPanel.tsx b/src/components/FloatingPinnedPanel.tsx
index 1f64f0d..2b74a6b 100644
--- a/src/components/FloatingPinnedPanel.tsx
+++ b/src/components/FloatingPinnedPanel.tsx
@@ -230,7 +230,9 @@ export function FloatingPinnedPanel({
// Derive ordered elements from itemOrder
const orderedElements =
itemOrder.length > 0
- ? itemOrder.map(id => elements.find(e => e.id === id)).filter((e): e is PinnedElement => !!e)
+ ? itemOrder
+ .map(id => elements.find(e => e.id === id))
+ .filter((e): e is PinnedElement => Boolean(e))
: elements;
// --- Panel drag ---
@@ -356,10 +358,11 @@ export function FloatingPinnedPanel({
const handleItemResizeMove = useCallback(
(e: React.PointerEvent) => {
- if (!itemResizing || !itemResizeStartRef.current) return;
- const dy = e.clientY - itemResizeStartRef.current.py;
- const newH = Math.max(MIN_ITEM_HEIGHT, itemResizeStartRef.current.h + dy);
- setItemHeights(prev => ({ ...prev, [itemResizeStartRef.current!.id]: newH }));
+ const resizeStart = itemResizeStartRef.current;
+ if (!itemResizing || !resizeStart) return;
+ const dy = e.clientY - resizeStart.py;
+ const newH = Math.max(MIN_ITEM_HEIGHT, resizeStart.h + dy);
+ setItemHeights(prev => ({ ...prev, [resizeStart.id]: newH }));
},
[itemResizing]
);
diff --git a/src/components/MeetingMode/MeetingMode.test.tsx b/src/components/MeetingMode/MeetingMode.test.tsx
index 54aaa2d..c65aa4f 100644
--- a/src/components/MeetingMode/MeetingMode.test.tsx
+++ b/src/components/MeetingMode/MeetingMode.test.tsx
@@ -136,6 +136,62 @@ describe('Meeting Mode handout — QFAF mode (D-021)', () => {
// Protection ratio is an EDI-economics metric — not shown with QFAF on.
expect(text).not.toContain('Protection ratio:');
});
+
+ it('classifies client outcomes without adding unused tax assets to realized savings', () => {
+ const { getByTestId } = renderMeetingMode(
+ createInputs({ qfafEnabled: true }),
+ DEFAULT_SETTINGS
+ );
+ const text = getByTestId('mm-client-outcomes').textContent ?? '';
+ expect(text).toContain('Realized modeled savings');
+ expect(text).toContain('Unused tax assets');
+ expect(text).toContain('not added to savings');
+ expect(text).toContain('Incremental tax at liquidation');
+ expect(text).toContain('Net after liquidation');
+ });
+
+ it('shows cumulative financing cost and the modeled rate schedule when fees are enabled', () => {
+ const { container } = renderMeetingMode(createInputs({ qfafEnabled: true }), {
+ ...DEFAULT_SETTINGS,
+ financingFeesEnabled: true,
+ });
+ const text = container.textContent ?? '';
+ expect(text).toContain('Financing cost:');
+ expect(text).toContain('cumulative, already reflected in projected wealth');
+ expect(text).toContain('Modeled using the simple financing schedule');
+ expect(text).toContain('not subtracted from the tax-savings headline');
+ expect(text).toContain('later losses, and NOL utilization');
+ });
+});
+
+describe('Meeting Mode gain-event bridge', () => {
+ it('surfaces event amount, shelter, with-strategy tax, and counterfactual tax', () => {
+ const inputs = createInputs({ qfafEnabled: true });
+ const props = buildProps(inputs, DEFAULT_SETTINGS);
+ const years = props.results.years.map((year, index) =>
+ index === 2
+ ? {
+ ...year,
+ gainEventAmount: 10_000_000,
+ gainEventCfShelter: 4_000_000,
+ gainEventTax: 1_500_000,
+ gainEventTaxWithoutStrategy: 2_400_000,
+ }
+ : year
+ );
+ const { getByTestId } = render(
+
+ );
+ const text = getByTestId('meeting-gain-event-bridge').textContent ?? '';
+ expect(text).toContain('$10.00M event in Year 3');
+ expect(text).toContain('$4.00M sheltered');
+ expect(text).toContain('$1.50M modeled event tax');
+ expect(text).toContain('$2.40M tax without the strategy');
+ expect(text).toContain('$900K');
+ expect(text).toContain('not added to strategy tax savings');
+ expect(text).toContain("adding the event can change when the strategy's NOL is used");
+ expect(text).toContain('event tax above remains a separate comparison');
+ });
});
// ─── D-022: the "How we get to {headline}" strip must SUM EXACTLY ───
@@ -215,6 +271,23 @@ describe('Meeting Mode decomposition sums to the headline (D-022)', () => {
});
});
+describe('Meeting Mode selected-year explanation', () => {
+ it('reconciles the selected engine year into benefits, costs, and net savings', () => {
+ const { getByText, getByTestId } = renderMeetingMode(
+ createInputs({ qfafEnabled: true }),
+ DEFAULT_SETTINGS
+ );
+ fireEvent.click(getByText('Detail'));
+ const text = getByTestId('mm-selected-year-reconciliation').textContent ?? '';
+ expect(text).toContain('Explain Year 1');
+ expect(text).toContain('ordinary-loss benefit');
+ expect(text).toContain('NOL-use benefit');
+ expect(text).toContain('capital-loss benefit');
+ expect(text).toContain('gain costs');
+ expect(text).toContain('future tax asset, not a current-year benefit');
+ });
+});
+
describe('Meeting Mode mechanics view (mock-meeting review)', () => {
it('EDI mode: no QFAF steps — harvest → reserve → shelter with real numbers', () => {
const { container, getByText } = renderMeetingMode(createInputs(), DEFAULT_SETTINGS);
@@ -238,9 +311,35 @@ describe('Meeting Mode mechanics view (mock-meeting review)', () => {
expect(text).toContain('§475(f)');
// $3M income, MFJ: year-1 ordinary losses far exceed the $512K cap.
expect(text).toContain('Year 1 ordinary deduction capped at $512,000 (MFJ)');
+ expect(text).toContain("Fund's modeled §475(f) ordinary losses offset ordinary income");
+ expect(text).toContain('Capital losses from the collateral overlay remain capital');
expect(text).toContain('Wash-sale assumption');
// CA scenario → one-line state caveat from the shared warning util.
expect(text).toContain('Modeled per California law');
+ expect(getByText('Mechanics').ownerDocument.body.textContent).toContain(
+ 'Federal vs. California timing'
+ );
+ expect(container.querySelector('[data-testid="mm-loss-character-map"]')?.textContent).toContain(
+ 'Capital loss'
+ );
+ expect(container.querySelector('[data-testid="mm-loss-character-map"]')?.textContent).toContain(
+ 'Fund ordinary loss'
+ );
+ expect(container.querySelector('[data-testid="mm-loss-character-map"]')?.textContent).toContain(
+ 'NOL carryforward'
+ );
+ });
+
+ it('labels Washington as a time-varying rate assumption', () => {
+ const { container, getByText } = renderMeetingMode(
+ createInputs({ qfafEnabled: true, stateCode: 'WA', stateRate: 0 }),
+ DEFAULT_SETTINGS
+ );
+ fireEvent.click(getByText('Mechanics'));
+ const text = container.textContent ?? '';
+ expect(text).toContain('WA current rate');
+ expect(text).toContain('0% individual income tax is shown for 2026–2027');
+ expect(text).toContain('enacted 9.9% income tax is modeled beginning in 2028');
});
it('explains the dead tail when the QFAF stops before the projection ends', () => {
@@ -280,6 +379,7 @@ describe('Meeting Mode comparison memory (D-024)', () => {
expect(chip.textContent).toContain(
`Was ${fmtCompact(oldHeadline)} → Now ${fmtCompact(newHeadline)}`
);
+ expect(getByTestId('mm-change-attribution').textContent).toContain('Why savings changed');
// Dismiss → chip disappears (baseline re-captured at "now").
fireEvent.click(getByLabelText('Dismiss comparison'));
diff --git a/src/components/MeetingMode/MeetingMode.tsx b/src/components/MeetingMode/MeetingMode.tsx
index 2e30722..0b3097a 100644
--- a/src/components/MeetingMode/MeetingMode.tsx
+++ b/src/components/MeetingMode/MeetingMode.tsx
@@ -1904,7 +1904,7 @@ function PrintPageHeader({
);
}
-function PrintPageFooter() {
+function PrintPageFooter({ financingIncluded }: { financingIncluded: boolean }) {
useBrandRevealed();
return (
Important disclosures. This is a hypothetical illustration for discussion
purposes only — not investment, tax, or legal advice, and not an offer of any security.
- Estimates do not reflect advisory fees, financing costs, tracking error, transaction costs, or
- behavioral effects; actual results will vary. Tax-loss harvesting reduces cost basis: a
- portion of projected savings is tax deferral that becomes due if the portfolio is liquidated
- (it may become permanent via basis step-up at death or charitable transfer). Ordinary loss
- deductions are limited by IRC §461(l) ($512K MFJ / $256K others, 2026); excess becomes an NOL
- usable against up to 80% of future taxable income. Projections assume 0–15% of harvested
- losses are disallowed as wash sales and that the{' '}
+ Estimates do not reflect advisory fees, tracking error, transaction costs, or behavioral
+ effects;{' '}
+ {financingIncluded
+ ? 'the selected financing-cost schedule is included'
+ : 'financing costs are excluded'}
+ ; actual results will vary. Tax-loss harvesting reduces cost basis: a portion of projected
+ savings is tax deferral that becomes due if the portfolio is liquidated (it may become
+ permanent via basis step-up at death or charitable transfer). Ordinary loss deductions are
+ limited by IRC §461(l) ($512K MFJ / $256K others, 2026); excess becomes an NOL usable against
+ up to 80% of future taxable income. Projections assume 0–15% of harvested losses are
+ disallowed as wash sales and that the{' '}
{brandText('QFAF (Quantinno Fundamental Arbitrage Fund)')} qualifies for the modeled tax
treatment under current IRS guidance, which may change. State treatment varies — CA, NY, PA,
NJ, MA, and WA differ materially from the federal rules modeled here. Consult qualified tax,
@@ -2030,9 +2034,11 @@ function MechanicsView({
},
{
n: 3,
- title: 'Loss harvesting generates NOL',
+ title: brandText('QFAF ordinary losses generate NOL'),
tone: '#f59e0b',
- body: `Realized short-term losses offset high-tax ordinary income first (combined ${fmtPercent1(taxRates.combinedOrdinaryRate)} ordinary rate), with the balance carried forward as NOL.`,
+ body: brandText(
+ `The QFAF's modeled §475(f) ordinary losses offset ordinary income, subject to §461(l), at the ${fmtPercent1(taxRates.combinedOrdinaryRate)} combined ordinary rate; excess becomes an NOL. Capital losses from the collateral overlay remain capital and follow §1211 netting.`
+ ),
metric: fmtCurrency(totalNol),
metricLabel: 'NOL generated',
},
@@ -2284,7 +2290,11 @@ function MechanicsView({
[
['Fed ST', fmtPercent(taxRates.federalStRate), false],
['Fed LT', fmtPercent(taxRates.federalLtRate), false],
- ['State', fmtPercent(taxRates.stateRate), false],
+ [
+ inputs.stateCode === 'WA' ? 'WA current rate' : 'State',
+ fmtPercent(taxRates.stateRate),
+ false,
+ ],
['Combined ST', fmtPercent(taxRates.combinedStRate), true],
['Ordinary (deductions)', fmtPercent(taxRates.combinedOrdinaryRate), true],
['Combined LT', fmtPercent(taxRates.combinedLtRate), true],
@@ -2315,8 +2325,82 @@ function MechanicsView({
))}
+ {inputs.stateCode === 'WA' && (
+
+ Washington changes during this projection: 0%
+ individual income tax is shown for 2026–2027; the enacted 9.9% income tax is modeled
+ beginning in 2028 after its statutory deduction and Washington capital-gains tax credit.
+ Actual liability depends on final guidance and the client's full return.
+
+ )}
+
+
+
+ {[
+ [
+ 'Capital loss',
+ 'Offsets capital gains',
+ 'Then up to $3,000/yr of ordinary income under §1211.',
+ ],
+ [
+ 'Fund ordinary loss',
+ 'Offsets ordinary income',
+ 'Modeled §475(f) treatment; current use is subject to §461(l).',
+ ],
+ [
+ 'NOL carryforward',
+ 'Offsets future taxable income',
+ 'Generally limited to 80% of taxable income in a later year.',
+ ],
+ ].map(([title, use, limit]) => (
+
+
{title}
+
{use}
+
+ {limit}
+
+
+ ))}
+ {inputs.stateCode === 'CA' && (
+
+ Federal vs. California timing: eligible federal
+ deductions and NOL use follow the federal limits shown above. For modeled MAGI of $1M or
+ more, California SB 167 suspends the state NOL deduction in tax year 2026, so the state
+ benefit may arrive later. A federal NOL does not automatically create an immediate
+ California benefit.
+
+ )}
+
{/* ─── For your CPA (4b) ─── Collapsible on screen; the print handout
renders the expanded variant instead (page 3, verified to still
paginate at exactly 3 sheets). Visibility is swapped via the
@@ -2462,6 +2546,27 @@ export function MeetingMode({
[results, exitAnalysis]
);
const insights = useMemo(() => computeEdiInsights(results), [results]);
+ const gainEventYears = useMemo(
+ () => visibleYears.filter(y => y.gainEventAmount > 0),
+ [visibleYears]
+ );
+ const gainEventTotals = useMemo(
+ () =>
+ gainEventYears.reduce(
+ (totals, y) => ({
+ amount: totals.amount + y.gainEventAmount,
+ tax: totals.tax + y.gainEventTax,
+ taxWithoutStrategy: totals.taxWithoutStrategy + y.gainEventTaxWithoutStrategy,
+ shelter: totals.shelter + y.gainEventCfShelter,
+ }),
+ { amount: 0, tax: 0, taxWithoutStrategy: 0, shelter: 0 }
+ ),
+ [gainEventYears]
+ );
+ const currentBenefitParts = useMemo(
+ () => computeBenefitDecomposition(visibleYears),
+ [visibleYears]
+ );
// Year focus defaults to Year 1 — the start of the story — not the final
// (often $0 wind-down) year (mock-meeting review, 7b).
@@ -2479,12 +2584,37 @@ export function MeetingMode({
const [chipBaseline, setChipBaseline] = useState(() => ({
savings: totalTaxSavings,
reserve: lossReserve,
+ benefitParts: currentBenefitParts,
}));
const chipSavingsMoved = Math.abs(totalTaxSavings - chipBaseline.savings) > 0.5;
const chipReserveMoved = ediMode && Math.abs(lossReserve - chipBaseline.reserve) > 0.5;
const showChangeChip = chipSavingsMoved || chipReserveMoved;
const dismissChangeChip = () =>
- setChipBaseline({ savings: totalTaxSavings, reserve: lossReserve });
+ setChipBaseline({
+ savings: totalTaxSavings,
+ reserve: lossReserve,
+ benefitParts: currentBenefitParts,
+ });
+ const changeAttribution = [
+ {
+ label: 'ordinary deductions',
+ value:
+ currentBenefitParts.ordinaryLossBenefit - chipBaseline.benefitParts.ordinaryLossBenefit,
+ },
+ {
+ label: 'NOL usage',
+ value: currentBenefitParts.nolUsageBenefit - chipBaseline.benefitParts.nolUsageBenefit,
+ },
+ {
+ label: 'capital-loss use',
+ value: currentBenefitParts.capitalLossBenefit - chipBaseline.benefitParts.capitalLossBenefit,
+ },
+ {
+ label: 'gain costs',
+ value: -(currentBenefitParts.gainAndOtherCosts - chipBaseline.benefitParts.gainAndOtherCosts),
+ },
+ ].filter(item => Math.abs(item.value) > 0.5);
+ const selectedYearResult = yearIdx > 0 ? visibleYears[yearIdx - 1] : undefined;
// Real pin (single slot, replace-on-repin). Values are FROZEN at pin time —
// a small struct, not live results — so later edits can't mutate the row.
@@ -3038,6 +3168,38 @@ export function MeetingMode({
)}
+ {showChangeChip && changeAttribution.length > 0 && !printMode && (
+
+ Gain-event context: event tax stays separate from strategy tax savings. The
+ event can change when strategy NOLs are used, so the savings headline may
+ move even when the event-tax bridge shows no carryforward shelter.
+
+ Financing context: the financing cost is not subtracted from tax savings. It
+ reduces projected portfolio values, which can change later modeled losses
+ and NOL utilization; those tax-component changes explain the headline move.
+
+ )}
+
+ )}
{/* ─── D-024 pinned ghost row ─── frozen comparison numbers
captured by "Pin current scenario" in the rail. */}
@@ -3417,6 +3579,104 @@ export function MeetingMode({
))}
+
+ {[
+ ['Realized modeled savings', totalTaxSavings, 'recognized during the projection'],
+ [
+ 'Unused tax assets',
+ cfReserveBalance +
+ (visibleYears[visibleYears.length - 1]?.nolCarryforward ?? 0),
+ 'ending loss balances · not added to savings',
+ ],
+ [
+ 'Incremental tax at liquidation',
+ exitAnalysis.incrementalDeferredTax,
+ 'signed reversal versus passive',
+ ],
+ [
+ 'Net after liquidation',
+ exitAnalysis.netBenefitAfterLiquidation,
+ 'conservative everything-sold comparison',
+ ],
+ ].map(([label, value, note]) => (
+
+
+ {label}
+
+
+ {fmtCurrency(value as number)}
+
+
{note}
+
+ ))}
+
+
+ {gainEventYears.length > 0 && (
+
+
+ Gain-event tax bridge
+
+
+
+ {fmtCurrency(gainEventTotals.amount)} event in{' '}
+ {gainEventYears.map(y => `Year ${y.year}`).join(', ')}
+
+
+ {fmtCurrency(gainEventTotals.shelter)} sheltered by current
+ losses and carryforwards
+
+
+ {fmtCurrency(gainEventTotals.tax)} modeled event tax
+
+
+ {fmtCurrency(gainEventTotals.taxWithoutStrategy)} tax without
+ the strategy
+
+
+
+ Estimated event-tax reduction:{' '}
+
+ {fmtCurrency(gainEventTotals.taxWithoutStrategy - gainEventTotals.tax)}
+
+ . Event tax is reported separately and is not added to strategy tax savings.
+
+
+ Why the savings headline can still move: adding the event can change when the
+ strategy's NOL is used across the projection. That timing effect belongs to
+ strategy tax savings; the event tax above remains a separate comparison.
+
+
+ )}
+
{/* Step-up co-metric (D-018) + EDI protection ratio — included in
the printed handout per D-021 (export parity). Compact strip
so the page-1 print pagination (exactly 3 sheets) holds. */}
@@ -3465,7 +3725,30 @@ export function MeetingMode({
cumulative financing cost
)}
+ {advancedSettings.financingFeesEnabled && (
+
+ Financing cost:{' '}
+
+ {fmtCurrency(insights.cumulativeFinancingCost)}
+ {' '}
+ cumulative, already reflected in projected wealth
+
+ )}
+ {advancedSettings.financingFeesEnabled && (
+
+ Modeled using the {advancedSettings.financingMode} financing schedule:{' '}
+ {advancedSettings.financingMode === 'simple'
+ ? `${fmtPercent1(advancedSettings.simpleWealthMgmtFee)} wealth-management fee plus ${fmtPercent1(advancedSettings.simpleManagerFeeBase)} × leverage and ${fmtPercent1(advancedSettings.simpleManagerFeeFixed)} fixed manager fee`
+ : `${fmtPercent1(advancedSettings.brokerMarginRate)} margin, ${fmtPercent1(advancedSettings.shortBorrowRate)} borrow, ${fmtPercent1(advancedSettings.shortDividendRate)} short dividends, and ${fmtPercent1(advancedSettings.wealthManagementFeeRate)} wealth-management fee`}
+ .
+
+ The financing cost reduces projected wealth; it is not subtracted from the
+ tax-savings headline. Any headline change comes indirectly from changes to
+ modeled portfolio values, later losses, and NOL utilization.
+
+
+ )}
“Net if held to step-up” is mortality-contingent and assumes basis
step-up under current law (IRC §1014). Unused loss carryforwards are lost at death
@@ -3588,7 +3871,9 @@ export function MeetingMode({