diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..1b6ba33
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,14 @@
+# PostgreSQL connection values. Copy names only; never commit real credentials.
+PGHOST=
+PGPORT=5432
+PGDATABASE=countydata
+PGUSER=
+PGPASSWORD=
+PGSSLMODE=require
+
+# Project-specific HMAC secret for replacing VINs in private analytical data.
+# Encode exactly 32 random bytes as 64 hexadecimal characters. Alternatively,
+# leave VIN_HASH_KEY empty and store the hex key in .secrets/vin_hmac.key (0600).
+VIN_HASH_KEY=
+VIN_HASH_KEY_FILE=.secrets/vin_hmac.key
+VIN_HASH_KEY_VERSION=v1
diff --git a/.gitignore b/.gitignore
index 2f6de41..5fc7f13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,12 @@
.env
+.env.*
+!.env.example
.venv/
__pycache__/
*.pyc
.ipynb_checkpoints/
.DS_Store
+/data/
+/artifacts/
+/models/
+/.secrets/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..bc9a081
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "ms-ossdata.vscode-pgsql"
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..f0d73de
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,113 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Utah Vehicle Health: create private VIN HMAC key",
+ "type": "shell",
+ "command": ".venv/bin/python scripts/create_vin_hash_key.py",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Countydata: test read-only connection",
+ "type": "shell",
+ "command": "set -a; source .env; set +a; .venv/bin/python scripts/check_connection.py",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: run Python tests",
+ "type": "shell",
+ "command": "PYTHONWARNINGS=error .venv/bin/python -m unittest discover -s tests -v",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: run aggregate feasibility check",
+ "type": "shell",
+ "command": "set -a; source .env; set +a; .venv/bin/python scripts/run_feasibility_check.py",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: audit source outcome mappings",
+ "type": "shell",
+ "command": "set -a; source .env; set +a; .venv/bin/python scripts/run_outcome_mapping_audit.py",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: export 10k development histories",
+ "type": "shell",
+ "command": "set -a; source .env; set +a; .venv/bin/python scripts/export_history_sample.py --vehicles 10000 --sample-percent 0.5 --overwrite",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: build private feature mart",
+ "type": "shell",
+ "command": ".venv/bin/python scripts/build_feature_mart.py --input data/private/development/history_sample_10000.csv.gz --memory-limit 4GB --threads 4 --overwrite",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: train baselines (2025 locked)",
+ "type": "shell",
+ "command": ".venv/bin/python scripts/train_baselines.py --mart data/private/marts/inspection_feature_mart.parquet --overwrite",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: train nonlinear model (2025 locked)",
+ "type": "shell",
+ "command": ".venv/bin/python scripts/train_tree_model.py --mart data/private/marts/inspection_feature_mart.parquet --overwrite",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: export sanitized dashboard preview",
+ "type": "shell",
+ "command": ".venv/bin/python scripts/export_dashboard_data.py --mart data/private/marts/inspection_feature_mart.parquet --model-manifest artifacts/private/baselines/baseline_v1/manifest.json --model-metrics artifacts/private/baselines/baseline_v1/metrics.json --model-manifest artifacts/private/tree/hist_gradient_boosting_v1/manifest.json --model-metrics artifacts/private/tree/hist_gradient_boosting_v1/metrics.json --development-preview --overwrite",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: run dashboard contract tests",
+ "type": "shell",
+ "command": "node --test dashboard/tests/contract.test.mjs",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Utah Vehicle Health: run all tests",
+ "dependsOrder": "sequence",
+ "dependsOn": [
+ "Utah Vehicle Health: run Python tests",
+ "Utah Vehicle Health: run dashboard contract tests"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/README.md b/README.md
index 90f9348..689d8df 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,127 @@
# SummerProject2026
-Kevin Bell's summer 2026 project
\ No newline at end of file
+Kevin Bell's summer 2026 data project using Utah county vehicle-registration
+and emissions-inspection data.
+
+See the [data inventory](docs/data_inventory.md) and the data-backed
+[project shortlist](docs/project_options.md).
+
+## Selected project
+
+The selected direction is **Utah Vehicle Health**, an explainable model and
+dashboard for the first-attempt outcome of a vehicle's next inspection episode.
+Start with the [project charter](docs/project_charter.md), then use the
+[modeling protocol](docs/modeling_protocol.md) and
+[dashboard specification](docs/dashboard_spec.md) as the project contracts.
+The latest private-sample pipeline findings are summarized in
+[development results](docs/development_results.md), with intended use and
+limitations consolidated in the [model card](docs/model_card.md).
+
+## Safe database access in VS Code
+
+The local `.env` file contains the standard PostgreSQL connection variables and
+is intentionally excluded from Git. Never put those values in source code,
+screenshots, browser JavaScript, or a Bolt project.
+
+1. Install the workspace-recommended **PostgreSQL** extension by Microsoft
+ (`ms-ossdata.vscode-pgsql`).
+2. In VS Code, run **Tasks: Run Task** and choose
+ **Countydata: test read-only connection**. It loads `.env`, requires TLS, and
+ forces the PostgreSQL session into read-only mode.
+3. To browse the server visually, open the PostgreSQL sidebar and add a
+ connection using the values in `.env`. Set SSL mode to **Require**, save the
+ profile at **User** scope, and store the password in macOS Keychain rather
+ than workspace settings.
+4. Connect to `countydata` and run [sql/00_read_only_connection_check.sql](sql/00_read_only_connection_check.sql).
+ Safe aggregate examples are in [sql/01_safe_data_overview.sql](sql/01_safe_data_overview.sql).
+
+The selected project's first aggregate cohort check is
+[sql/10_episode_cohort_feasibility.sql](sql/10_episode_cohort_feasibility.sql).
+Private bounded extraction is handled by
+[scripts/export_inspection_batch.py](scripts/export_inspection_batch.py); it
+requires the project-specific `VIN_HASH_KEY` described in `.env.example` and
+never writes a raw VIN.
+
+The current database login has write privileges even though this project only
+needs reads. Ask the database administrator for a dedicated read-only role
+before connecting any deployed service. Until then, always use explicit
+read-only transactions.
+
+## Reproducible development workflow
+
+Create the local environment once:
+
+```bash
+python3 -m venv .venv
+.venv/bin/pip install -r requirements.txt
+.venv/bin/python scripts/create_vin_hash_key.py
+```
+
+The recommended way to run the pipeline is **Tasks: Run Task** in VS Code. The
+tasks preserve the intended order:
+
+1. Test the read-only TLS connection.
+2. Run the source outcome-mapping and aggregate feasibility audits.
+3. Export the private 10,000-vehicle development histories.
+4. Build the private DuckDB warehouse and Parquet feature mart.
+5. Run every test.
+6. Train the baselines while leaving the 2025 test partition locked.
+7. Train the nonlinear comparison while leaving the same partition locked.
+
+The development history sample is deliberately not population-representative.
+It exists to exercise feature engineering and modeling before a complete,
+contiguous bounded extraction is approved. Generated extracts, marts, database
+files, keys, and model artifacts remain under Git-ignored private paths.
+
+The 2025 metrics are not calculated by the normal training task. Unlocking them
+requires the conspicuous `--evaluate-locked` flag after the feature set,
+hyperparameters, calibration method, and reporting plan are frozen.
+That gate was opened once on the page-sampled development extract during live
+verification; the normal artifact paths were then regenerated closed. The
+audit trail is in [development results](docs/development_results.md), and no
+further 2025-informed tuning is permitted.
+
+## Sanitized dashboard preview
+
+After training both closed model bundles, run **Tasks: Run Task** and choose
+**Utah Vehicle Health: export sanitized dashboard preview**. The exporter
+publishes only suppression-reviewed JSON under `dashboard/public/data`; it
+refuses the sampled mart unless the explicit development-preview flag is used
+and refuses model artifacts containing holdout metrics.
+
+Preview the site without exposing the repository root:
+
+```bash
+node dashboard/server.mjs
+```
+
+Then open `http://localhost:4173`. Do not serve the repository root, because it
+contains the local connection profile and private ignored directories.
+
+The checked-in bundle is visibly labeled as a non-population development
+preview. Before a real public-data release, rerun the frozen pipeline on an
+approved complete extraction and pass the publication review documented in
+[the dashboard specification](docs/dashboard_spec.md).
+
+Local and Bolt handoff instructions are in
+[dashboard/README.md](dashboard/README.md). Bolt can import a GitHub repository,
+but the hosted site root must be the `dashboard` directory; never copy `.env`,
+private data, model artifacts, or database tooling into a web project.
+
+## Data safety and publishing
+
+The source contains direct identifiers and operational data, including VINs,
+plates, user records, sessions, and upload metadata. A public dashboard should
+contain only de-identified aggregates and model outputs with minimum group-size
+suppression.
+
+Recommended deployment flow:
+
+```text
+countydata (read-only) -> local ETL/modeling -> sanitized aggregate tables/files
+ -> Bolt-hosted dashboard
+```
+
+Do not connect a browser directly to `countydata`. If the dashboard must refresh
+automatically, use a scheduled backend job with a dedicated read-only database
+role and copy only approved aggregate results into the hosted application.
diff --git a/dashboard/README.md b/dashboard/README.md
new file mode 100644
index 0000000..95150b3
--- /dev/null
+++ b/dashboard/README.md
@@ -0,0 +1,67 @@
+# Utah Vehicle Health dashboard
+
+This directory is a dependency-free static prototype. It uses semantic HTML,
+CSS, vanilla ES modules, and inline SVG generated from approved aggregate JSON.
+It has no database client, server credential, analytics SDK, or external CDN.
+
+## Run locally from the repository root
+
+```bash
+node dashboard/server.mjs
+```
+
+Open `http://127.0.0.1:4173`. Do not open `index.html` directly with a `file:`
+URL; browsers will block the JSON module requests. To use another port:
+
+```bash
+PORT=8080 node dashboard/server.mjs
+```
+
+Run the dependency-free contract checks with:
+
+```bash
+node --test dashboard/tests/contract.test.mjs
+```
+
+## Public-data boundary
+
+The browser requires and validates these files beneath `public/data/`:
+
+- `data_manifest.json`
+- `overview_period_county.json`
+- `age_risk_curve.json`
+- `cohort_scorecard.json`
+- `model_diagnostics.json`
+- `coverage_quality.json`
+- `filter_catalog.json`
+- `sha256_manifest.json`
+
+If any file is missing, malformed, has inconsistent publication flags, or
+contains a denied identifier-like field, the dashboard shows an unavailable
+state and no estimates. Regenerate assets with the repository's private local
+pipeline; never hand-edit public JSON to bypass suppression.
+
+The next-test estimator is intentionally disabled. It must remain disabled
+until a separately reviewed and suppressed `prediction_lookup` contract is
+approved. Do not add a connection from this site to `countydata`.
+
+## Import into Bolt
+
+The recommended publication boundary is a separate static Bolt project (and,
+ideally, a separate deployment repository) containing only the contents of
+`dashboard/`. Upload or copy this directory's contents so `index.html` is at the
+new project root. There is no install or build step. If Bolt asks for a preview
+command, use `node server.mjs` with `HOST=0.0.0.0`; Bolt supplies `PORT`.
+
+Before publishing, confirm that `/public/data/data_manifest.json` resolves,
+the development-preview banner remains visible, and the contract test passes.
+The generated JSON bundle must also complete its privacy review.
+
+Importing the whole source repository is a discouraged fallback because Bolt
+would receive SQL, database tooling, and other files that are not needed by the
+public site. If that has already happened, set the project/working root to
+`dashboard`, never serve the repository root, and create a dashboard-only
+project before production publication. Do not copy private `data/`,
+`artifacts/`, `models/`, `.env`, or database tooling into the public project.
+
+No publishing action is performed by this repository.
diff --git a/dashboard/index.html b/dashboard/index.html
new file mode 100644
index 0000000..c205cea
--- /dev/null
+++ b/dashboard/index.html
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+
+ Overview · Utah Vehicle Health
+
+
+
+
+ Skip to dashboard content
+
+
+
◆
+
+ Development preview
+ Sample results are not population estimates and must not be used for individual decisions.
+
+
+
+
+
+
+
+
+
+
+
Inspection outcomes over time
+
A clearer view of the next inspection
+
Explore aggregate first-attempt outcomes for returning vehicles in participating Utah county feeds. Non-pass combines fail, reject, and abort.
+
+
+ Data through
+ Unavailable
+ Waiting for manifest
+
+
+
+
+
!
+
+
Dashboard data is unavailable
+
The approved aggregate files could not be validated. No estimates are being shown.
+
+
+
+
+
+ Published support
+ —
+ Rounded total; suppressed cells omitted
+
+
+ Pass rate
+ —
+ Recognized first-attempt outcomes
+
+
+ Non-pass rate
+ —
+ Fail + reject + abort
+
+
+ Covered counties
+ —
+ Not statewide coverage
+
+
+
+
+
+
+
+ Partial periods and source changes are called out when present.
+
+
+
+
+
+
+ Teal counties appear in the approved inspection feeds. Gray counties are unavailable, not zero.
+
+
+
+
+
+ The development bundle does not include confidence intervals; published aggregate rates are shown as points.
+
+
+
+
+ i
+ What this measures: emissions-inspection outcomes for covered feeds. It is not a diagnosis of mechanical condition, safety, roadworthiness, or legal compliance.
+
+
+
+
+
+
+
+ Supported aggregate cohorts
+ Reliability explorer
+ Compare published observed next-episode non-pass rates. Every result is a supported aggregate cohort—not an individual prediction.
+
+
+
+
!
+
Explorer unavailable Approved scorecards could not be validated.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No supported cohorts match
+
Try clearing one or more filters. Suppressed cohorts are never included in the browser bundle.
+
+
+
+
+
+
+
+
+
+ Cohort estimate · not an individual diagnosis
+ Next-test risk estimator
+ This planned tool will combine approved, coarsened attributes to return a calibrated next-episode non-pass probability.
+
+
+
+
◇
+
+
Intentionally disabled
+
An approved prediction lookup is not available
+
The estimator will remain off until a privacy-reviewed, suppressed prediction_lookup is published. The current model diagnostics are not enough to serve individual or row-level estimates.
+
+
+
+
+
No VIN, plate, exact address, station, free text, or current-test diagnostic will ever be requested.
+
+
+
+
Future approved output
+
Designed for calibrated context
+
+
+ Calibrated non-pass probability and uncertainty
+ Relevant aggregate baseline
+ High-level factor contributions
+ Support and coverage limitations
+
+
+
+
+
+
+
+
+ Transparent by design
+ Data & methods
+ How episodes, labels, temporal evaluation, coverage limits, and privacy controls shape every published result.
+
+
+
+
!
+
Live diagnostics unavailable Static methodology remains below; no performance values are being shown.
+
+
+
+
+ 01
+ Prediction unit
+ The first attempt of the next inspection episode. Attempts no more than 30 days apart form one episode.
+
+
+ 02
+ Binary target
+ Pass versus non-pass. Non-pass combines fail, reject, and abort; blanks and unrecognized outcomes are not labels.
+
+
+ 03
+ Point-in-time features
+ Every history window ends before the target episode. Current-test results, diagnostics, stations, and later attempts are excluded.
+
+
+
+
+
+
+ The slc and slco labels are source eras for Salt Lake County, not separate physical counties.
+
+
+
+
+
No random row split
+
Chronological evaluation
+
Historical events establish prior context. The 2025 development-sample holdout was evaluated once after model choices were frozen, so it is no longer a pristine final test. A later complete-data run is confirmatory; the partial 2026 feed is monitoring only.
+
+
+ 2010–15 Context History only
+ 2016–22 Train Fit preprocessing + model
+ 2023 Tune Select regularization
+ 2024 Calibrate Scale probabilities
+ 2025 One-time holdout Already evaluated
+ 2026 Shadow Partial-period drift
+
+
+
+
+
+
+
+
PR-AUC —
+
Brier score —
+
Support —
+
+
+
+
+
+
+
+ Participating inspection feeds do not cover all 29 counties.
+ Feed and program changes can resemble real-world trends.
+ DMV history has a 2021 gap and ends in March 2024.
+ Reject and abort can reflect process or readiness issues.
+ Association is not causation or a mechanical diagnosis.
+
+
+
+
+
+ Publication boundary
Only approved aggregates reach this site
+
+ Private sourceRead-only local pipeline
+ →
+ SuppressionMinimum support + complements
+ →
+ Public assetsAggregate JSON only
+
+ Direct identifiers, private vehicle tokens, plates, ZIPs, stations, raw records, and row-level predictions are outside the public data contract.
+
+
+
+
+
+
+
+
+ JavaScript is required to validate and display the approved aggregate data files. No estimates are shown without validation.
+
+
diff --git a/dashboard/js/app.js b/dashboard/js/app.js
new file mode 100644
index 0000000..5e120df
--- /dev/null
+++ b/dashboard/js/app.js
@@ -0,0 +1,352 @@
+import { loadDashboardData } from "./data.js";
+import {
+ aggregateOverview,
+ compactNumber,
+ percent,
+ renderAgeRisk,
+ renderCohortDotPlot,
+ renderCoverageHeatmap,
+ renderNoCalibration,
+ renderOutcomeTrend,
+ renderUtahCoverage,
+} from "./charts.js";
+
+const ROUTES = Object.freeze({
+ overview: "Overview",
+ reliability: "Reliability explorer",
+ estimator: "Next-test estimator",
+ methods: "Data & methods",
+});
+
+const UTAH_COUNTIES = [
+ "Beaver",
+ "Box Elder",
+ "Cache",
+ "Carbon",
+ "Daggett",
+ "Davis",
+ "Duchesne",
+ "Emery",
+ "Garfield",
+ "Grand",
+ "Iron",
+ "Juab",
+ "Kane",
+ "Millard",
+ "Morgan",
+ "Piute",
+ "Rich",
+ "Salt Lake",
+ "San Juan",
+ "Sanpete",
+ "Sevier",
+ "Summit",
+ "Tooele",
+ "Uintah",
+ "Utah",
+ "Wasatch",
+ "Washington",
+ "Wayne",
+ "Weber",
+];
+
+let dashboardData = null;
+let visibleScorecards = [];
+
+function byId(id) {
+ return document.getElementById(id);
+}
+
+function setText(id, value) {
+ const element = byId(id);
+ if (element) element.textContent = value;
+}
+
+function normalizeCounty(value) {
+ return String(value).trim().toLowerCase().replaceAll("_", " ").replace(/\s+/g, " ");
+}
+
+function displayCategory(value) {
+ return String(value)
+ .replaceAll("_", " ")
+ .toLowerCase()
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}
+
+function routeFromHash() {
+ const candidate = window.location.hash.replace(/^#/, "").toLowerCase();
+ return Object.hasOwn(ROUTES, candidate) ? candidate : "overview";
+}
+
+function showRoute({ announce = true, focus = false } = {}) {
+ const route = routeFromHash();
+ for (const view of document.querySelectorAll("[data-view]")) {
+ view.hidden = view.dataset.view !== route;
+ }
+ for (const link of document.querySelectorAll("[data-route]")) {
+ if (link.dataset.route === route) link.setAttribute("aria-current", "page");
+ else link.removeAttribute("aria-current");
+ }
+ document.title = `${ROUTES[route]} · Utah Vehicle Health`;
+ if (announce) setText("route-announcer", `${ROUTES[route]} view`);
+ if (focus) {
+ const heading = document.querySelector(`[data-view="${route}"] h1`);
+ if (heading) {
+ heading.setAttribute("tabindex", "-1");
+ heading.focus({ preventScroll: true });
+ heading.scrollIntoView({ behavior: "smooth", block: "start" });
+ }
+ }
+}
+
+function initializeRouting() {
+ showRoute({ announce: false, focus: false });
+ window.addEventListener("hashchange", () => showRoute({ focus: true }));
+}
+
+function setHeaderStatus(state, message) {
+ const status = byId("header-status");
+ if (!status) return;
+ status.dataset.state = state;
+ const text = status.querySelector("span:last-child");
+ if (text) text.textContent = message;
+}
+
+function setPreviewBanner(manifest) {
+ const preview = manifest.development_preview || !manifest.population_estimate_allowed;
+ const banner = byId("preview-banner");
+ banner.hidden = !preview;
+ if (!preview) return;
+ setText("preview-title", "Development preview");
+ setText(
+ "preview-copy",
+ "These suppressed sample aggregates are not population estimates and must not be used for individual decisions.",
+ );
+}
+
+function setUnavailable(error) {
+ const safeMessage =
+ error instanceof Error && error.message
+ ? `${error.message} No estimates are being shown.`
+ : "The approved aggregate files could not be validated. No estimates are being shown.";
+ for (const state of document.querySelectorAll("[data-unavailable]")) {
+ state.hidden = false;
+ const copy = state.querySelector("[data-unavailable-message]");
+ if (copy) copy.textContent = safeMessage;
+ }
+ for (const region of document.querySelectorAll("[data-requires-data]")) {
+ region.hidden = true;
+ }
+ const banner = byId("preview-banner");
+ banner.hidden = false;
+ setText("preview-title", "Data unavailable");
+ setText("preview-copy", "This static shell could not validate every required public aggregate. No fallback or estimated values are displayed.");
+ setHeaderStatus("error", "Approved data unavailable");
+}
+
+function populateSelect(id, values, formatter = displayCategory) {
+ const select = byId(id);
+ if (!select) return;
+ for (const value of values) {
+ const option = document.createElement("option");
+ option.value = String(value);
+ option.textContent = formatter(value);
+ select.append(option);
+ }
+}
+
+function disableUnsupportedFilter(id, explanation) {
+ const select = byId(id);
+ if (!select) return;
+ select.disabled = true;
+ select.title = explanation;
+ select.options[0].textContent = explanation;
+}
+
+function renderCountyList(coveredCounties) {
+ const container = byId("county-coverage-list");
+ container.replaceChildren();
+ const normalized = new Set(coveredCounties.map(normalizeCounty));
+ for (const county of UTAH_COUNTIES) {
+ const chip = document.createElement("span");
+ chip.className = "county-chip";
+ chip.dataset.covered = String(normalized.has(normalizeCounty(county)));
+ chip.textContent = county;
+ container.append(chip);
+ }
+}
+
+function renderOverview(data) {
+ const rows = data.overview.rows;
+ const periods = aggregateOverview(rows);
+ const totalSupport = rows.reduce((sum, row) => sum + row.support_rounded, 0);
+ const weightedNonpass = rows.reduce(
+ (sum, row) => sum + row.support_rounded * row.nonpass_rate,
+ 0,
+ );
+ const nonpassRate = totalSupport > 0 ? weightedNonpass / totalSupport : 0;
+ const counties = [...new Set(rows.map((row) => row.public_county))].sort();
+ const latest = periods[periods.length - 1];
+
+ setText("kpi-eligible", `≈${compactNumber(totalSupport)}`);
+ setText("kpi-pass", `≈${percent(1 - nonpassRate)}`);
+ setText("kpi-nonpass", `≈${percent(nonpassRate)}`);
+ setText("kpi-counties", String(counties.length));
+ setText("data-cutoff", latest ? `${latest.year} Q${latest.quarter}` : "Unavailable");
+ setText(
+ "model-version",
+ `Release ${data.manifest.release_id.slice(0, 8)} · ${data.manifest.model_versions.join(" / ")}`,
+ );
+ renderOutcomeTrend(byId("outcome-trend-chart"), rows);
+ setText(
+ "outcome-trend-note",
+ "The current approved bundle publishes binary non-pass rates only. Four-class outcome mix and blank rates are not inferred or displayed.",
+ );
+ renderAgeRisk(byId("age-risk-chart"), data.ageRisk.rows);
+ renderUtahCoverage(byId("utah-coverage-map"), counties);
+ renderCountyList(counties);
+}
+
+function scorecardLabel(row) {
+ return `${row.prior_make} ${row.prior_model}`.trim();
+}
+
+function sortedScorecards(rows, mode) {
+ const sorted = [...rows];
+ if (mode === "risk-desc") sorted.sort((a, b) => b.nonpass_rate - a.nonpass_rate);
+ else if (mode === "risk-asc") sorted.sort((a, b) => a.nonpass_rate - b.nonpass_rate);
+ else if (mode === "name") sorted.sort((a, b) => scorecardLabel(a).localeCompare(scorecardLabel(b)));
+ else sorted.sort((a, b) => b.support_rounded - a.support_rounded);
+ return sorted;
+}
+
+function renderScorecardCards(rows) {
+ const container = byId("cohort-cards");
+ container.replaceChildren();
+ for (const row of rows.slice(0, 18)) {
+ const article = document.createElement("article");
+ article.className = "cohort-card";
+ const heading = document.createElement("h3");
+ heading.textContent = scorecardLabel(row);
+ const meta = document.createElement("div");
+ meta.className = "cohort-card__meta";
+ meta.textContent = `${compactNumber(row.support_rounded)} rounded support`;
+ const bar = document.createElement("div");
+ bar.className = "risk-bar";
+ bar.setAttribute("aria-hidden", "true");
+ const fill = document.createElement("span");
+ fill.style.width = `${Math.min(100, row.nonpass_rate * 100)}%`;
+ bar.append(fill);
+ const value = document.createElement("div");
+ value.className = "cohort-card__value";
+ const strong = document.createElement("strong");
+ strong.textContent = percent(row.nonpass_rate);
+ const small = document.createElement("small");
+ small.textContent = "Observed non-pass";
+ value.append(strong, small);
+ article.append(heading, meta, bar, value);
+ container.append(article);
+ }
+}
+
+function updateExplorer() {
+ if (!dashboardData) return;
+ const query = byId("cohort-search").value.trim().toLowerCase();
+ const sortMode = byId("result-sort").value;
+ const matches = dashboardData.scorecards.rows.filter((row) =>
+ scorecardLabel(row).toLowerCase().includes(query),
+ );
+ visibleScorecards = sortedScorecards(matches, sortMode);
+ setText(
+ "result-summary",
+ `${visibleScorecards.length} supported cohort${visibleScorecards.length === 1 ? "" : "s"}; showing up to 18 cards and 12 chart rows.`,
+ );
+ byId("empty-results").hidden = visibleScorecards.length > 0;
+ renderCohortDotPlot(byId("cohort-dot-plot"), visibleScorecards);
+ renderScorecardCards(visibleScorecards);
+}
+
+function initializeExplorer(data) {
+ populateSelect("filter-county", data.filters.public_counties);
+ populateSelect("filter-age", data.filters.age_bands, (value) => String(value));
+ populateSelect(
+ "filter-period",
+ data.filters.periods.map((period) => period.year),
+ (value) => String(value),
+ );
+
+ // Current scorecards are make/model aggregates only. These planned controls
+ // remain visible but disabled so the UI never implies unsupported slicing.
+ disableUnsupportedFilter("filter-county", "Unavailable at current scorecard grain");
+ disableUnsupportedFilter("filter-age", "Unavailable at current scorecard grain");
+ disableUnsupportedFilter("filter-fuel", "Fuel not published in this bundle");
+ disableUnsupportedFilter("filter-program", "Program not published in this bundle");
+ disableUnsupportedFilter("filter-period", "Period not published in this scorecard");
+ const adjusted = document.querySelector('input[name="risk-view"][value="adjusted"]');
+ adjusted.disabled = true;
+ adjusted.parentElement.title = "Model-adjusted cohort scorecards are not published.";
+
+ byId("cohort-search").addEventListener("input", updateExplorer);
+ byId("result-sort").addEventListener("change", updateExplorer);
+ byId("reset-filters").addEventListener("click", () => {
+ byId("explorer-filters").reset();
+ byId("cohort-search").value = "";
+ byId("result-sort").value = "support";
+ updateExplorer();
+ });
+ updateExplorer();
+}
+
+function chooseDiagnostic(rows) {
+ const partitionRank = { calibrate: 3, tune: 2, train: 1 };
+ return [...rows].sort((left, right) => {
+ const preferredLeft = left.model === "logistic_platt" ? 1 : 0;
+ const preferredRight = right.model === "logistic_platt" ? 1 : 0;
+ return (
+ preferredRight - preferredLeft ||
+ (partitionRank[right.partition] ?? 0) - (partitionRank[left.partition] ?? 0)
+ );
+ })[0];
+}
+
+function renderMethods(data) {
+ renderCoverageHeatmap(byId("coverage-heatmap"), data.coverage.rows);
+ const diagnostic = chooseDiagnostic(data.diagnostics.rows);
+ setText(
+ "diagnostic-scope-label",
+ diagnostic
+ ? `${diagnostic.partition === "calibrate" ? "Calibration cohort" : "Development diagnostics"} · ${diagnostic.model}`
+ : "Development diagnostics · model unavailable",
+ );
+ setText("diagnostic-ap", diagnostic ? diagnostic.average_precision.toFixed(3) : "—");
+ setText("diagnostic-brier", diagnostic ? diagnostic.brier.toFixed(3) : "—");
+ setText("diagnostic-n", "Not published");
+ renderNoCalibration(byId("calibration-chart"));
+}
+
+function renderDashboard(data) {
+ dashboardData = data;
+ setPreviewBanner(data.manifest);
+ renderOverview(data);
+ initializeExplorer(data);
+ renderMethods(data);
+ setHeaderStatus(
+ "ready",
+ data.manifest.development_preview ? "Validated development aggregates" : "Validated public aggregates",
+ );
+}
+
+async function initialize() {
+ initializeRouting();
+ try {
+ const data = await loadDashboardData();
+ renderDashboard(data);
+ } catch (error) {
+ console.error("Dashboard data contract rejected the public assets.", error);
+ setUnavailable(error);
+ }
+}
+
+initialize();
+
+export { displayCategory, normalizeCounty, routeFromHash };
diff --git a/dashboard/js/charts.js b/dashboard/js/charts.js
new file mode 100644
index 0000000..2372c89
--- /dev/null
+++ b/dashboard/js/charts.js
@@ -0,0 +1,281 @@
+const SVG_NS = "http://www.w3.org/2000/svg";
+
+function escapeText(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function percent(value, digits = 1) {
+ return new Intl.NumberFormat("en-US", {
+ style: "percent",
+ minimumFractionDigits: digits,
+ maximumFractionDigits: digits,
+ }).format(value);
+}
+
+function compactNumber(value) {
+ return new Intl.NumberFormat("en-US", {
+ notation: value >= 10_000 ? "compact" : "standard",
+ maximumFractionDigits: 1,
+ }).format(value);
+}
+
+function emptyChart(container, message) {
+ container.innerHTML = ``;
+ container.setAttribute("aria-label", message);
+}
+
+function aggregateOverview(rows) {
+ const groups = new Map();
+ for (const row of rows) {
+ const key = `${row.year}-Q${row.quarter}`;
+ const current = groups.get(key) ?? {
+ label: `${row.year} Q${row.quarter}`,
+ year: row.year,
+ quarter: row.quarter,
+ support: 0,
+ weightedRisk: 0,
+ };
+ current.support += row.support_rounded;
+ current.weightedRisk += row.support_rounded * row.nonpass_rate;
+ groups.set(key, current);
+ }
+ return [...groups.values()]
+ .map((group) => ({
+ ...group,
+ risk: group.support > 0 ? group.weightedRisk / group.support : 0,
+ }))
+ .sort((left, right) => left.year - right.year || left.quarter - right.quarter);
+}
+
+export function renderOutcomeTrend(container, rows) {
+ const points = aggregateOverview(rows);
+ if (points.length < 2) {
+ emptyChart(container, "Not enough approved periods to draw a trend.");
+ return;
+ }
+ const width = 920;
+ const height = 330;
+ const margin = { top: 24, right: 22, bottom: 54, left: 58 };
+ const plotWidth = width - margin.left - margin.right;
+ const plotHeight = height - margin.top - margin.bottom;
+ const maxRisk = Math.max(0.05, ...points.map((point) => point.risk));
+ const yMax = Math.min(1, Math.ceil(maxRisk * 20) / 20);
+ const x = (index) => margin.left + (index / (points.length - 1)) * plotWidth;
+ const y = (value) => margin.top + plotHeight - (value / yMax) * plotHeight;
+ const path = points
+ .map((point, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(1)},${y(point.risk).toFixed(1)}`)
+ .join(" ");
+
+ const yTicks = Array.from({ length: 5 }, (_, index) => (index / 4) * yMax);
+ const tickEvery = Math.max(1, Math.ceil(points.length / 8));
+ const labelled = points.filter(
+ (_point, index) => index % tickEvery === 0 || index === points.length - 1,
+ );
+ const description = points
+ .map((point) => `${point.label}: ${percent(point.risk)}`)
+ .join("; ");
+
+ container.innerHTML = `
+
+ ${yTicks
+ .map(
+ (tick) => `
+
+ ${percent(tick, 0)} `,
+ )
+ .join("")}
+
+
+ ${points
+ .map(
+ (point, index) => `
+
+ ${escapeText(point.label)}: ${percent(point.risk)} non-pass (${compactNumber(point.support)} rounded support)
+ `,
+ )
+ .join("")}
+ ${labelled
+ .map((point) => {
+ const index = points.indexOf(point);
+ return `${escapeText(point.label.replace(" ", "\u00a0"))} `;
+ })
+ .join("")}
+ Non-pass rate
+ `;
+ container.setAttribute(
+ "aria-label",
+ `Quarterly aggregate non-pass trend. ${description}`,
+ );
+}
+
+export function renderAgeRisk(container, rows) {
+ if (!rows.length) {
+ emptyChart(container, "No supported vehicle-age bands are available.");
+ return;
+ }
+ const points = [...rows];
+ const width = 600;
+ const height = 300;
+ const margin = { top: 24, right: 20, bottom: 58, left: 52 };
+ const plotWidth = width - margin.left - margin.right;
+ const plotHeight = height - margin.top - margin.bottom;
+ const maxRisk = Math.max(0.05, ...points.map((point) => point.nonpass_rate));
+ const yMax = Math.min(1, Math.ceil(maxRisk * 20) / 20);
+ const denominator = Math.max(1, points.length - 1);
+ const x = (index) => margin.left + (index / denominator) * plotWidth;
+ const y = (value) => margin.top + plotHeight - (value / yMax) * plotHeight;
+ const path = points
+ .map((point, index) => `${index === 0 ? "M" : "L"}${x(index)},${y(point.nonpass_rate)}`)
+ .join(" ");
+
+ container.innerHTML = `
+
+ ${[0, 0.25, 0.5, 0.75, 1]
+ .map((ratio) => {
+ const tick = ratio * yMax;
+ return `
+ ${percent(tick, 0)} `;
+ })
+ .join("")}
+
+ ${points
+ .map(
+ (point, index) => `
+
+ ${escapeText(point.age_band)}: ${percent(point.nonpass_rate)} (${compactNumber(point.support_rounded)} rounded support)
+
+ ${escapeText(point.age_band)} `,
+ )
+ .join("")}
+ `;
+ container.setAttribute(
+ "aria-label",
+ `Non-pass risk by vehicle-age band. ${points
+ .map((point) => `${point.age_band}: ${percent(point.nonpass_rate)}`)
+ .join("; ")}`,
+ );
+}
+
+export function renderCohortDotPlot(container, rows) {
+ const points = rows.slice(0, 12);
+ if (!points.length) {
+ emptyChart(container, "No supported cohorts match the current filters.");
+ return;
+ }
+ const width = 820;
+ const rowHeight = 34;
+ const margin = { top: 24, right: 60, bottom: 42, left: 220 };
+ const height = margin.top + margin.bottom + points.length * rowHeight;
+ const plotWidth = width - margin.left - margin.right;
+ const maxRisk = Math.max(0.05, ...points.map((point) => point.nonpass_rate));
+ const xMax = Math.min(1, Math.ceil(maxRisk * 20) / 20);
+ const x = (value) => margin.left + (value / xMax) * plotWidth;
+
+ container.innerHTML = `
+
+ ${[0, 0.25, 0.5, 0.75, 1]
+ .map((ratio) => {
+ const value = ratio * xMax;
+ return `
+ ${percent(value, 0)} `;
+ })
+ .join("")}
+ ${points
+ .map((point, index) => {
+ const y = margin.top + index * rowHeight + rowHeight / 2;
+ const label = `${point.prior_make} ${point.prior_model}`;
+ return `
+ ${escapeText(label)}
+
+ ${escapeText(label)}: ${percent(point.nonpass_rate)}, ${compactNumber(point.support_rounded)} rounded support
+ ${percent(point.nonpass_rate)} `;
+ })
+ .join("")}
+ `;
+ container.setAttribute(
+ "aria-label",
+ `Ranked supported cohort non-pass risk. ${points
+ .map((point) => `${point.prior_make} ${point.prior_model}: ${percent(point.nonpass_rate)}`)
+ .join("; ")}`,
+ );
+}
+
+const COUNTY_POINTS = {
+ cache: [154, 40],
+ weber: [137, 86],
+ davis: [126, 108],
+ salt_lake: [133, 133],
+ "salt lake": [133, 133],
+ utah: [140, 170],
+};
+
+export function renderUtahCoverage(container, coveredCounties) {
+ const normalized = new Set(coveredCounties.map((county) => county.toLowerCase()));
+ const points = [...normalized]
+ .map((county) => ({ county, coordinates: COUNTY_POINTS[county] }))
+ .filter((item) => item.coordinates);
+ container.innerHTML = `
+
+
+
+ ${points
+ .map(
+ ({ county, coordinates }) => `
+ ${escapeText(county.replace("_", " "))} feed available `,
+ )
+ .join("")}
+ Participating feeds highlighted
+ `;
+ container.setAttribute(
+ "aria-label",
+ coveredCounties.length
+ ? `Utah feed coverage includes ${coveredCounties.join(", ")}. Other counties are unavailable.`
+ : "No county feed coverage is available.",
+ );
+}
+
+export function renderCoverageHeatmap(container, rows) {
+ if (!rows.length) {
+ emptyChart(container, "No source-era coverage rows are available.");
+ return;
+ }
+ const years = [...new Set(rows.map((row) => row.year))].sort((a, b) => a - b);
+ const eras = [...new Set(rows.map((row) => row.source_era))].sort();
+ const lookup = new Map(rows.map((row) => [`${row.source_era}-${row.year}`, row]));
+ const maxSupport = Math.max(1, ...rows.map((row) => row.support_rounded));
+ const columns = `90px repeat(${years.length}, minmax(34px, 1fr))`;
+ container.innerHTML = `
+
+ ${years.map((year) => `${year} `).join("")}
+ ${eras
+ .map(
+ (era) => `${escapeText(era)} ${years
+ .map((year) => {
+ const row = lookup.get(`${era}-${year}`);
+ if (!row) return ` `;
+ const alpha = 0.15 + 0.75 * Math.sqrt(row.support_rounded / maxSupport);
+ return ` `;
+ })
+ .join("")}`,
+ )
+ .join("")}
+
`;
+ container.setAttribute(
+ "aria-label",
+ `Source-era coverage from ${years[0]} through ${years[years.length - 1]} for ${eras.join(", ")}.`,
+ );
+}
+
+export function renderNoCalibration(container) {
+ emptyChart(
+ container,
+ "Calibration-bin data is not published in this development bundle. No calibration curve is shown.",
+ );
+}
+
+export { aggregateOverview, compactNumber, percent };
diff --git a/dashboard/js/data.js b/dashboard/js/data.js
new file mode 100644
index 0000000..21c2133
--- /dev/null
+++ b/dashboard/js/data.js
@@ -0,0 +1,574 @@
+const SCHEMA_VERSION = "dashboard_data_v1";
+const APPROVED_PARTITIONS = Object.freeze(["train", "tune", "calibrate"]);
+const ENVELOPE_KEYS = Object.freeze([
+ "development_preview",
+ "population_estimate_allowed",
+ "schema_version",
+]);
+
+export const REQUIRED_ASSETS = Object.freeze({
+ manifest: "data_manifest.json",
+ overview: "overview_period_county.json",
+ ageRisk: "age_risk_curve.json",
+ scorecards: "cohort_scorecard.json",
+ diagnostics: "model_diagnostics.json",
+ coverage: "coverage_quality.json",
+ filters: "filter_catalog.json",
+ shaManifest: "sha256_manifest.json",
+});
+
+const SENSITIVE_KEY_PARTS = new Set([
+ "address",
+ "certificate",
+ "email",
+ "internal",
+ "ip",
+ "owner",
+ "pid",
+ "plate",
+ "raw",
+ "session",
+ "station",
+ "token",
+ "user",
+ "vin",
+ "zip",
+]);
+
+const ASSET_VALIDATORS = {
+ overview: (value) => validateRows(value, validateOverviewRow, "overview_period_county"),
+ ageRisk: (value) => validateRows(value, validateAgeRiskRow, "age_risk_curve"),
+ scorecards: (value) => validateRows(value, validateScorecardRow, "cohort_scorecard"),
+ diagnostics: (value) => validateRows(value, validateDiagnosticRow, "model_diagnostics"),
+ coverage: (value) => validateRows(value, validateCoverageRow, "coverage_quality"),
+ filters: validateFilterCatalog,
+ shaManifest: validateShaManifest,
+};
+
+export class DataContractError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = "DataContractError";
+ }
+}
+
+function isPlainObject(value) {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
+ const prototype = Object.getPrototypeOf(value);
+ return prototype === Object.prototype || prototype === null;
+}
+
+function requirePlainObject(value, label) {
+ if (!isPlainObject(value)) {
+ throw new DataContractError(`${label} must be a JSON object.`);
+ }
+}
+
+function requireExactKeys(value, expectedKeys, label) {
+ const observed = Object.keys(value).sort();
+ const expected = [...expectedKeys].sort();
+ if (JSON.stringify(observed) !== JSON.stringify(expected)) {
+ throw new DataContractError(`${label} does not match the approved field schema.`);
+ }
+}
+
+function requireExactStringSet(value, expectedValues, label) {
+ validateStringArray(value, label);
+ const observed = [...value].sort();
+ const expected = [...expectedValues].sort();
+ if (JSON.stringify(observed) !== JSON.stringify(expected)) {
+ throw new DataContractError(`${label} does not match the approved values.`);
+ }
+}
+
+function requireBoolean(value, label) {
+ if (typeof value !== "boolean") {
+ throw new DataContractError(`${label} must be true or false.`);
+ }
+}
+
+function requireString(value, label) {
+ if (typeof value !== "string" || value.trim() === "") {
+ throw new DataContractError(`${label} must be a non-empty string.`);
+ }
+}
+
+function requireSafeModelVersion(value, label) {
+ requireString(value, label);
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(value)) {
+ throw new DataContractError(`${label} is not an approved model version.`);
+ }
+}
+
+function requireNumber(value, label, { min = -Infinity, max = Infinity } = {}) {
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
+ throw new DataContractError(`${label} must be a finite number in the approved range.`);
+ }
+}
+
+function requireInteger(value, label, { min = -Infinity, max = Infinity } = {}) {
+ requireNumber(value, label, { min, max });
+ if (!Number.isInteger(value)) {
+ throw new DataContractError(`${label} must be an integer.`);
+ }
+}
+
+function validateEnvelope(value, label) {
+ requirePlainObject(value, label);
+ if (value.schema_version !== SCHEMA_VERSION) {
+ throw new DataContractError(`${label} uses an unsupported schema version.`);
+ }
+ requireBoolean(value.development_preview, `${label}.development_preview`);
+ requireBoolean(
+ value.population_estimate_allowed,
+ `${label}.population_estimate_allowed`,
+ );
+ rejectSensitiveKeys(value, label);
+}
+
+function validateRows(value, rowValidator, label) {
+ validateEnvelope(value, label);
+ requireExactKeys(value, [...ENVELOPE_KEYS, "rows"], label);
+ if (!Array.isArray(value.rows)) {
+ throw new DataContractError(`${label}.rows must be an array.`);
+ }
+ value.rows.forEach((row, index) => {
+ requirePlainObject(row, `${label}.rows[${index}]`);
+ rowValidator(row, `${label}.rows[${index}]`);
+ });
+ return value;
+}
+
+function validateOverviewRow(row, label) {
+ requireExactKeys(
+ row,
+ ["year", "quarter", "public_county", "support_rounded", "nonpass_rate"],
+ label,
+ );
+ requireInteger(row.year, `${label}.year`, { min: 2010, max: 2100 });
+ requireInteger(row.quarter, `${label}.quarter`, { min: 1, max: 4 });
+ requireString(row.public_county, `${label}.public_county`);
+ requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
+ requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
+}
+
+function validateAgeRiskRow(row, label) {
+ requireExactKeys(row, ["age_band", "support_rounded", "nonpass_rate"], label);
+ requireString(row.age_band, `${label}.age_band`);
+ requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
+ requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
+}
+
+function validateScorecardRow(row, label) {
+ requireExactKeys(
+ row,
+ ["prior_make", "prior_model", "support_rounded", "nonpass_rate"],
+ label,
+ );
+ requireString(row.prior_make, `${label}.prior_make`);
+ requireString(row.prior_model, `${label}.prior_model`);
+ requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
+ requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
+}
+
+function validateDiagnosticRow(row, label) {
+ requireExactKeys(
+ row,
+ [
+ "model",
+ "partition",
+ "average_precision",
+ "brier",
+ "log_loss",
+ "roc_auc",
+ "top_10_capture",
+ ],
+ label,
+ );
+ requireString(row.model, `${label}.model`);
+ requireString(row.partition, `${label}.partition`);
+ if (!APPROVED_PARTITIONS.includes(row.partition)) {
+ throw new DataContractError(`${label}.partition is not approved for this public schema.`);
+ }
+ for (const metric of [
+ "average_precision",
+ "brier",
+ "log_loss",
+ "roc_auc",
+ "top_10_capture",
+ ]) {
+ requireNumber(row[metric], `${label}.${metric}`, { min: 0 });
+ }
+ if (row.average_precision > 1 || row.brier > 1 || row.roc_auc > 1 || row.top_10_capture > 1) {
+ throw new DataContractError(`${label} contains a probability metric above 1.`);
+ }
+}
+
+function validateCoverageRow(row, label) {
+ requireExactKeys(
+ row,
+ [
+ "year",
+ "source_era",
+ "support_rounded",
+ "labeled_rate",
+ "overall_result_share",
+ "utah_obd_proxy_share",
+ ],
+ label,
+ );
+ requireInteger(row.year, `${label}.year`, { min: 2010, max: 2100 });
+ requireString(row.source_era, `${label}.source_era`);
+ requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
+ for (const metric of ["labeled_rate", "overall_result_share", "utah_obd_proxy_share"]) {
+ requireNumber(row[metric], `${label}.${metric}`, { min: 0, max: 1 });
+ }
+}
+
+function validateStringArray(value, label) {
+ if (!Array.isArray(value)) throw new DataContractError(`${label} must be an array.`);
+ value.forEach((item, index) => requireString(item, `${label}[${index}]`));
+}
+
+function validateFilterCatalog(value) {
+ const label = "filter_catalog";
+ validateEnvelope(value, label);
+ requireExactKeys(
+ value,
+ [
+ ...ENVELOPE_KEYS,
+ "public_counties",
+ "age_bands",
+ "prior_make_models",
+ "periods",
+ "models",
+ "partitions",
+ ],
+ label,
+ );
+ validateStringArray(value.public_counties, `${label}.public_counties`);
+ validateStringArray(value.age_bands, `${label}.age_bands`);
+ validateStringArray(value.models, `${label}.models`);
+ requireExactStringSet(value.partitions, APPROVED_PARTITIONS, `${label}.partitions`);
+ if (!Array.isArray(value.prior_make_models)) {
+ throw new DataContractError(`${label}.prior_make_models must be an array.`);
+ }
+ value.prior_make_models.forEach((row, index) => {
+ requirePlainObject(row, `${label}.prior_make_models[${index}]`);
+ requireExactKeys(
+ row,
+ ["prior_make", "prior_model"],
+ `${label}.prior_make_models[${index}]`,
+ );
+ requireString(row.prior_make, `${label}.prior_make_models[${index}].prior_make`);
+ requireString(row.prior_model, `${label}.prior_make_models[${index}].prior_model`);
+ });
+ if (!Array.isArray(value.periods)) {
+ throw new DataContractError(`${label}.periods must be an array.`);
+ }
+ value.periods.forEach((period, index) => {
+ requirePlainObject(period, `${label}.periods[${index}]`);
+ requireExactKeys(period, ["year", "quarters"], `${label}.periods[${index}]`);
+ requireInteger(period.year, `${label}.periods[${index}].year`, {
+ min: 2010,
+ max: 2100,
+ });
+ if (!Array.isArray(period.quarters) || period.quarters.length === 0) {
+ throw new DataContractError(`${label}.periods[${index}].quarters must be non-empty.`);
+ }
+ period.quarters.forEach((quarter, quarterIndex) =>
+ requireInteger(quarter, `${label}.periods[${index}].quarters[${quarterIndex}]`, {
+ min: 1,
+ max: 4,
+ }),
+ );
+ });
+ return value;
+}
+
+function validateShaManifest(value) {
+ const label = "sha256_manifest";
+ validateEnvelope(value, label);
+ requireExactKeys(value, [...ENVELOPE_KEYS, "files"], label);
+ if (!Array.isArray(value.files)) {
+ throw new DataContractError(`${label}.files must be an array.`);
+ }
+ const expectedNames = Object.entries(REQUIRED_ASSETS)
+ .filter(([name]) => name !== "shaManifest")
+ .map(([, filename]) => filename)
+ .sort();
+ const observedNames = [];
+ for (const [index, file] of value.files.entries()) {
+ requirePlainObject(file, `${label}.files[${index}]`);
+ requireExactKeys(file, ["name", "sha256"], `${label}.files[${index}]`);
+ requireString(file.name, `${label}.files[${index}].name`);
+ requireString(file.sha256, `${label}.files[${index}].sha256`);
+ if (!/^[0-9a-f]{64}$/.test(file.sha256)) {
+ throw new DataContractError(`${label}.files[${index}].sha256 is invalid.`);
+ }
+ observedNames.push(file.name);
+ }
+ observedNames.sort();
+ if (JSON.stringify(observedNames) !== JSON.stringify(expectedNames)) {
+ throw new DataContractError(`${label} does not cover the exact approved asset set.`);
+ }
+ return value;
+}
+
+function rejectSensitiveKeys(value, label, seen = new WeakSet()) {
+ if (value === null || typeof value !== "object") return;
+ if (seen.has(value)) return;
+ seen.add(value);
+ if (Array.isArray(value)) {
+ value.forEach((item, index) => rejectSensitiveKeys(item, `${label}[${index}]`, seen));
+ return;
+ }
+ for (const [key, child] of Object.entries(value)) {
+ const normalizedParts = key.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
+ if (normalizedParts.some((part) => SENSITIVE_KEY_PARTS.has(part))) {
+ throw new DataContractError(`${label} contains a field outside the public data contract.`);
+ }
+ rejectSensitiveKeys(child, `${label}.${key}`, seen);
+ }
+}
+
+export function validateAssetSet(rawAssets) {
+ requirePlainObject(rawAssets, "asset set");
+ requireExactKeys(rawAssets, Object.keys(REQUIRED_ASSETS), "asset set");
+ const manifest = rawAssets.manifest;
+ validateEnvelope(manifest, "data_manifest");
+ requireExactKeys(
+ manifest,
+ [
+ ...ENVELOPE_KEYS,
+ "assets",
+ "data_scope",
+ "definitions",
+ "model_versions",
+ "release_id",
+ ],
+ "data_manifest",
+ );
+ if (Object.hasOwn(manifest, "rows")) {
+ throw new DataContractError("data_manifest must not contain row data.");
+ }
+ requirePlainObject(manifest.data_scope, "data_manifest.data_scope");
+ requirePlainObject(manifest.definitions, "data_manifest.definitions");
+ requireString(manifest.release_id, "data_manifest.release_id");
+ if (!/^[0-9a-f]{64}$/.test(manifest.release_id)) {
+ throw new DataContractError("data_manifest.release_id is invalid.");
+ }
+ if (!Array.isArray(manifest.model_versions) || manifest.model_versions.length === 0) {
+ throw new DataContractError("data_manifest.model_versions must be a non-empty array.");
+ }
+ manifest.model_versions.forEach((version, index) =>
+ requireSafeModelVersion(version, `data_manifest.model_versions[${index}]`),
+ );
+ if (
+ JSON.stringify(manifest.model_versions) !==
+ JSON.stringify([...new Set(manifest.model_versions)].sort())
+ ) {
+ throw new DataContractError("data_manifest.model_versions must be sorted and unique.");
+ }
+ requireExactKeys(
+ manifest.data_scope,
+ ["first_year", "last_year", "model_names", "partitions"],
+ "data_manifest.data_scope",
+ );
+ requireInteger(manifest.data_scope.first_year, "data_manifest.data_scope.first_year", {
+ min: 2010,
+ max: 2100,
+ });
+ requireInteger(manifest.data_scope.last_year, "data_manifest.data_scope.last_year", {
+ min: manifest.data_scope.first_year,
+ max: 2100,
+ });
+ validateStringArray(manifest.data_scope.model_names, "data_manifest.data_scope.model_names");
+ requireExactStringSet(
+ manifest.data_scope.partitions,
+ APPROVED_PARTITIONS,
+ "data_manifest.data_scope.partitions",
+ );
+ requireExactKeys(
+ manifest.definitions,
+ [
+ "episode_gap_days",
+ "locked_test_metrics_published",
+ "support_rounding",
+ "suppression_min_nonpass",
+ "suppression_min_pass",
+ "suppression_min_support",
+ "suppression_min_distinct_vehicles",
+ "suppression_min_distinct_pass_vehicles",
+ "suppression_min_distinct_nonpass_vehicles",
+ "target",
+ ],
+ "data_manifest.definitions",
+ );
+ requireInteger(manifest.definitions.episode_gap_days, "data_manifest.definitions.episode_gap_days", {
+ min: 1,
+ });
+ requireBoolean(
+ manifest.definitions.locked_test_metrics_published,
+ "data_manifest.definitions.locked_test_metrics_published",
+ );
+ if (manifest.definitions.locked_test_metrics_published !== false) {
+ throw new DataContractError("Locked-test metrics are not approved for this public preview.");
+ }
+ for (const key of [
+ "support_rounding",
+ "suppression_min_nonpass",
+ "suppression_min_pass",
+ "suppression_min_support",
+ "suppression_min_distinct_vehicles",
+ "suppression_min_distinct_pass_vehicles",
+ "suppression_min_distinct_nonpass_vehicles",
+ ]) {
+ requireInteger(manifest.definitions[key], `data_manifest.definitions.${key}`, { min: 1 });
+ }
+ requireString(manifest.definitions.target, "data_manifest.definitions.target");
+ if (!Array.isArray(manifest.assets)) {
+ throw new DataContractError("data_manifest.assets must be an array.");
+ }
+ validateStringArray(manifest.assets, "data_manifest.assets");
+ const expectedManifestAssets = Object.entries(REQUIRED_ASSETS)
+ .filter(([name]) => !["manifest", "shaManifest"].includes(name))
+ .map(([, filename]) => filename)
+ .sort();
+ const declaredAssets = [...manifest.assets].sort();
+ if (JSON.stringify(declaredAssets) !== JSON.stringify(expectedManifestAssets)) {
+ throw new DataContractError("data_manifest.assets does not match the approved asset set.");
+ }
+
+ const validated = { manifest };
+ for (const [name, validator] of Object.entries(ASSET_VALIDATORS)) {
+ if (!Object.hasOwn(rawAssets, name)) {
+ throw new DataContractError(`A required approved aggregate is missing: ${name}.`);
+ }
+ validated[name] = validator(rawAssets[name]);
+ if (
+ validated[name].development_preview !== manifest.development_preview ||
+ validated[name].population_estimate_allowed !== manifest.population_estimate_allowed
+ ) {
+ throw new DataContractError(`${name} publication flags disagree with data_manifest.`);
+ }
+ }
+ requireExactStringSet(
+ validated.filters.models,
+ manifest.data_scope.model_names,
+ "filter_catalog.models",
+ );
+ for (const [index, row] of validated.diagnostics.rows.entries()) {
+ if (!validated.filters.models.includes(row.model)) {
+ throw new DataContractError(`model_diagnostics.rows[${index}].model is not cataloged.`);
+ }
+ }
+ const minimumSupport = manifest.definitions.suppression_min_support;
+ const supportRounding = manifest.definitions.support_rounding;
+ for (const name of ["overview", "ageRisk", "scorecards", "coverage"]) {
+ for (const [index, row] of validated[name].rows.entries()) {
+ if (
+ row.support_rounded < minimumSupport ||
+ row.support_rounded % supportRounding !== 0
+ ) {
+ throw new DataContractError(
+ `${name}.rows[${index}].support_rounded violates the publication thresholds.`,
+ );
+ }
+ }
+ }
+ return Object.freeze(validated);
+}
+
+async function fetchBytes(path, fetchImplementation) {
+ let response;
+ try {
+ response = await fetchImplementation(path, {
+ cache: "no-store",
+ credentials: "same-origin",
+ });
+ } catch {
+ throw new DataContractError("Approved dashboard assets could not be reached.");
+ }
+ if (!response.ok) {
+ throw new DataContractError("One or more approved dashboard assets are unavailable.");
+ }
+ try {
+ return await response.arrayBuffer();
+ } catch {
+ throw new DataContractError("An approved dashboard asset could not be read safely.");
+ }
+}
+
+function parseJsonBytes(bytes, label) {
+ if (typeof TextDecoder !== "function") {
+ throw new DataContractError("This browser cannot decode dashboard assets safely.");
+ }
+ try {
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
+ } catch {
+ throw new DataContractError(`${label} is not valid UTF-8 JSON.`);
+ }
+}
+
+async function sha256Hex(bytes, cryptoImplementation) {
+ if (!cryptoImplementation?.subtle || typeof cryptoImplementation.subtle.digest !== "function") {
+ throw new DataContractError("This browser cannot verify dashboard asset integrity.");
+ }
+ let digest;
+ try {
+ digest = await cryptoImplementation.subtle.digest("SHA-256", bytes);
+ } catch {
+ throw new DataContractError("Dashboard asset integrity verification failed.");
+ }
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
+}
+
+export async function loadDashboardData({
+ basePath = "./public/data/",
+ fetchImplementation = globalThis.fetch,
+ cryptoImplementation = globalThis.crypto,
+} = {}) {
+ if (typeof fetchImplementation !== "function") {
+ throw new DataContractError("This browser cannot load dashboard assets safely.");
+ }
+ const baseUrl = new URL(basePath, globalThis.location?.href ?? "http://local/");
+ const shaBytes = await fetchBytes(
+ new URL(REQUIRED_ASSETS.shaManifest, baseUrl),
+ fetchImplementation,
+ );
+ const shaManifest = validateShaManifest(
+ parseJsonBytes(shaBytes, "The checksum manifest"),
+ );
+ const expectedDigests = new Map(
+ shaManifest.files.map((entry) => [entry.name, entry.sha256]),
+ );
+ const assetEntries = Object.entries(REQUIRED_ASSETS).filter(
+ ([name]) => name !== "shaManifest",
+ );
+ const rawEntries = await Promise.all(
+ assetEntries.map(async ([name, filename]) => [
+ name,
+ filename,
+ await fetchBytes(new URL(filename, baseUrl), fetchImplementation),
+ ]),
+ );
+
+ await Promise.all(
+ rawEntries.map(async ([, filename, bytes]) => {
+ const observed = await sha256Hex(bytes, cryptoImplementation);
+ if (observed !== expectedDigests.get(filename)) {
+ throw new DataContractError(`Integrity verification failed for ${filename}.`);
+ }
+ }),
+ );
+
+ const parsedEntries = rawEntries.map(([name, filename, bytes]) => [
+ name,
+ parseJsonBytes(bytes, filename),
+ ]);
+ parsedEntries.push(["shaManifest", shaManifest]);
+ return validateAssetSet(Object.fromEntries(parsedEntries));
+}
+
+export { SCHEMA_VERSION };
diff --git a/dashboard/package.json b/dashboard/package.json
new file mode 100644
index 0000000..6160fc8
--- /dev/null
+++ b/dashboard/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "utah-vehicle-health-dashboard",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "description": "Dependency-free static dashboard for approved Utah Vehicle Health aggregates.",
+ "scripts": {
+ "start": "node server.mjs",
+ "test": "node --test tests/contract.test.mjs"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/dashboard/public/data/age_risk_curve.json b/dashboard/public/data/age_risk_curve.json
new file mode 100644
index 0000000..319c5ad
--- /dev/null
+++ b/dashboard/public/data/age_risk_curve.json
@@ -0,0 +1 @@
+{"development_preview":true,"population_estimate_allowed":false,"rows":[{"age_band":"0-3","nonpass_rate":0.058,"support_rounded":500},{"age_band":"4-7","nonpass_rate":0.06,"support_rounded":5900},{"age_band":"8-11","nonpass_rate":0.077,"support_rounded":9800},{"age_band":"12-15","nonpass_rate":0.109,"support_rounded":9700},{"age_band":"16-20","nonpass_rate":0.157,"support_rounded":8600},{"age_band":"21+","nonpass_rate":0.221,"support_rounded":4700}],"schema_version":"dashboard_data_v1"}
diff --git a/dashboard/public/data/cohort_scorecard.json b/dashboard/public/data/cohort_scorecard.json
new file mode 100644
index 0000000..a09c894
--- /dev/null
+++ b/dashboard/public/data/cohort_scorecard.json
@@ -0,0 +1 @@
+{"development_preview":true,"population_estimate_allowed":false,"rows":[{"nonpass_rate":0.116,"prior_make":"FORD","prior_model":"F150","support_rounded":1100},{"nonpass_rate":0.09,"prior_make":"HONDA","prior_model":"ACCORD","support_rounded":400},{"nonpass_rate":0.073,"prior_make":"HONDA","prior_model":"CIVIC","support_rounded":300},{"nonpass_rate":0.086,"prior_make":"TOYOTA","prior_model":"CAMRY","support_rounded":500}],"schema_version":"dashboard_data_v1"}
diff --git a/dashboard/public/data/coverage_quality.json b/dashboard/public/data/coverage_quality.json
new file mode 100644
index 0000000..d53b6c3
--- /dev/null
+++ b/dashboard/public/data/coverage_quality.json
@@ -0,0 +1 @@
+{"development_preview":true,"population_estimate_allowed":false,"rows":[{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3100,"utah_obd_proxy_share":0.0,"year":2016},{"labeled_rate":0.883,"overall_result_share":0.002,"source_era":"utah","support_rounded":600,"utah_obd_proxy_share":0.998,"year":2016},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":700,"utah_obd_proxy_share":0.0,"year":2016},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3200,"utah_obd_proxy_share":0.0,"year":2017},{"labeled_rate":0.899,"overall_result_share":0.0,"source_era":"utah","support_rounded":900,"utah_obd_proxy_share":1.0,"year":2017},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":700,"utah_obd_proxy_share":0.0,"year":2017},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3200,"utah_obd_proxy_share":0.0,"year":2018},{"labeled_rate":0.921,"overall_result_share":0.001,"source_era":"utah","support_rounded":1000,"utah_obd_proxy_share":0.999,"year":2018},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2018},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3300,"utah_obd_proxy_share":0.0,"year":2019},{"labeled_rate":0.93,"overall_result_share":0.001,"source_era":"utah","support_rounded":800,"utah_obd_proxy_share":0.999,"year":2019},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2019},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3200,"utah_obd_proxy_share":0.0,"year":2020},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2020},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":2200,"utah_obd_proxy_share":0.0,"year":2021},{"labeled_rate":0.931,"overall_result_share":0.0,"source_era":"utah","support_rounded":1100,"utah_obd_proxy_share":1.0,"year":2021},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2021},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3000,"utah_obd_proxy_share":0.0,"year":2022},{"labeled_rate":0.927,"overall_result_share":0.001,"source_era":"utah","support_rounded":1100,"utah_obd_proxy_share":0.999,"year":2022},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2022},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3000,"utah_obd_proxy_share":0.0,"year":2023},{"labeled_rate":0.93,"overall_result_share":0.0,"source_era":"utah","support_rounded":1200,"utah_obd_proxy_share":1.0,"year":2023},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2023},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":500,"utah_obd_proxy_share":0.0,"year":2024},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slco","support_rounded":300,"utah_obd_proxy_share":0.0,"year":2024},{"labeled_rate":0.929,"overall_result_share":0.0,"source_era":"utah","support_rounded":1100,"utah_obd_proxy_share":1.0,"year":2024},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2024}],"schema_version":"dashboard_data_v1"}
diff --git a/dashboard/public/data/data_manifest.json b/dashboard/public/data/data_manifest.json
new file mode 100644
index 0000000..44bf720
--- /dev/null
+++ b/dashboard/public/data/data_manifest.json
@@ -0,0 +1 @@
+{"assets":["age_risk_curve.json","cohort_scorecard.json","coverage_quality.json","filter_catalog.json","model_diagnostics.json","overview_period_county.json"],"data_scope":{"first_year":2016,"last_year":2024,"model_names":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"]},"definitions":{"episode_gap_days":30,"locked_test_metrics_published":false,"support_rounding":100,"suppression_min_distinct_nonpass_vehicles":10,"suppression_min_distinct_pass_vehicles":10,"suppression_min_distinct_vehicles":100,"suppression_min_nonpass":10,"suppression_min_pass":10,"suppression_min_support":100,"target":"first-attempt next-episode binary non-pass rate"},"development_preview":true,"model_versions":["baseline_v1","hist_gradient_boosting_v1"],"population_estimate_allowed":false,"release_id":"7c1af8d22d3841b84d2cf4cd5ba7bce6a43578c78bc3d32a25d200b4b1986704","schema_version":"dashboard_data_v1"}
diff --git a/dashboard/public/data/filter_catalog.json b/dashboard/public/data/filter_catalog.json
new file mode 100644
index 0000000..83721a2
--- /dev/null
+++ b/dashboard/public/data/filter_catalog.json
@@ -0,0 +1 @@
+{"age_bands":["0-3","4-7","8-11","12-15","16-20","21+"],"development_preview":true,"models":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"],"periods":[{"quarters":[1,2,3,4],"year":2016},{"quarters":[1,2,3,4],"year":2017},{"quarters":[1,2,3,4],"year":2018},{"quarters":[1,2,3,4],"year":2019},{"quarters":[1,2,3,4],"year":2020},{"quarters":[1,2,3,4],"year":2021},{"quarters":[1,2,3,4],"year":2022},{"quarters":[1,2,3,4],"year":2023},{"quarters":[1,2,3,4],"year":2024}],"population_estimate_allowed":false,"prior_make_models":[{"prior_make":"FORD","prior_model":"F150"},{"prior_make":"HONDA","prior_model":"ACCORD"},{"prior_make":"HONDA","prior_model":"CIVIC"},{"prior_make":"TOYOTA","prior_model":"CAMRY"}],"public_counties":["salt_lake","utah","weber"],"schema_version":"dashboard_data_v1"}
diff --git a/dashboard/public/data/model_diagnostics.json b/dashboard/public/data/model_diagnostics.json
new file mode 100644
index 0000000..c7f2788
--- /dev/null
+++ b/dashboard/public/data/model_diagnostics.json
@@ -0,0 +1 @@
+{"development_preview":true,"population_estimate_allowed":false,"rows":[{"average_precision":0.275,"brier":0.0928,"log_loss":0.3218,"model":"hist_gradient_boosting_platt","partition":"calibrate","roc_auc":0.7072,"top_10_capture":0.2744},{"average_precision":0.3966,"brier":0.0877,"log_loss":0.3011,"model":"hist_gradient_boosting_raw","partition":"train","roc_auc":0.7871,"top_10_capture":0.3658},{"average_precision":0.2743,"brier":0.1011,"log_loss":0.3431,"model":"hist_gradient_boosting_raw","partition":"tune","roc_auc":0.7147,"top_10_capture":0.2635},{"average_precision":0.275,"brier":0.0928,"log_loss":0.3218,"model":"hist_gradient_boosting_raw","partition":"calibrate","roc_auc":0.7072,"top_10_capture":0.2744},{"average_precision":0.2573,"brier":0.0936,"log_loss":0.3261,"model":"logistic_platt","partition":"calibrate","roc_auc":0.6905,"top_10_capture":0.2729},{"average_precision":0.2482,"brier":0.0963,"log_loss":0.3327,"model":"logistic_raw","partition":"train","roc_auc":0.6955,"top_10_capture":0.2593},{"average_precision":0.2821,"brier":0.1012,"log_loss":0.3448,"model":"logistic_raw","partition":"tune","roc_auc":0.7072,"top_10_capture":0.2527},{"average_precision":0.2573,"brier":0.0936,"log_loss":0.3261,"model":"logistic_raw","partition":"calibrate","roc_auc":0.6905,"top_10_capture":0.2729},{"average_precision":0.146,"brier":0.1695,"log_loss":5.8429,"model":"previous_episode_literal","partition":"train","roc_auc":0.5687,"top_10_capture":0.2128},{"average_precision":0.1658,"brier":0.1723,"log_loss":5.9436,"model":"previous_episode_literal","partition":"tune","roc_auc":0.5821,"top_10_capture":0.2291},{"average_precision":0.1476,"brier":0.1677,"log_loss":5.7909,"model":"previous_episode_literal","partition":"calibrate","roc_auc":0.5767,"top_10_capture":0.224},{"average_precision":0.116,"brier":0.1025,"log_loss":0.3588,"model":"training_prevalence","partition":"train","roc_auc":0.5,"top_10_capture":0.1},{"average_precision":0.1246,"brier":0.1092,"log_loss":0.3764,"model":"training_prevalence","partition":"tune","roc_auc":0.5,"top_10_capture":0.1},{"average_precision":0.1129,"brier":0.1002,"log_loss":0.3526,"model":"training_prevalence","partition":"calibrate","roc_auc":0.5,"top_10_capture":0.1}],"schema_version":"dashboard_data_v1"}
diff --git a/dashboard/public/data/overview_period_county.json b/dashboard/public/data/overview_period_county.json
new file mode 100644
index 0000000..5c3357d
--- /dev/null
+++ b/dashboard/public/data/overview_period_county.json
@@ -0,0 +1 @@
+{"development_preview":true,"population_estimate_allowed":false,"rows":[{"nonpass_rate":0.134,"public_county":"salt_lake","quarter":1,"support_rounded":800,"year":2016},{"nonpass_rate":0.142,"public_county":"utah","quarter":1,"support_rounded":100,"year":2016},{"nonpass_rate":0.151,"public_county":"weber","quarter":1,"support_rounded":200,"year":2016},{"nonpass_rate":0.115,"public_county":"salt_lake","quarter":2,"support_rounded":800,"year":2016},{"nonpass_rate":0.078,"public_county":"utah","quarter":2,"support_rounded":200,"year":2016},{"nonpass_rate":0.118,"public_county":"weber","quarter":2,"support_rounded":200,"year":2016},{"nonpass_rate":0.103,"public_county":"salt_lake","quarter":3,"support_rounded":800,"year":2016},{"nonpass_rate":0.081,"public_county":"utah","quarter":3,"support_rounded":200,"year":2016},{"nonpass_rate":0.09,"public_county":"weber","quarter":3,"support_rounded":200,"year":2016},{"nonpass_rate":0.116,"public_county":"salt_lake","quarter":4,"support_rounded":700,"year":2016},{"nonpass_rate":0.092,"public_county":"utah","quarter":4,"support_rounded":100,"year":2016},{"nonpass_rate":0.132,"public_county":"weber","quarter":4,"support_rounded":100,"year":2016},{"nonpass_rate":0.104,"public_county":"salt_lake","quarter":1,"support_rounded":800,"year":2017},{"nonpass_rate":0.075,"public_county":"utah","quarter":1,"support_rounded":200,"year":2017},{"nonpass_rate":0.119,"public_county":"weber","quarter":1,"support_rounded":200,"year":2017},{"nonpass_rate":0.117,"public_county":"salt_lake","quarter":2,"support_rounded":900,"year":2017},{"nonpass_rate":0.075,"public_county":"utah","quarter":2,"support_rounded":200,"year":2017},{"nonpass_rate":0.121,"public_county":"weber","quarter":2,"support_rounded":200,"year":2017},{"nonpass_rate":0.103,"public_county":"salt_lake","quarter":3,"support_rounded":900,"year":2017},{"nonpass_rate":0.079,"public_county":"utah","quarter":3,"support_rounded":200,"year":2017},{"nonpass_rate":0.137,"public_county":"weber","quarter":3,"support_rounded":200,"year":2017},{"nonpass_rate":0.123,"public_county":"salt_lake","quarter":4,"support_rounded":700,"year":2017},{"nonpass_rate":0.123,"public_county":"utah","quarter":4,"support_rounded":100,"year":2017},{"nonpass_rate":0.066,"public_county":"weber","quarter":4,"support_rounded":200,"year":2017},{"nonpass_rate":0.102,"public_county":"salt_lake","quarter":1,"support_rounded":800,"year":2018},{"nonpass_rate":0.088,"public_county":"utah","quarter":1,"support_rounded":200,"year":2018},{"nonpass_rate":0.138,"public_county":"weber","quarter":1,"support_rounded":200,"year":2018},{"nonpass_rate":0.116,"public_county":"salt_lake","quarter":2,"support_rounded":900,"year":2018},{"nonpass_rate":0.092,"public_county":"utah","quarter":2,"support_rounded":200,"year":2018},{"nonpass_rate":0.089,"public_county":"weber","quarter":2,"support_rounded":200,"year":2018},{"nonpass_rate":0.118,"public_county":"salt_lake","quarter":3,"support_rounded":800,"year":2018},{"nonpass_rate":0.107,"public_county":"utah","quarter":3,"support_rounded":300,"year":2018},{"nonpass_rate":0.1,"public_county":"weber","quarter":3,"support_rounded":200,"year":2018},{"nonpass_rate":0.12,"public_county":"salt_lake","quarter":4,"support_rounded":700,"year":2018},{"nonpass_rate":0.103,"public_county":"utah","quarter":4,"support_rounded":200,"year":2018},{"nonpass_rate":0.091,"public_county":"weber","quarter":4,"support_rounded":200,"year":2018},{"nonpass_rate":0.125,"public_county":"salt_lake","quarter":1,"support_rounded":800,"year":2019},{"nonpass_rate":0.081,"public_county":"utah","quarter":1,"support_rounded":200,"year":2019},{"nonpass_rate":0.083,"public_county":"weber","quarter":1,"support_rounded":200,"year":2019},{"nonpass_rate":0.117,"public_county":"salt_lake","quarter":2,"support_rounded":800,"year":2019},{"nonpass_rate":0.097,"public_county":"utah","quarter":2,"support_rounded":300,"year":2019},{"nonpass_rate":0.103,"public_county":"weber","quarter":2,"support_rounded":200,"year":2019},{"nonpass_rate":0.121,"public_county":"salt_lake","quarter":3,"support_rounded":900,"year":2019},{"nonpass_rate":0.092,"public_county":"utah","quarter":3,"support_rounded":200,"year":2019},{"nonpass_rate":0.115,"public_county":"weber","quarter":3,"support_rounded":200,"year":2019},{"nonpass_rate":0.121,"public_county":"salt_lake","quarter":4,"support_rounded":800,"year":2019},{"nonpass_rate":0.068,"public_county":"weber","quarter":4,"support_rounded":200,"year":2019},{"nonpass_rate":0.119,"public_county":"salt_lake","quarter":1,"support_rounded":800,"year":2020},{"nonpass_rate":0.133,"public_county":"weber","quarter":1,"support_rounded":200,"year":2020},{"nonpass_rate":0.125,"public_county":"salt_lake","quarter":2,"support_rounded":900,"year":2020},{"nonpass_rate":0.129,"public_county":"weber","quarter":2,"support_rounded":300,"year":2020},{"nonpass_rate":0.129,"public_county":"salt_lake","quarter":3,"support_rounded":900,"year":2020},{"nonpass_rate":0.146,"public_county":"weber","quarter":3,"support_rounded":200,"year":2020},{"nonpass_rate":0.147,"public_county":"salt_lake","quarter":4,"support_rounded":600,"year":2020},{"nonpass_rate":0.095,"public_county":"weber","quarter":4,"support_rounded":100,"year":2020},{"nonpass_rate":0.131,"public_county":"salt_lake","quarter":1,"support_rounded":400,"year":2021},{"nonpass_rate":0.105,"public_county":"utah","quarter":1,"support_rounded":200,"year":2021},{"nonpass_rate":0.15,"public_county":"weber","quarter":1,"support_rounded":200,"year":2021},{"nonpass_rate":0.117,"public_county":"salt_lake","quarter":2,"support_rounded":700,"year":2021},{"nonpass_rate":0.106,"public_county":"utah","quarter":2,"support_rounded":300,"year":2021},{"nonpass_rate":0.167,"public_county":"weber","quarter":2,"support_rounded":200,"year":2021},{"nonpass_rate":0.117,"public_county":"salt_lake","quarter":3,"support_rounded":500,"year":2021},{"nonpass_rate":0.077,"public_county":"utah","quarter":3,"support_rounded":300,"year":2021},{"nonpass_rate":0.162,"public_county":"weber","quarter":3,"support_rounded":300,"year":2021},{"nonpass_rate":0.133,"public_county":"salt_lake","quarter":4,"support_rounded":500,"year":2021},{"nonpass_rate":0.099,"public_county":"utah","quarter":4,"support_rounded":200,"year":2021},{"nonpass_rate":0.126,"public_county":"weber","quarter":4,"support_rounded":100,"year":2021},{"nonpass_rate":0.125,"public_county":"salt_lake","quarter":1,"support_rounded":700,"year":2022},{"nonpass_rate":0.063,"public_county":"utah","quarter":1,"support_rounded":300,"year":2022},{"nonpass_rate":0.131,"public_county":"weber","quarter":1,"support_rounded":200,"year":2022},{"nonpass_rate":0.143,"public_county":"salt_lake","quarter":2,"support_rounded":700,"year":2022},{"nonpass_rate":0.087,"public_county":"utah","quarter":2,"support_rounded":300,"year":2022},{"nonpass_rate":0.134,"public_county":"weber","quarter":2,"support_rounded":200,"year":2022},{"nonpass_rate":0.136,"public_county":"salt_lake","quarter":3,"support_rounded":800,"year":2022},{"nonpass_rate":0.076,"public_county":"utah","quarter":3,"support_rounded":300,"year":2022},{"nonpass_rate":0.134,"public_county":"weber","quarter":3,"support_rounded":200,"year":2022},{"nonpass_rate":0.132,"public_county":"salt_lake","quarter":4,"support_rounded":700,"year":2022},{"nonpass_rate":0.155,"public_county":"utah","quarter":4,"support_rounded":200,"year":2022},{"nonpass_rate":0.09,"public_county":"weber","quarter":4,"support_rounded":200,"year":2022},{"nonpass_rate":0.121,"public_county":"salt_lake","quarter":1,"support_rounded":700,"year":2023},{"nonpass_rate":0.102,"public_county":"utah","quarter":1,"support_rounded":300,"year":2023},{"nonpass_rate":0.124,"public_county":"weber","quarter":1,"support_rounded":200,"year":2023},{"nonpass_rate":0.107,"public_county":"salt_lake","quarter":2,"support_rounded":800,"year":2023},{"nonpass_rate":0.11,"public_county":"utah","quarter":2,"support_rounded":300,"year":2023},{"nonpass_rate":0.141,"public_county":"weber","quarter":2,"support_rounded":200,"year":2023},{"nonpass_rate":0.138,"public_county":"salt_lake","quarter":3,"support_rounded":800,"year":2023},{"nonpass_rate":0.095,"public_county":"utah","quarter":3,"support_rounded":300,"year":2023},{"nonpass_rate":0.178,"public_county":"weber","quarter":3,"support_rounded":200,"year":2023},{"nonpass_rate":0.137,"public_county":"salt_lake","quarter":4,"support_rounded":700,"year":2023},{"nonpass_rate":0.097,"public_county":"utah","quarter":4,"support_rounded":200,"year":2023},{"nonpass_rate":0.1,"public_county":"weber","quarter":4,"support_rounded":200,"year":2023},{"nonpass_rate":0.124,"public_county":"salt_lake","quarter":1,"support_rounded":500,"year":2024},{"nonpass_rate":0.105,"public_county":"utah","quarter":1,"support_rounded":200,"year":2024},{"nonpass_rate":0.09,"public_county":"weber","quarter":1,"support_rounded":200,"year":2024},{"nonpass_rate":0.123,"public_county":"utah","quarter":2,"support_rounded":300,"year":2024},{"nonpass_rate":0.105,"public_county":"weber","quarter":2,"support_rounded":200,"year":2024},{"nonpass_rate":0.096,"public_county":"utah","quarter":3,"support_rounded":300,"year":2024},{"nonpass_rate":0.15,"public_county":"weber","quarter":3,"support_rounded":200,"year":2024},{"nonpass_rate":0.094,"public_county":"salt_lake","quarter":4,"support_rounded":300,"year":2024},{"nonpass_rate":0.128,"public_county":"utah","quarter":4,"support_rounded":200,"year":2024},{"nonpass_rate":0.098,"public_county":"weber","quarter":4,"support_rounded":200,"year":2024}],"schema_version":"dashboard_data_v1"}
diff --git a/dashboard/public/data/sha256_manifest.json b/dashboard/public/data/sha256_manifest.json
new file mode 100644
index 0000000..86b1e1e
--- /dev/null
+++ b/dashboard/public/data/sha256_manifest.json
@@ -0,0 +1 @@
+{"development_preview":true,"files":[{"name":"age_risk_curve.json","sha256":"5445d52365ad3494ba05f097e7f4f5e41fffeecfe50fbb2952d72044f496c925"},{"name":"cohort_scorecard.json","sha256":"2ae83d9d55aa1f2ccae6420c66350fb491405eedfa90374aa3df2782a4aa0bf1"},{"name":"coverage_quality.json","sha256":"35b7dbed50e1b8260aac18c269f5045010ea23649a649f984a4693f3f00a1e83"},{"name":"data_manifest.json","sha256":"b7196b04eb223683c928cfb5375ed618e1230fa03382bd8a889de24c589557a0"},{"name":"filter_catalog.json","sha256":"9e45d428cd38853002ebd0bda089eb46d0832cd1b7b6b05ac953bc8472b01eb5"},{"name":"model_diagnostics.json","sha256":"ebf99eb32436a5eabb7d754cd88109f2cc0981575dd28966757031ac7faafa57"},{"name":"overview_period_county.json","sha256":"fe2a4233563b47fa31fc87859b100983d3bfb574c5a7fbc7241b9cfb4661b038"}],"population_estimate_allowed":false,"schema_version":"dashboard_data_v1"}
diff --git a/dashboard/server.mjs b/dashboard/server.mjs
new file mode 100644
index 0000000..4cadc66
--- /dev/null
+++ b/dashboard/server.mjs
@@ -0,0 +1,82 @@
+import { createReadStream, realpathSync, statSync } from "node:fs";
+import { createServer } from "node:http";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const ROOT = realpathSync(path.dirname(fileURLToPath(import.meta.url)));
+const HOST = process.env.HOST || "127.0.0.1";
+const PORT = Number.parseInt(process.env.PORT || "4173", 10);
+const TYPES = new Map([
+ [".css", "text/css; charset=utf-8"],
+ [".html", "text/html; charset=utf-8"],
+ [".js", "text/javascript; charset=utf-8"],
+ [".json", "application/json; charset=utf-8"],
+ [".mjs", "text/javascript; charset=utf-8"],
+ [".svg", "image/svg+xml"],
+]);
+const PUBLIC_PATHS = new Set([
+ "/index.html",
+ "/styles.css",
+ "/js/app.js",
+ "/js/charts.js",
+ "/js/data.js",
+ "/public/data/age_risk_curve.json",
+ "/public/data/cohort_scorecard.json",
+ "/public/data/coverage_quality.json",
+ "/public/data/data_manifest.json",
+ "/public/data/filter_catalog.json",
+ "/public/data/model_diagnostics.json",
+ "/public/data/overview_period_county.json",
+ "/public/data/sha256_manifest.json",
+]);
+
+function safeFile(requestUrl) {
+ let pathname;
+ try {
+ pathname = decodeURIComponent(new URL(requestUrl, "http://local").pathname);
+ } catch {
+ return { file: null, status: 400 };
+ }
+ if (pathname === "/") pathname = "/index.html";
+ if (!PUBLIC_PATHS.has(pathname)) return { file: null, status: 404 };
+ try {
+ const candidate = realpathSync(path.join(ROOT, pathname.slice(1)));
+ if (!candidate.startsWith(`${ROOT}${path.sep}`) || !statSync(candidate).isFile()) {
+ return { file: null, status: 404 };
+ }
+ return { file: candidate, status: 200 };
+ } catch {
+ return { file: null, status: 404 };
+ }
+}
+
+const server = createServer((request, response) => {
+ if (!request.url || !["GET", "HEAD"].includes(request.method || "")) {
+ response.writeHead(405, { Allow: "GET, HEAD" });
+ response.end("Method not allowed");
+ return;
+ }
+ const resolved = safeFile(request.url);
+ if (!resolved.file) {
+ response.writeHead(resolved.status, { "Content-Type": "text/plain; charset=utf-8" });
+ response.end(resolved.status === 400 ? "Bad request" : "Not found");
+ return;
+ }
+ const file = resolved.file;
+ const extension = path.extname(file).toLowerCase();
+ response.writeHead(200, {
+ "Content-Type": TYPES.get(extension) || "application/octet-stream",
+ "Cache-Control": extension === ".json" ? "no-store" : "public, max-age=300",
+ "X-Content-Type-Options": "nosniff",
+ "Referrer-Policy": "no-referrer",
+ });
+ if (request.method === "HEAD") {
+ response.end();
+ return;
+ }
+ createReadStream(file).on("error", () => response.destroy()).pipe(response);
+});
+
+server.listen(PORT, HOST, () => {
+ process.stdout.write(`Utah Vehicle Health dashboard: http://${HOST}:${PORT}\n`);
+});
diff --git a/dashboard/styles.css b/dashboard/styles.css
new file mode 100644
index 0000000..c865326
--- /dev/null
+++ b/dashboard/styles.css
@@ -0,0 +1,1358 @@
+:root {
+ color-scheme: light;
+ --ink-950: #172b2b;
+ --ink-800: #294342;
+ --ink-650: #49615f;
+ --ink-500: #6d817f;
+ --paper: #fffdf8;
+ --paper-warm: #faf5e9;
+ --sand-100: #f4ead4;
+ --sand-200: #e7d8b9;
+ --sand-400: #c5a66c;
+ --teal-800: #155e60;
+ --teal-700: #197477;
+ --teal-600: #23888a;
+ --teal-100: #dcefee;
+ --red-700: #a63d32;
+ --red-100: #f7e3df;
+ --amber-700: #a66516;
+ --amber-100: #f9eccd;
+ --purple-700: #704b87;
+ --purple-100: #eee4f3;
+ --gray-100: #edf0ed;
+ --gray-300: #cbd3d0;
+ --white: #ffffff;
+ --shadow-sm: 0 1px 2px rgb(23 43 43 / 7%), 0 7px 24px rgb(23 43 43 / 5%);
+ --shadow-md: 0 18px 48px rgb(23 43 43 / 10%);
+ --radius-sm: 0.55rem;
+ --radius-md: 0.9rem;
+ --radius-lg: 1.35rem;
+ --content: 1180px;
+ --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --font-display: Georgia, "Times New Roman", serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ min-width: 320px;
+ margin: 0;
+ color: var(--ink-950);
+ background:
+ radial-gradient(circle at 7% 2%, rgb(231 216 185 / 35%), transparent 30rem),
+ var(--paper);
+ font-family: var(--font-sans);
+ font-size: 1rem;
+ line-height: 1.55;
+ -webkit-font-smoothing: antialiased;
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+button,
+select {
+ cursor: pointer;
+}
+
+button:disabled,
+select:disabled,
+input:disabled {
+ cursor: not-allowed;
+}
+
+a {
+ color: var(--teal-800);
+}
+
+code {
+ padding: 0.08em 0.28em;
+ border-radius: 0.3em;
+ background: var(--gray-100);
+ font-size: 0.9em;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin-top: 0;
+}
+
+h1,
+h2,
+h3 {
+ line-height: 1.16;
+}
+
+h1 {
+ max-width: 18ch;
+ margin-bottom: 1rem;
+ font-family: var(--font-display);
+ font-size: clamp(2.35rem, 6vw, 4.4rem);
+ font-weight: 500;
+ letter-spacing: -0.035em;
+}
+
+h2 {
+ margin-bottom: 0.7rem;
+ font-size: clamp(1.15rem, 2vw, 1.45rem);
+ letter-spacing: -0.018em;
+}
+
+.skip-link {
+ position: fixed;
+ z-index: 100;
+ top: 0.75rem;
+ left: 0.75rem;
+ padding: 0.75rem 1rem;
+ transform: translateY(-200%);
+ border-radius: var(--radius-sm);
+ color: var(--white);
+ background: var(--ink-950);
+ font-weight: 700;
+}
+
+.skip-link:focus {
+ transform: translateY(0);
+}
+
+:focus-visible {
+ outline: 3px solid var(--amber-700);
+ outline-offset: 3px;
+}
+
+.sr-only {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+
+.preview-banner {
+ position: relative;
+ z-index: 20;
+ display: flex;
+ justify-content: center;
+ gap: 0.65rem;
+ padding: 0.58rem 1rem;
+ border-bottom: 1px solid #e9c77d;
+ color: #503407;
+ background: var(--amber-100);
+ font-size: 0.86rem;
+ text-align: center;
+}
+
+.preview-banner[hidden] {
+ display: none;
+}
+
+.preview-banner strong {
+ margin-right: 0.35rem;
+}
+
+.preview-banner__icon {
+ color: var(--amber-700);
+}
+
+.site-header {
+ position: sticky;
+ z-index: 10;
+ top: 0;
+ border-bottom: 1px solid rgb(197 166 108 / 30%);
+ background: rgb(255 253 248 / 93%);
+ box-shadow: 0 3px 16px rgb(23 43 43 / 4%);
+ backdrop-filter: blur(16px);
+}
+
+.site-header__inner,
+.primary-nav__inner,
+.page-shell {
+ width: min(calc(100% - 2rem), var(--content));
+ margin-inline: auto;
+}
+
+.site-header__inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+ min-height: 78px;
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.8rem;
+ color: var(--ink-950);
+ text-decoration: none;
+}
+
+.brand__mark {
+ width: 44px;
+ height: 44px;
+ flex: 0 0 auto;
+ color: var(--teal-800);
+}
+
+.brand__name,
+.brand__tagline {
+ display: block;
+}
+
+.brand__name {
+ font-family: var(--font-display);
+ font-size: 1.22rem;
+ font-weight: 700;
+ letter-spacing: -0.015em;
+}
+
+.brand__tagline {
+ margin-top: 0.05rem;
+ color: var(--ink-650);
+ font-size: 0.72rem;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+}
+
+.header-status {
+ display: flex;
+ align-items: center;
+ gap: 0.55rem;
+ color: var(--ink-650);
+ font-size: 0.8rem;
+ font-weight: 650;
+}
+
+.status-dot {
+ width: 0.6rem;
+ height: 0.6rem;
+ border-radius: 50%;
+ background: var(--amber-700);
+ box-shadow: 0 0 0 4px var(--amber-100);
+}
+
+.header-status[data-state="ready"] .status-dot {
+ background: var(--teal-700);
+ box-shadow: 0 0 0 4px var(--teal-100);
+}
+
+.header-status[data-state="error"] .status-dot {
+ background: var(--red-700);
+ box-shadow: 0 0 0 4px var(--red-100);
+}
+
+.primary-nav {
+ border-top: 1px solid rgb(197 166 108 / 22%);
+}
+
+.primary-nav__inner {
+ display: flex;
+ gap: 0.2rem;
+ overflow-x: auto;
+ scrollbar-width: thin;
+}
+
+.primary-nav a {
+ position: relative;
+ flex: 0 0 auto;
+ padding: 0.9rem 1rem 0.82rem;
+ color: var(--ink-650);
+ font-size: 0.88rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.primary-nav a::after {
+ position: absolute;
+ right: 1rem;
+ bottom: 0;
+ left: 1rem;
+ height: 3px;
+ border-radius: 3px 3px 0 0;
+ background: transparent;
+ content: "";
+}
+
+.primary-nav a:hover {
+ color: var(--teal-800);
+ background: rgb(220 239 238 / 40%);
+}
+
+.primary-nav a[aria-current="page"] {
+ color: var(--teal-800);
+}
+
+.primary-nav a[aria-current="page"]::after {
+ background: var(--teal-700);
+}
+
+main {
+ min-height: 65vh;
+}
+
+.view {
+ padding: clamp(2.5rem, 6vw, 5rem) 0 5rem;
+}
+
+.view[hidden] {
+ display: none;
+}
+
+.page-shell--narrow {
+ max-width: 940px;
+}
+
+.page-heading {
+ margin-bottom: clamp(2rem, 5vw, 3.4rem);
+}
+
+.page-heading--split {
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ gap: 3rem;
+}
+
+.eyebrow {
+ margin-bottom: 0.45rem;
+ color: var(--teal-800);
+ font-size: 0.73rem;
+ font-weight: 800;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.lede {
+ max-width: 70ch;
+ margin-bottom: 0;
+ color: var(--ink-650);
+ font-size: clamp(1.02rem, 2vw, 1.17rem);
+}
+
+.data-stamp {
+ min-width: 190px;
+ padding: 1rem 1.1rem;
+ border: 1px solid var(--sand-200);
+ border-radius: var(--radius-md);
+ background: rgb(255 255 255 / 70%);
+ box-shadow: var(--shadow-sm);
+}
+
+.data-stamp span,
+.data-stamp strong,
+.data-stamp small {
+ display: block;
+}
+
+.data-stamp span,
+.data-stamp small {
+ color: var(--ink-650);
+ font-size: 0.76rem;
+}
+
+.data-stamp strong {
+ margin: 0.22rem 0;
+ font-size: 1.04rem;
+}
+
+.unavailable-state {
+ display: flex;
+ align-items: flex-start;
+ gap: 1rem;
+ margin: 0 0 2rem;
+ padding: 1.25rem;
+ border: 1px solid #e7b7b1;
+ border-radius: var(--radius-md);
+ background: var(--red-100);
+}
+
+.unavailable-state[hidden] {
+ display: none;
+}
+
+.unavailable-state h2,
+.unavailable-state p {
+ margin-bottom: 0.25rem;
+}
+
+.unavailable-state__icon {
+ display: grid;
+ width: 2rem;
+ height: 2rem;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 50%;
+ color: var(--white);
+ background: var(--red-700);
+ font-weight: 900;
+}
+
+.kpi-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.kpi-card {
+ position: relative;
+ min-height: 155px;
+ padding: 1.25rem;
+ overflow: hidden;
+ border: 1px solid var(--sand-200);
+ border-radius: var(--radius-md);
+ background: rgb(255 255 255 / 80%);
+ box-shadow: var(--shadow-sm);
+}
+
+.kpi-card::before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 4px;
+ background: var(--sand-400);
+ content: "";
+}
+
+.kpi-card--pass::before {
+ background: var(--teal-700);
+}
+
+.kpi-card--nonpass::before {
+ background: var(--red-700);
+}
+
+.kpi-card__label,
+.kpi-card strong,
+.kpi-card small {
+ display: block;
+}
+
+.kpi-card__label {
+ color: var(--ink-650);
+ font-size: 0.8rem;
+ font-weight: 750;
+}
+
+.kpi-card strong {
+ margin: 0.45rem 0 0.25rem;
+ font-family: var(--font-display);
+ font-size: clamp(2rem, 4vw, 2.85rem);
+ font-weight: 500;
+ line-height: 1;
+}
+
+.kpi-card small {
+ color: var(--ink-500);
+ font-size: 0.73rem;
+}
+
+.dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+ margin: 1rem 0;
+}
+
+.dashboard-grid--wide {
+ grid-template-columns: minmax(0, 1.45fr) minmax(290px, 0.75fr);
+}
+
+.panel {
+ min-width: 0;
+ padding: clamp(1.15rem, 2.5vw, 1.55rem);
+ border: 1px solid var(--sand-200);
+ border-radius: var(--radius-lg);
+ background: rgb(255 255 255 / 86%);
+ box-shadow: var(--shadow-sm);
+}
+
+.panel--wide {
+ grid-column: 1 / -1;
+}
+
+.panel__header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.panel__header h2,
+.panel__header p {
+ margin-bottom: 0;
+}
+
+.chart-legend {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 0.5rem 0.9rem;
+ color: var(--ink-650);
+ font-size: 0.7rem;
+}
+
+.chart-legend span {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.legend-swatch {
+ width: 0.62rem;
+ height: 0.62rem;
+ border-radius: 0.18rem;
+ background: var(--gray-300);
+}
+
+.legend-swatch--pass { background: var(--teal-700); }
+.legend-swatch--fail { background: var(--red-700); }
+.legend-swatch--reject { background: var(--amber-700); }
+.legend-swatch--abort { background: var(--purple-700); }
+.legend-swatch--missing { background: var(--gray-300); }
+
+.chart {
+ min-height: 260px;
+}
+
+.chart--large {
+ min-height: 330px;
+}
+
+.chart--scorecard {
+ min-height: 360px;
+}
+
+.chart svg,
+.utah-map svg {
+ display: block;
+ width: 100%;
+ height: auto;
+ overflow: visible;
+}
+
+.chart .axis-line,
+.chart .grid-line {
+ stroke: var(--gray-300);
+ vector-effect: non-scaling-stroke;
+}
+
+.chart .grid-line {
+ stroke-dasharray: 3 4;
+ opacity: 0.7;
+}
+
+.chart .axis-label {
+ fill: var(--ink-650);
+ font-family: var(--font-sans);
+ font-size: 11px;
+}
+
+.chart .chart-line {
+ fill: none;
+ stroke: var(--red-700);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 3;
+ vector-effect: non-scaling-stroke;
+}
+
+.chart .chart-dot {
+ fill: var(--paper);
+ stroke: var(--red-700);
+ stroke-width: 2.5;
+ vector-effect: non-scaling-stroke;
+}
+
+.chart .confidence-line {
+ stroke: var(--ink-500);
+ stroke-width: 1.5;
+ vector-effect: non-scaling-stroke;
+}
+
+.chart .reference-line {
+ stroke: var(--ink-500);
+ stroke-dasharray: 6 4;
+ vector-effect: non-scaling-stroke;
+}
+
+.chart-note {
+ margin: 0.7rem 0 0;
+ color: var(--ink-650);
+ font-size: 0.75rem;
+}
+
+.chart-empty {
+ display: grid;
+ min-height: 240px;
+ place-items: center;
+ border: 1px dashed var(--gray-300);
+ border-radius: var(--radius-md);
+ color: var(--ink-650);
+ background: var(--gray-100);
+ text-align: center;
+}
+
+.utah-map {
+ max-width: 250px;
+ margin: 0 auto 1rem;
+}
+
+.county-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+}
+
+.county-chip {
+ padding: 0.28rem 0.5rem;
+ border: 1px solid var(--gray-300);
+ border-radius: 999px;
+ color: var(--ink-500);
+ background: var(--gray-100);
+ font-size: 0.65rem;
+}
+
+.county-chip[data-covered="true"] {
+ border-color: #9bc9c6;
+ color: var(--teal-800);
+ background: var(--teal-100);
+ font-weight: 700;
+}
+
+.callout {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.8rem;
+ margin-top: 1rem;
+ padding: 1rem 1.15rem;
+ border-left: 4px solid var(--teal-700);
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
+ background: var(--teal-100);
+}
+
+.callout p {
+ margin: 0;
+ font-size: 0.86rem;
+}
+
+.callout__icon {
+ display: grid;
+ width: 1.4rem;
+ height: 1.4rem;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 50%;
+ color: var(--white);
+ background: var(--teal-800);
+ font-size: 0.75rem;
+ font-weight: 800;
+}
+
+.explorer-layout {
+ display: grid;
+ grid-template-columns: 250px minmax(0, 1fr);
+ gap: 1.25rem;
+ align-items: start;
+}
+
+.filter-panel {
+ position: sticky;
+ top: 155px;
+ padding: 1.2rem;
+ border: 1px solid var(--sand-200);
+ border-radius: var(--radius-lg);
+ background: var(--paper-warm);
+}
+
+.filter-panel__heading,
+.result-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.filter-panel__heading h2 {
+ margin: 0;
+}
+
+.text-button {
+ padding: 0.2rem;
+ border: 0;
+ color: var(--teal-800);
+ background: transparent;
+ font-size: 0.75rem;
+ font-weight: 750;
+ text-decoration: underline;
+ text-underline-offset: 0.18em;
+}
+
+.field {
+ display: grid;
+ gap: 0.35rem;
+ margin-top: 0.9rem;
+}
+
+.field > span,
+.segmented-control legend {
+ color: var(--ink-650);
+ font-size: 0.73rem;
+ font-weight: 750;
+}
+
+.field input,
+.field select {
+ width: 100%;
+ min-height: 42px;
+ padding: 0.58rem 0.68rem;
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius-sm);
+ color: var(--ink-950);
+ background: var(--white);
+ font-size: 0.83rem;
+}
+
+.field input:disabled,
+.field select:disabled,
+fieldset:disabled .field input,
+fieldset:disabled .field select {
+ color: var(--ink-500);
+ background: var(--gray-100);
+}
+
+.field--inline {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0;
+}
+
+.field--inline select {
+ width: auto;
+}
+
+.segmented-control {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.3rem;
+ margin: 1rem 0 0;
+ padding: 0;
+ border: 0;
+}
+
+.segmented-control legend {
+ grid-column: 1 / -1;
+ margin-bottom: 0.35rem;
+}
+
+.segmented-control label {
+ position: relative;
+}
+
+.segmented-control input {
+ position: absolute;
+ opacity: 0;
+}
+
+.segmented-control span {
+ display: block;
+ padding: 0.48rem 0.4rem;
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius-sm);
+ color: var(--ink-650);
+ background: var(--white);
+ font-size: 0.7rem;
+ font-weight: 750;
+ text-align: center;
+}
+
+.segmented-control input:checked + span {
+ border-color: var(--teal-700);
+ color: var(--teal-800);
+ background: var(--teal-100);
+}
+
+.segmented-control input:focus-visible + span {
+ outline: 3px solid var(--amber-700);
+ outline-offset: 2px;
+}
+
+.result-toolbar {
+ min-height: 48px;
+ margin-bottom: 0.75rem;
+}
+
+.result-toolbar p {
+ margin: 0;
+ color: var(--ink-650);
+ font-size: 0.83rem;
+}
+
+.cohort-cards {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.75rem;
+ margin-top: 0.75rem;
+}
+
+.cohort-card {
+ padding: 1rem;
+ border: 1px solid var(--sand-200);
+ border-radius: var(--radius-md);
+ background: var(--white);
+}
+
+.cohort-card h3 {
+ margin-bottom: 0.2rem;
+ font-size: 0.95rem;
+}
+
+.cohort-card__meta {
+ color: var(--ink-500);
+ font-size: 0.7rem;
+}
+
+.risk-bar {
+ height: 0.52rem;
+ margin: 0.85rem 0 0.45rem;
+ overflow: hidden;
+ border-radius: 999px;
+ background: var(--gray-100);
+}
+
+.risk-bar span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: var(--red-700);
+}
+
+.cohort-card__value {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.cohort-card__value strong {
+ font-family: var(--font-display);
+ font-size: 1.5rem;
+ font-weight: 500;
+}
+
+.cohort-card__value small {
+ color: var(--ink-500);
+}
+
+.empty-results {
+ padding: 2.5rem 1rem;
+ color: var(--ink-650);
+ text-align: center;
+}
+
+.locked-panel {
+ display: flex;
+ gap: 1.2rem;
+ margin-bottom: 1.25rem;
+ padding: clamp(1.25rem, 3vw, 1.8rem);
+ border: 1px solid var(--sand-400);
+ border-radius: var(--radius-lg);
+ background: var(--amber-100);
+}
+
+.locked-panel p:last-child {
+ margin-bottom: 0;
+}
+
+.locked-panel__icon {
+ display: grid;
+ width: 3rem;
+ height: 3rem;
+ flex: 0 0 auto;
+ place-items: center;
+ border: 2px solid var(--amber-700);
+ border-radius: 50%;
+ color: var(--amber-700);
+ font-size: 1.4rem;
+}
+
+.estimator-form {
+ padding: clamp(1.2rem, 3vw, 1.7rem);
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius-lg);
+ background: var(--white);
+ opacity: 0.72;
+}
+
+.estimator-form fieldset {
+ padding: 0;
+ border: 0;
+}
+
+.estimator-form legend {
+ margin-bottom: 0.2rem;
+ font-size: 1.05rem;
+ font-weight: 750;
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0 1rem;
+}
+
+.button {
+ margin-top: 1.2rem;
+ padding: 0.72rem 1rem;
+ border: 0;
+ border-radius: var(--radius-sm);
+ color: var(--white);
+ background: var(--teal-800);
+ font-weight: 750;
+}
+
+.form-note {
+ margin: 0.65rem 0 2rem;
+ color: var(--ink-650);
+ font-size: 0.78rem;
+}
+
+.method-preview {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ align-items: start;
+ padding: 1.5rem;
+ border-radius: var(--radius-lg);
+ color: var(--white);
+ background: var(--ink-950);
+}
+
+.method-preview .eyebrow {
+ color: #9bd4d1;
+}
+
+.check-list,
+.limitation-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.check-list li,
+.limitation-list li {
+ position: relative;
+ padding: 0.48rem 0 0.48rem 1.5rem;
+}
+
+.check-list li::before,
+.limitation-list li::before {
+ position: absolute;
+ left: 0;
+ content: "✓";
+ color: #9bd4d1;
+ font-weight: 800;
+}
+
+.method-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.method-card {
+ padding: 1.25rem;
+ border: 1px solid var(--sand-200);
+ border-radius: var(--radius-md);
+ background: var(--paper-warm);
+}
+
+.method-card__number {
+ display: block;
+ margin-bottom: 1.2rem;
+ color: var(--sand-400);
+ font-family: var(--font-display);
+ font-size: 1.6rem;
+}
+
+.method-card p {
+ margin: 0;
+ color: var(--ink-650);
+ font-size: 0.83rem;
+}
+
+.heatmap {
+ min-height: 210px;
+ overflow-x: auto;
+}
+
+.heatmap-grid {
+ display: grid;
+ gap: 0.3rem;
+ min-width: 650px;
+}
+
+.heatmap-label {
+ align-self: center;
+ color: var(--ink-650);
+ font-size: 0.68rem;
+ font-weight: 700;
+}
+
+.heatmap-cell {
+ min-height: 30px;
+ border: 1px solid rgb(21 94 96 / 12%);
+ border-radius: 0.25rem;
+ background: var(--gray-100);
+}
+
+.split-section {
+ display: grid;
+ grid-template-columns: minmax(220px, 0.55fr) minmax(0, 1.45fr);
+ gap: 2rem;
+ align-items: center;
+ margin: 1rem 0;
+ padding: clamp(1.3rem, 3vw, 2rem);
+ border-radius: var(--radius-lg);
+ background: var(--paper-warm);
+}
+
+.split-section__copy p:last-child {
+ margin: 0;
+ color: var(--ink-650);
+ font-size: 0.84rem;
+}
+
+.timeline {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(95px, 1fr));
+ margin: 0;
+ padding: 0;
+ overflow-x: auto;
+ list-style: none;
+}
+
+.timeline li {
+ position: relative;
+ min-width: 95px;
+ padding: 1.2rem 0.5rem 0;
+ border-top: 3px solid var(--teal-700);
+}
+
+.timeline li::before {
+ position: absolute;
+ top: -7px;
+ left: 0;
+ width: 11px;
+ height: 11px;
+ border: 2px solid var(--paper-warm);
+ border-radius: 50%;
+ background: var(--teal-700);
+ box-shadow: 0 0 0 1px var(--teal-700);
+ content: "";
+}
+
+.timeline__holdout {
+ border-color: var(--purple-700) !important;
+}
+
+.timeline__holdout::before {
+ background: var(--purple-700) !important;
+ box-shadow: 0 0 0 1px var(--purple-700) !important;
+}
+
+.timeline span,
+.timeline strong,
+.timeline small {
+ display: block;
+}
+
+.timeline span {
+ color: var(--ink-500);
+ font-size: 0.65rem;
+}
+
+.timeline strong {
+ margin: 0.1rem 0;
+ font-size: 0.75rem;
+}
+
+.timeline small {
+ color: var(--ink-500);
+ font-size: 0.58rem;
+ line-height: 1.3;
+}
+
+.diagnostic-kpis {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.diagnostic-kpis > div {
+ padding: 0.8rem;
+ border-radius: var(--radius-sm);
+ background: var(--gray-100);
+}
+
+.diagnostic-kpis span,
+.diagnostic-kpis strong {
+ display: block;
+}
+
+.diagnostic-kpis span {
+ color: var(--ink-500);
+ font-size: 0.65rem;
+}
+
+.diagnostic-kpis strong {
+ margin-top: 0.2rem;
+ font-family: var(--font-display);
+ font-size: 1.45rem;
+ font-weight: 500;
+}
+
+.limitation-list li {
+ border-bottom: 1px solid var(--gray-100);
+ color: var(--ink-650);
+ font-size: 0.82rem;
+}
+
+.limitation-list li::before {
+ content: "—";
+ color: var(--amber-700);
+}
+
+.privacy-panel {
+ display: grid;
+ grid-template-columns: 0.7fr 1.3fr;
+ gap: 1.5rem 2.5rem;
+ align-items: center;
+ margin-top: 1rem;
+ padding: clamp(1.3rem, 3vw, 2rem);
+ border-radius: var(--radius-lg);
+ color: var(--white);
+ background: var(--ink-950);
+}
+
+.privacy-panel .eyebrow {
+ color: #9bd4d1;
+}
+
+.privacy-panel > p {
+ grid-column: 1 / -1;
+ margin: 0;
+ color: #cad7d5;
+ font-size: 0.78rem;
+}
+
+.privacy-flow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.6rem;
+ text-align: center;
+}
+
+.privacy-flow span {
+ padding: 0.7rem;
+ border: 1px solid #536c69;
+ border-radius: var(--radius-sm);
+ font-size: 0.75rem;
+}
+
+.privacy-flow small {
+ color: #b6c5c3;
+ font-size: 0.6rem;
+}
+
+.privacy-flow__private {
+ border-color: #c7867d !important;
+}
+
+.privacy-flow__public {
+ border-color: #72aaa6 !important;
+}
+
+.site-footer {
+ border-top: 1px solid var(--sand-200);
+ background: var(--paper-warm);
+}
+
+.site-footer__inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+ min-height: 125px;
+ color: var(--ink-650);
+ font-size: 0.76rem;
+}
+
+.site-footer strong {
+ color: var(--ink-950);
+ font-family: var(--font-display);
+ font-size: 1rem;
+}
+
+.site-footer p {
+ max-width: 62ch;
+ margin: 0.25rem 0 0;
+}
+
+.noscript {
+ position: fixed;
+ z-index: 100;
+ right: 1rem;
+ bottom: 1rem;
+ left: 1rem;
+ padding: 1rem;
+ border: 2px solid var(--red-700);
+ border-radius: var(--radius-sm);
+ background: var(--red-100);
+ text-align: center;
+}
+
+@media (max-width: 900px) {
+ .kpi-grid,
+ .method-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .dashboard-grid--wide,
+ .explorer-layout,
+ .split-section,
+ .privacy-panel {
+ grid-template-columns: 1fr;
+ }
+
+ .filter-panel {
+ position: static;
+ }
+
+ .cohort-cards {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .privacy-panel > p {
+ grid-column: auto;
+ }
+}
+
+@media (max-width: 660px) {
+ .site-header__inner {
+ min-height: 68px;
+ }
+
+ .header-status,
+ .brand__tagline {
+ display: none;
+ }
+
+ .brand__mark {
+ width: 37px;
+ height: 37px;
+ }
+
+ .brand__name {
+ font-size: 1.05rem;
+ }
+
+ .primary-nav a {
+ padding-inline: 0.75rem;
+ }
+
+ .page-heading--split {
+ display: block;
+ }
+
+ .data-stamp {
+ width: 100%;
+ margin-top: 1.3rem;
+ }
+
+ .kpi-grid,
+ .dashboard-grid,
+ .method-grid,
+ .cohort-cards,
+ .form-grid,
+ .method-preview {
+ grid-template-columns: 1fr;
+ }
+
+ .kpi-card {
+ min-height: 130px;
+ }
+
+ .panel__header,
+ .result-toolbar,
+ .site-footer__inner {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .chart-legend {
+ justify-content: flex-start;
+ }
+
+ .locked-panel {
+ display: block;
+ }
+
+ .locked-panel__icon {
+ margin-bottom: 1rem;
+ }
+
+ .privacy-flow {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .diagnostic-kpis {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }
+}
+
+@media (forced-colors: active) {
+ .status-dot,
+ .legend-swatch,
+ .risk-bar span,
+ .timeline li::before {
+ forced-color-adjust: none;
+ }
+}
diff --git a/dashboard/tests/contract.test.mjs b/dashboard/tests/contract.test.mjs
new file mode 100644
index 0000000..6c0f096
--- /dev/null
+++ b/dashboard/tests/contract.test.mjs
@@ -0,0 +1,196 @@
+import assert from "node:assert/strict";
+import { createHash, webcrypto } from "node:crypto";
+import { readFileSync, readdirSync, statSync } from "node:fs";
+import path from "node:path";
+import test from "node:test";
+import { fileURLToPath } from "node:url";
+
+import {
+ DataContractError,
+ REQUIRED_ASSETS,
+ SCHEMA_VERSION,
+ loadDashboardData,
+ validateAssetSet,
+} from "../js/data.js";
+
+const DASHBOARD_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const PUBLIC_DATA = path.join(DASHBOARD_ROOT, "public", "data");
+
+function json(filename) {
+ return JSON.parse(readFileSync(path.join(PUBLIC_DATA, filename), "utf8"));
+}
+
+function assetSet() {
+ return Object.fromEntries(
+ Object.entries(REQUIRED_ASSETS).map(([name, filename]) => [name, json(filename)]),
+ );
+}
+
+function publicDataFetch(overrides = new Map()) {
+ return async (requestUrl) => {
+ const filename = path.basename(new URL(String(requestUrl)).pathname);
+ const source = path.join(PUBLIC_DATA, filename);
+ let bytes;
+ try {
+ bytes = overrides.has(filename) ? overrides.get(filename) : readFileSync(source);
+ } catch {
+ return { ok: false, async arrayBuffer() { return new ArrayBuffer(0); } };
+ }
+ return {
+ ok: true,
+ async arrayBuffer() {
+ return Uint8Array.from(bytes).buffer;
+ },
+ };
+ };
+}
+
+function filesRecursively(directory) {
+ return readdirSync(directory).flatMap((name) => {
+ const target = path.join(directory, name);
+ return statSync(target).isDirectory() ? filesRecursively(target) : [target];
+ });
+}
+
+test("static shell exposes four semantic navigable views", () => {
+ const html = readFileSync(path.join(DASHBOARD_ROOT, "index.html"), "utf8");
+ assert.match(html, /]*aria-label="Dashboard views"/);
+ assert.match(html, /One-time holdout<\/strong>Already evaluated<\/small>/);
+ assert.doesNotMatch(html, /Locked test<\/strong>|Final evaluation<\/small>/);
+});
+
+test("estimator remains disabled and contains no identifying input", () => {
+ const html = readFileSync(path.join(DASHBOARD_ROOT, "index.html"), "utf8");
+ const estimator = html.match(//)?.[0];
+ assert.ok(estimator, "estimator section is present");
+ assert.match(estimator, //);
+ const controlAttributes = [...estimator.matchAll(/<(?:input|select|textarea)\b([^>]*)>/g)].map(
+ (match) => match[1],
+ );
+ for (const attributes of controlAttributes) {
+ assert.doesNotMatch(
+ attributes,
+ /(?:name|id)\s*=\s*["'][^"']*(?:vin|plate|address|station|free.?text|zip)[^"']*["']/i,
+ );
+ }
+});
+
+test("all required generated assets satisfy the browser contract", () => {
+ const validated = validateAssetSet(assetSet());
+ assert.equal(validated.manifest.schema_version, SCHEMA_VERSION);
+ assert.equal(validated.manifest.development_preview, true);
+ assert.equal(validated.manifest.population_estimate_allowed, false);
+ assert.equal(validated.manifest.definitions.locked_test_metrics_published, false);
+ assert.match(validated.manifest.release_id, /^[0-9a-f]{64}$/);
+ assert.ok(validated.manifest.model_versions.length > 0);
+ assert.deepEqual(
+ validated.filters.partitions,
+ ["train", "tune", "calibrate"],
+ );
+ assert.ok(validated.overview.rows.length > 0);
+});
+
+test("browser loader verifies every raw asset digest before rendering", async () => {
+ const validated = await loadDashboardData({
+ basePath: "http://dashboard.test/public/data/",
+ fetchImplementation: publicDataFetch(),
+ cryptoImplementation: webcrypto,
+ });
+ assert.equal(validated.manifest.schema_version, SCHEMA_VERSION);
+
+ const changedOverview = Buffer.concat([
+ readFileSync(path.join(PUBLIC_DATA, REQUIRED_ASSETS.overview)),
+ Buffer.from("\n"),
+ ]);
+ await assert.rejects(
+ loadDashboardData({
+ basePath: "http://dashboard.test/public/data/",
+ fetchImplementation: publicDataFetch(
+ new Map([[REQUIRED_ASSETS.overview, changedOverview]]),
+ ),
+ cryptoImplementation: webcrypto,
+ }),
+ (error) =>
+ error instanceof DataContractError &&
+ /Integrity verification failed for overview_period_county\.json/.test(error.message),
+ );
+});
+
+test("sha256 manifest covers and matches every approved data asset", () => {
+ const manifest = json(REQUIRED_ASSETS.shaManifest);
+ const expectedNames = Object.entries(REQUIRED_ASSETS)
+ .filter(([name]) => name !== "shaManifest")
+ .map(([, filename]) => filename)
+ .sort();
+ assert.deepEqual(
+ manifest.files.map((file) => file.name).sort(),
+ expectedNames,
+ );
+ for (const entry of manifest.files) {
+ const digest = createHash("sha256")
+ .update(readFileSync(path.join(PUBLIC_DATA, entry.name)))
+ .digest("hex");
+ assert.equal(digest, entry.sha256, `${entry.name} digest`);
+ }
+});
+
+test("contract fails closed for missing, inconsistent, or sensitive data", () => {
+ const missing = assetSet();
+ delete missing.overview;
+ assert.throws(() => validateAssetSet(missing), DataContractError);
+
+ const inconsistent = assetSet();
+ inconsistent.ageRisk.population_estimate_allowed = true;
+ assert.throws(() => validateAssetSet(inconsistent), DataContractError);
+
+ const sensitive = assetSet();
+ sensitive.scorecards.rows[0].vehicle_token = "not-public";
+ assert.throws(() => validateAssetSet(sensitive), DataContractError);
+
+ const extraRowField = assetSet();
+ extraRowField.overview.rows[0].note = "unapproved";
+ assert.throws(() => validateAssetSet(extraRowField), DataContractError);
+
+ const lockedMetrics = assetSet();
+ lockedMetrics.manifest.definitions.locked_test_metrics_published = true;
+ assert.throws(() => validateAssetSet(lockedMetrics), DataContractError);
+
+ const extraPartition = assetSet();
+ extraPartition.filters.partitions.push("locked_test");
+ assert.throws(() => validateAssetSet(extraPartition), DataContractError);
+
+ const unsortedVersions = assetSet();
+ unsortedVersions.manifest.model_versions = ["z_v1", "a_v1"];
+ assert.throws(() => validateAssetSet(unsortedVersions), DataContractError);
+
+ const belowSuppression = assetSet();
+ belowSuppression.overview.rows[0].support_rounded =
+ belowSuppression.manifest.definitions.suppression_min_support - 1;
+ assert.throws(() => validateAssetSet(belowSuppression), DataContractError);
+
+ const incorrectlyRounded = assetSet();
+ incorrectlyRounded.coverage.rows[0].support_rounded += 1;
+ assert.throws(() => validateAssetSet(incorrectlyRounded), DataContractError);
+});
+
+test("dashboard source has no external runtime dependency or credential marker", () => {
+ const sourceFiles = filesRecursively(DASHBOARD_ROOT).filter(
+ (filename) =>
+ !filename.includes(`${path.sep}public${path.sep}data${path.sep}`) &&
+ !filename.includes(`${path.sep}tests${path.sep}`) &&
+ /\.(?:html|css|js|mjs)$/.test(filename),
+ );
+ for (const filename of sourceFiles) {
+ const source = readFileSync(filename, "utf8");
+ assert.doesNotMatch(source, /(?:postgres(?:ql)?:\/\/|PGPASSWORD|PGHOST|countydata\.)/i);
+ assert.doesNotMatch(source, /