assets/js/ — interactive components
Everything a page needs to mount one of this site’s four interactive tools, in one place. Each
tool’s own file carries the same information as a header comment; this file exists so a page
author does not have to open four files to find it. Read CONVENTIONS.md first — it is the
overall contract for content pages (front matter, layouts, CSS class vocabulary); this file only
covers the JS/data layer underneath it.
Every script here is vanilla ES5, no build step, no CDN, no framework. A page opts into a script
with scripts: [...] in its front matter (see CONVENTIONS.md §4); site.js loads on every
page regardless and needs no opt-in.
What’s in this folder
assets/js/
site.js -- always loaded (progress bar, chapter rail, nav toggle)
contact.js -- contact form submit handling (contact/index.md only)
petition.js -- petition dialog, corner card and the standalone /petition/ page's
inline form; loaded on every page, but only when
site.petition_endpoint is set (see _config.yml and CONVENTIONS.md)
csv-slider.js -- generic "slider(s) -> grid lookup -> readouts" engine
(child-care valve slider, credit-collapse slider)
sortable-table.js -- generic sortable/filterable <table> engine (fifty-jurisdiction table)
jurisdiction-finder.js -- the same table's jurisdiction finder <select> (highlight + scroll + readout)
lightbox.js -- exhibits-gallery lightbox
calculator.js -- the home-page two-income calculator (COMPUTES, does not look up)
calculator.test.js -- node assets/js/calculator.test.js -- fidelity tests, run this
before trusting any number the calculator shows
lib/
worksheet.js -- ported model/worksheet.py (CJ-D 304)
net-position.js -- ported model/net_position.py (TY2026 tax model)
assets/data/
valve-units-lag.json -- figures/working/fig6_valve_units_lag.csv (130 rows)
heatmap-3child-box1.json -- figures/working/fig1_heatmap_3child_box1.csv (1,147 rows)
parenting-time-credit.json -- figures/working/fig3_credit_collapse.csv (75 rows)
states-fifty-jurisdictions.json -- data/fifty-state/tier-50-2026-09-05.json (50 rows)
All four JSON files are flat arrays of objects, keys = the source CSV’s/JSON’s own column names,
generated once and checked in (there is no build-time conversion step in this Jekyll route — see
CONVENTIONS.md §10). If a source file is ever updated, regenerate the matching JSON by hand and
recommit it; nothing regenerates these automatically.
1. The two-income calculator (home page)
This is the one tool on the site that computes rather than looks up a precomputed grid. The
design brief’s original spec called for a $5,000-step lookup grid
(heatmap-3child-box1.json, still generated and present, see note below); it cannot reproduce the
worked example ($201,000 / $29,640) to the dollar, and the project’s own rule is that no number
ships unless it’s tested against the Python it came from. So this tool instead ports
model/worksheet.py and model/net_position.py into lib/worksheet.js / lib/net-position.js
and computes the real answer for any two incomes, at any dollar value, not just the grid’s steps.
Run the test before trusting this tool, and after any change to either lib/ file:
node assets/js/calculator.test.js
1280 checks: all six worked examples from model/runs/official-xfa-vs-model-2026-09-05.txt (the
Commonwealth’s own CJ-D 304 XFA calculate-scripts), both in “round every line” mode (matches the
official scripts to the dollar on all six) and in the default unrounded mode (matches
model/worksheet.py’s own output to six decimal places — this is what the letter, the paper and
this site’s own copy quote); the worked example’s tax position from
model/runs/submission-figures-run-2026-09-09.txt to the cent; the sanity guard firing/clearing;
(v2, 2026-09-07) the full children x custody x six-income-pair fixture grid,
fixtures/calculator-v2.json (36 rows, 0 disabled), generated straight from model/worksheet.py
and model/net_position.py in the private repo and checked to the dollar and to 0.001 on ratios;
and (v3, 2026-09-07) the full children x custody x child-care-scenario x six-income-pair fixture
grid, fixtures/calculator-childcare.json (108 rows, 0 disabled), generated by
tools/gen_calculator_childcare_fixtures.py from this repo’s own model/ — unlike the v2
generator (see below), this one IS checked in, since the childcare control is this repo’s own
addition rather than a port of a number already published elsewhere. All 1280 pass as of
2026-09-07. If you change either lib/ file and a check fails, the port has diverged from the
Python — fix the port, never the test. If you add a new children/custody/childcare/income
combination the tool should support, regenerate the relevant fixture file from the Python first —
never hand-write an expected value.
For fixtures/calculator-v2.json: the generator script that produced it was written to /tmp
(per this project’s convention that a one-off script does not belong in either repo) and is not
checked in; to reproduce it: for each of children 1/2/3 x box 1/2 x the six (higher, lower)
annual-gross pairs (201000,29640) (150000,30000) (250000,60000) (120000,80000) (300000,0)
(90000,45000), call worksheet.run(box, lower/52, higher/52, kids, 0, a_health=33.0,
b_health=43.0) then net_position.analyze(payor_gross, recip_gross, kids, r["7d"], 0.0, 0.0,
kids_under_13=0) (payor/recip resolved from r["payor"]), and write kids, box, higher, lower,
payor, order_wk (r["7d"]), line_7e (r["7e"]), true_pct_net (support_pct_of_payor_net), payor_net,
payor_after, recip_after, recip_per_person per row, plus a disabled array for any combination
that raised. All 36 combinations computed cleanly for this grid (disabled: []); if a future grid
addition can’t be computed, list it there and disable that combination in the UI instead of
guessing a number.
For fixtures/calculator-childcare.json: run python3 tools/gen_calculator_childcare_fixtures.py
— it imports model/worksheet.py and model/net_position.py from this repo directly (no path to
the private repo needed) and writes the JSON in one step. See that script’s own header comment for
the child-care scenario definitions (0 none, 1 recipient pays, 2 both pay) and the
composition into net_position.analyze()’s weekly_childcare / payor_childcare_share
arguments — the same analyze() call v2 already made, not a new function.
Markup a page must contain
<div class="tool tool-two-up" data-calculator>
<div class="tool-slider-row">
<label for="calc-higher">Higher earner, gross per year
<output id="calc-higher-output" for="calc-higher">$201,000/yr</output>
</label>
<input type="range" id="calc-higher" data-calc-input="higher"
min="60000" max="300000" step="1000" value="201000">
</div>
<div class="tool-slider-row">
<label for="calc-lower">Lower earner, gross per year
<output id="calc-lower-output" for="calc-lower">$29,640/yr</output>
</label>
<input type="range" id="calc-lower" data-calc-input="lower"
min="0" max="120000" step="120" value="29640">
</div>
<div class="tool-controls-row">
<fieldset class="segmented">
<legend>Children</legend>
<label class="segmented-option"><input type="radio" name="calc-kids" value="1" data-calc-radio="kids"> 1</label>
<label class="segmented-option"><input type="radio" name="calc-kids" value="2" data-calc-radio="kids"> 2</label>
<label class="segmented-option"><input type="radio" name="calc-kids" value="3" data-calc-radio="kids" checked> 3</label>
</fieldset>
<fieldset class="segmented">
<legend>Custody</legend>
<label class="segmented-option"><input type="radio" name="calc-custody" value="1" data-calc-radio="box" checked> Shared, equal time (Box 1)</label>
<label class="segmented-option"><input type="radio" name="calc-custody" value="2" data-calc-radio="box"> Primary with the lower earner (Box 2)</label>
</fieldset>
<fieldset class="segmented segmented--compact">
<legend>Child care ($300/wk)</legend>
<label class="segmented-option"><input type="radio" name="calc-childcare" value="0" data-calc-radio="childcare" checked> None</label>
<label class="segmented-option"><input type="radio" name="calc-childcare" value="1" data-calc-radio="childcare"> Recipient pays</label>
<label class="segmented-option"><input type="radio" name="calc-childcare" value="2" data-calc-radio="childcare"> Both pay</label>
</fieldset>
</div>
<div class="tool-readout" aria-live="polite">
<div>
<p class="cell-label">Weekly order (Line 7d)</p>
<p class="cell-value" data-calc-cell="order_wk">$1,013/wk</p>
</div>
<div>
<p class="cell-label">Line 7e's reading</p>
<p class="cell-value payor-line" data-calc-cell="line_7e">26.5%</p>
</div>
<div>
<p class="cell-label">True share of the payor's net income</p>
<p class="cell-value" data-calc-cell="true_pct_net">36.8%</p>
<p class="cell-note" data-calc-note="true_pct_net"> </p>
</div>
</div>
<p class="tool-flag" data-calc-flag> </p>
<details class="tool-detail" open>
<summary>After tax and the order, per year</summary>
<div class="tool-readout tool-readout--pair" aria-live="polite">
<div>
<p class="cell-label">Payor keeps</p>
<p class="cell-value" data-calc-cell="payor_after">$90,447/yr</p>
</div>
<div>
<p class="cell-label">Recipient household holds</p>
<p class="cell-value" data-calc-cell="recip_after">$90,631/yr</p>
<p class="cell-sub">Per person: <strong data-calc-cell="recip_per_person">$22,658/yr</strong></p>
<p class="cell-note" data-calc-note="recip_after"> </p>
</div>
</div>
<p class="tool-flag" data-calc-flag-household> </p>
</details>
<p>Three children, Box 1 (shared parenting), and no child care by default. Health premiums
$33/wk (lower earner) / $43/wk (higher earner), MA under-13 credit fixed at zero. Method:
<a href="/model/worksheet.py"><code>model/worksheet.py</code></a> and
<a href="/model/net_position.py"><code>model/net_position.py</code></a>, ported and tested in
<a href="/assets/js/lib/worksheet.js"><code>assets/js/lib/worksheet.js</code></a> and
<a href="/assets/js/calculator.test.js"><code>assets/js/calculator.test.js</code></a>.</p>
</div>
(v2 introduces four new classes, all in assets/css/site.css’s “Live tools” section:
.tool-controls-row / .segmented / .segmented-option — the children/custody segmented radio
controls, grid-column: 1 / -1 so the row spans the two-up grid regardless of implicit
auto-placement — and .tool-detail — the <details> wrapper for the after-tax row, same reason.
.cell-note and .cell-sub are new too: a bold short text label paired with the warning colour
(WCAG 1.4.1 — colour is never the only channel) and the per-person figure beside a household one,
respectively. .check-yourself/.ask exist in assets/css/site.css but are built for a whole
section with an <h2> and a link grid — the method line above is one plain paragraph inside
.tool and needs no special class; a page author who wants the full check-yourself treatment
should put it in its own section outside the tool, per CONVENTIONS.md §7, not force it onto this
one line. v3 adds one more: .segmented--compact, a modifier on the third, child-care fieldset
that shrinks its horizontal padding and label font size so three segmented fieldsets fit
.tool-controls-row without the tool growing noticeably taller — it does not shrink
.segmented-option’s min-height, which stays the 44px WCAG 2.5.5 touch target every other
segmented option already uses.)
Front matter: scripts: ["/assets/js/lib/worksheet.js", "/assets/js/lib/net-position.js", "/assets/js/calculator.js"]
— in that order; calculator.js reads window.MCSGWorksheet / window.MCSGNetPosition and
does nothing if either is missing (fails safe onto the static markup below, see next point).
No-JS / load-failure fallback is REQUIRED and is not automatic: every data-calc-cell span,
both sliders’ value attributes, all three radio groups’ checked attributes, and the
is-warning class / note text on recip_after must already contain the real worked-example
numbers exactly as written above ($1,013/wk, 26.5%, 36.8%, $90,447/yr, $90,631/yr, $22,658/yr,
$201,000/yr, $29,640/yr, kids=3, box=1, childcare=0, credits on) — copy them verbatim, they are tested
(calculator.test.js PART 3). A reader with JavaScript off, or whose browser fails to load one of
the two lib/ scripts, sees the worked example stated correctly, including which of the two
warning colours is on, and only loses the ability to change it. The <details open> renders its
content with no JavaScript at all; only the ability to collapse it is progressive enhancement (the
browser’s native behaviour).
Fixed facts, not sliders (per the design brief’s word budget): health premiums $33/wk to
whichever slider is currently lower and $43/wk to whichever is currently higher (matches the
convention already used by every heatmap exhibit in this project, model/charts/_common.py’s
order()), and the MA Child and Family Tax Credit for children under 13 fixed at zero qualifying
children (same generic-grid convention as every heatmap on this site — there is no fourth control
for how many of the children are under 13). The tool now counts the refundable credits by default (changed 2026-09-09 evening, as the
better default for the tool; net_position.py’s analyze() still defaults to credits-off, and every published figure
in the comments, on the finding pages and in the charts stays on that withholding basis). So the
kids-under-13 convention DOES bite the tool’s default readout: the Massachusetts Child and Family
Tax Credit is held at zero qualifying children, which understates the recipient household by $440
per child under 13 against the site’s own worked example. That runs against this project’s own
argument, which is the safe direction, and it is why the tool’s after-tax row can sit slightly
below the worked-example figures. Switch the credits toggle to “Leave them out” and the convention
has nothing left to bite: no refundable credit is counted, kids-under-13 changes nothing, and the
after-tax row matches the site’s worked example exactly ($87,172/$77,395).
Children (1/2/3, default 3), custody (Box 1/Box 2, default Box 1), and (v3, 2026-09-07) child
care (None/Recipient pays/Both pay, default None) are all real controls — real
<input type="radio"> elements in a <fieldset>/<legend>, keyboard-operable, each group’s
name distinct. Child care is a fixed $300/wk claim per scenario rather than a fourth slider (see
calculator.js’s v3 header comment for why a discrete scenario matches the site’s own findings
better than a continuous range); “Recipient pays” assigns the whole $300/wk to Parent A (the lower
earner), “Both pay” gives $300/wk to each parent. The two income sliders remain independent
controls; if a reader drags “lower” past “higher” the script silently swaps which value plays
which role so each label still names the parent it describes — it does not clamp or block the drag. This calculator
models Massachusetts only — the method paragraph says so in one clause; no other jurisdiction’s
worksheet is ported here (contrast the fifty-jurisdiction table below, which is a lookup across
all 51, not a computation).
heatmap-3child-box1.json is not used by this tool and is not dead weight to remove — it is
the CSV figures/working/fig1_heatmap_3child_box1.csv already committed to the repo and cited
elsewhere (e.g. exhibits E01/E02/E04 are drawn from its 1/2-child siblings), converted to JSON
in case a future page wants a client-side grid lookup (a hover tooltip over the static heatmap
exhibit, for instance). Converted and checked in now so the conversion doesn’t have to happen
twice; not wired to any control.
2. The child-care valve slider (hardship-test finding page)
Already fully built by csv-slider.js (a generic engine — read that file’s own header comment
for the complete markup contract and every data-tool-* attribute it reads). Data file:
valve-units-lag.json. No-JS fallback: the shipped site’s existing five-row table, in a
<noscript> block (design brief §3.7 item 2) — this script only enhances it, it does not
replace it.
Static-exhibit pairing: figures/exhibits/E05-what-line-7e-sees-vs-true-burden-worked-example.png.
3. The parenting-time credit-collapse tool (parenting-time finding page)
Built on the same csv-slider.js engine as the valve slider — no new script, no vendored
charting library. The design brief (§3.7 item 4) specified Observable Plot for this exhibit,
chosen for built-in per-mark ARIA labels on an SVG curve with a moving marker. That was not
built: vendoring, licensing and testing a 69KB third-party library for one exhibit was not
justified once a plain native-range-input slider (identical in kind to the valve slider, already
tested, already accessible with zero additional ARIA work because there is no SVG mark to label)
delivers the same reader-facing fact — the credit percentage at a given income disparity — from
the same CSV. What’s given up: the moving marker on a visible curve. What’s kept: every
number the brief’s spec cared about, native keyboard support, and no new dependency. Flagged here
so a reviewer can decide this was the right trade, not discover it by accident.
Data file: parenting-time-credit.json (from figures/working/fig3_credit_collapse.csv, 75
rows spanning the lower earner’s income from $2,000 to $150,000/yr against a fixed higher
earner, at which the payor’s income share of the combined household, payor_3c, ranges
57.3%–99.9%). Markup:
<div class="tool" data-tool data-tool-src="/assets/data/parenting-time-credit.json">
<div class="tool-slider-row">
<label for="credit-slider">Payor's share of the combined household income
<output id="credit-slider-output" for="credit-slider"></output>
</label>
<input type="range" id="credit-slider" data-tool-input="payor_3c"
data-tool-output-format="pct0"
data-tool-valuetext="Payor holds {value} of the combined household income"
min="0.573" max="0.999" step="0.001" value="0.877">
</div>
<div class="tool-readout" aria-live="polite">
<div>
<p class="cell-label">Box 2 order (primary custody)</p>
<p class="cell-value" data-tool-cell="box2_7d" data-tool-format="money-wk"></p>
</div>
<div>
<p class="cell-label">Box 1 order (shared parenting)</p>
<p class="cell-value" data-tool-cell="box1_7d" data-tool-format="money-wk"></p>
</div>
<div>
<p class="cell-label">The shared-parenting credit</p>
<p class="cell-value true-burden" data-tool-cell="reduction_current" data-tool-format="pct1"></p>
</div>
</div>
</div>
Default value="0.877" is the worked example’s own payor income share (87.7%), where
reduction_current = 6.9% — matches model/runs/submission-figures-run-2026-09-09.txt’s
“SECTION 5” table row 87.7% 1,088 1,013 6.9%. Front matter:
scripts: ["/assets/js/csv-slider.js"] (same file as the valve slider — a page using both tools
loads it once). No-JS fallback: render the “SECTION 5” table from the same run file as a plain
Markdown table (it already exists as prose in the project’s documents; this is not new content to
write, only to place before the slider).
Static-exhibit pairing:
figures/exhibits/E08-credit-shrinks-as-gap-widens-3-children.png (and E09, the implied-
overnight-share companion).
4. The fifty-jurisdiction sortable, searchable table (home page + its finding page)
Already fully built by sortable-table.js (generic engine — read that file’s own header comment
for the complete markup contract: <button data-sort-key="..."> headers with aria-sort, an
optional <input data-table-filter="...">). This script does not fetch or render the table —
it only reorders <tr> elements already in the DOM (design brief’s own requirement: the table
must be complete and correctly pre-sorted with no JS at all). A page author renders the 50 rows
as real HTML, from states-fifty-jurisdictions.json (or straight from
data/fifty-state/tier-50-2026-09-05.json, which this file is a byte-for-byte copy of plus one
derived field), sorted descending by s1 to match the brief’s stated no-JS default order.
Each row needs: the jurisdiction name; s1 (equal-parenting order) and s2 (primary-custody
order), both shown — this project’s finding is specifically about the GAP between a
jurisdiction’s own two numbers, so a table that only showed one would misrepresent it; and, for
Massachusetts only, the is_reader_state/.is-reader-state class so it stays visually pinned
regardless of sort order (is_massachusetts: true in the JSON marks which row). Numeric columns
need data-sort-value set to the raw number (see sortable-table.js’s own comment for why:
$4,388/mo as text would sort lexicographically, not numerically).
<table class="exhibit-table" id="fifty-table" data-sortable>
<thead>
<tr>
<th><button data-sort-key="state" aria-sort="none">Jurisdiction</button></th>
<th class="numeric"><button data-sort-key="s1" data-sort-type="number" aria-sort="descending">Equal parenting (S1)</button></th>
<th class="numeric"><button data-sort-key="s2" data-sort-type="number" aria-sort="none">Primary custody (S2)</button></th>
</tr>
</thead>
<tbody>
<tr class="is-reader-state" data-filter-text="massachusetts">
<td>Massachusetts</td>
<td class="numeric" data-sort-value="4388.48">$4,388/mo</td>
<td class="numeric" data-sort-value="4714.22">$4,714/mo</td>
</tr>
<!-- ... 49 more rows, from states-fifty-jurisdictions.json, s1 descending ... -->
</tbody>
</table>
<label for="jurisdiction-filter">Filter by jurisdiction</label>
<input type="text" id="jurisdiction-filter" data-table-filter="fifty-table" aria-controls="fifty-table">
Front matter: scripts: ["/assets/js/sortable-table.js"]. Tiering caveat is a page-copy
responsibility, not this script’s — per CONVENTIONS.md/the exhibit direction, this is a
single-fact-pattern, tiered comparison (see data/fifty-state/tier-50-2026-09-05.json’s own
per-row note field, present on Hawaii, for the one row needing one) and the surrounding prose
must say so; the table itself carries no confidence styling.
Static-exhibit pairing (the lead exhibit pair per the 2026-09-06 exhibit direction):
figures/exhibits/E12-fifty-states-lower-earner-primary-one-fact-pattern.png then
figures/exhibits/E17-ma-equal-time-vs-others-primary-custody.png.
4a. The jurisdiction finder (2026-09-07) — a separate control, next to the filter
jurisdiction-finder.js (generic-ish but built for this one table — full markup contract in its
own header comment). This is a finder, not a filter: it never hides a row. A real
<select>/<label> with 51 alphabetical options (the table’s 50 rows plus Georgia, marked
data-held-out="true" since Georgia has no row), Massachusetts preselected. Choosing an option:
- adds
.is-selected-stateto that jurisdiction’s<tr>(distinct from.is-reader-state, Massachusetts’s own permanent tint — a blue outline,--hue-recipient, so “my state” and “the state I just looked up” stay visually separate even when both land on the same row) and scrolls it into view; - writes a one-line comparison into
#jurisdiction-readoutabove the table: the jurisdiction’s own S1/S2 and its rank on each (computed client-side by sorting the table’s owndata-sort-valueattributes — no separate rank data file), next to Massachusetts’s; - for Georgia, clears any highlight and swaps the readout for the held-out sentence instead of a number.
Rank follows the table’s own numbers, not a precomputed field — this is a deliberate choice to
keep one source of truth (the rendered table) rather than a parallel dataset that could drift from
it. Works with the existing sort: the highlight is a class on the <tr> itself, and
sortable-table.js only reorders existing <tr> elements when a column header is clicked, so the
highlighted row’s class survives any re-sort (checked: select Texas, sort by S2, the highlight
follows Texas to its new position).
Front matter: scripts: ["/assets/js/jurisdiction-finder.js"], alongside (not instead of)
sortable-table.js — independent listeners on the same #fifty-table. No-JS fallback: the
<option value="massachusetts" selected> and the readout’s starting text (Massachusetts’s own two
numbers and ranks, written out as plain static text) are both already correct without JavaScript;
selecting a different option does nothing without JS, same as the pre-existing filter input next
to it — the table itself is already complete and correct either way.
5. The exhibits-gallery lightbox
Already fully built by lightbox.js — read that file’s own header comment for the complete
markup contract. Not data-driven; no assets/data/*.json file involved.
6. The petition sign-up (2026-09-26)
Header trigger, corner card, <dialog>, and the standalone /petition/ page’s own inline form —
petition.js’s own header comment has the full markup contract for all four. Gated end to end on
site.petition_endpoint (_config.yml): empty means the script is never even requested, per the
conditional script tag in _layouts/default.html. See CONVENTIONS.md’s own note on this feature
and docs/plans/2026-09-26-site-petition-build.md in the private repo for the spec and the
server’s request/response contract.
Uses a real <dialog> shown with showModal(), unlike lightbox.js’s hand-rolled
<div role="dialog"> — while a native dialog is open with showModal(), the browser keeps focus
inside it and treats the rest of the page as inert, so this needs no manual Tab trap the way
lightbox.js needs one for its own dialog. Not data-driven; no assets/data/*.json file
involved, and the form posts to a small purpose-built server rather than a third-party form
backend (unlike contact.js, which does use one).
Read-back done on this pass
node assets/js/calculator.test.js— 38/38 checks pass (see file for what each one proves).- Every
assets/data/*.jsonfile spot-checked against its source CSV/JSON’s first and last rows and against figures already quoted incontext/MEMORY.md(Massachusetts $4,388.48/$4,714.22; the worked example’s $1,012.7257303613252 unrounded 7d; the 87.7%-income-share row’s 6.9% credit). - No em dash in any reader-visible string this pass added (
calculator.js’s flag text); the pre-existing em dashes in every file’s own code comments are not reader-visible prose and were left as the foundation pass wrote them. - Every script here is self-contained ES5 with no absolute local file path.
Read-back done on the v3 pass (child care control, 2026-09-07)
node assets/js/calculator.test.js— 1280/1280 checks pass, up from 338 (the new PART 6 adds 108 fixture-grid checks plus per-scenario checks; every existing PART still passes unchanged).python3 tools/gen_calculator_childcare_fixtures.pyregeneratesassets/js/fixtures/calculator-childcare.jsonfrom this repo’s ownmodel/worksheet.py/model/net_position.pywith zero disabled combinations (108/108 computed).- PART 6 also checks every
childcare=0row against the pre-existingcalculator-v2.jsonrow for the same kids/box/income combination, proving the newburden_pct_of_payor_netreadout is identical to the oldsupport_pct_of_payor_netone whenever the payor pays no child care directly — v3 changes nothing about v2’s numbers. - Mounted page (
index.html) read back in a real browser: the default (childcare=0) numbers are unchanged from before this pass; switching to “Recipient pays” reproduces $1,276/wk, 33.4%, 47.4%; switching to “Both pay” pushes the true-share-of-net cell over 40% and into.is-warning. 390px screenshot taken; the third.segmented--compactfieldset wraps onto its own line under 700px, same as the existing two, and stays within the.toolborder at 390px. - No em dash in any new reader-visible string or code comment this pass added.