Files
stack/dev/scripts/dossier_page/template.html
kert a3d9761a55
Some checks failed
CI / lint (push) Successful in 1m39s
CI / notebooks-smoke (push) Successful in 2m8s
Deploy / changes (push) Successful in 14s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / bcda (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
CI / test (push) Has been cancelled
Deploy / report (push) Has been cancelled
fix(dossier): lex shows the held-out stance measurement (dockets list) and current commenter coverage
2026-11-12 20:26:25 -05:00

1333 lines
114 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>lex · Home or Residence Services</title>
<!-- fhirworx favicon set (assets/icons), inlined by build.py -->
<link rel="icon" type="image/svg+xml" href="/*__FAVICON_SVG__*/">
<link rel="icon" type="image/png" sizes="32x32" href="/*__FAVICON_PNG__*/">
<link rel="apple-touch-icon" sizes="180x180" href="/*__APPLE_TOUCH_ICON__*/">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script>
/* Light is the default, as on the dashboard and the Gitea fhirworx theme; dark is opt-in (the
Gitea fhirworx-dark theme is a user choice too), remembered per browser. */
try { if (localStorage.getItem("lex-theme") === "dark") document.documentElement.setAttribute("data-theme", "dark"); } catch (e) {}
</script>
<!-- HTI-5 fhirworx design system: assets/css/fhirworx.css, inlined by build.py (palette, fonts, base classes) -->
<style id="fhirworx">/*__FHIRWORX_CSS__*/</style>
<!-- the fhirworx dark palette (infra/gitea/theme/theme-fhirworx-dark.css) on the same token names, by build.py -->
<style id="fhirworx-dark">/*__FHIRWORX_DARK__*/</style>
<style>
/* lex on the fhirworx design system. Every colour, font and radius below is a fhirworx.css token
(or a mix of two). Patterns come from the system's own pages:
masthead, section header, tile + icon plate + health box + badge + stripe, footer: assets/css/dashboard.css
navbar, tables, buttons, inputs, links, scrollbars: assets/css/inject.css, Gitea theme
in-page sidebar: assets/css/marimo.css (.app-sidebar)
Layout: the masthead, then a three-pane workspace: the book in the left rail, the selected
object with one tab per view, and the evidence drawer. */
:root {
color-scheme: light only; /* as the Gitea fhirworx theme: an OS dark setting does not flip it */
/* Roles, each one a system token. */
--s-final: var(--chart-2); /* chart: final values (indigo) */
--s-prop: var(--chart-5); /* chart: proposed values (amber) */
--s-ruc: var(--muted-foreground); /* chart: neutral reference marks */
--s-alt: var(--chart-1); /* chart: second series (indigo, lighter step) */
--st-support: var(--chart-4); /* stance: teal */
--st-modify: var(--chart-5); /* stance: amber */
--st-oppose: var(--destructive);
--st-unclear: var(--muted-foreground);
--on-amber: var(--foreground); /* Gitea theme rule: near-black, not white, on orange */
--nav-rule: var(--foreground); /* the 3px ink line under the navbar */
}
:root[data-theme="dark"] {
color-scheme: dark;
--s-final: var(--chart-1); /* the system has no dark indigo scale; chart-2 is under 3:1 on the dark card */
--s-alt: var(--muted-foreground);
--on-amber: var(--primary-foreground);
--nav-rule: var(--border); /* Gitea fhirworx-dark draws the navbar rule in the border colour */
}
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; overflow-x: hidden; }
a { color: var(--primary); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; text-decoration-color: color-mix(in srgb, currentColor 35%, transparent); transition: color .15s ease; }
a:hover { color: var(--foreground); text-decoration-color: currentColor; }
::selection { background: var(--primary); color: var(--primary-foreground); }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--secondary); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; border: 2px solid var(--secondary); }
::-webkit-scrollbar-thumb:hover { background: var(--muted-foreground); }
h1, h2, h3 { text-wrap: balance; }
.mono { font-family: var(--font-mono); }
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12.5px; }
.muted { color: var(--muted-foreground); }
.nowrap { white-space: nowrap; }
.small { font-size: 13px; }
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
/* ── navbar: the Gitea fhirworx bar (sidebar navy, 3px ink rule, logo first) ── */
.nav { background: var(--sidebar); color: var(--sidebar-foreground); border-bottom: 3px solid var(--nav-rule); }
.nav-in { max-width: 1480px; margin: 0 auto; padding: 6px 1.5rem; min-height: 54px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.brand-group { display: inline-flex; align-items: center; gap: 12px; margin-right: 10px; }
.brand { display: inline-flex; align-items: center; gap: 10px; font-family: var(--font-display); font-weight: 700; font-size: 1.2rem; letter-spacing: -.01em; color: var(--sidebar-accent-foreground); }
.brand svg { width: 30px; height: 30px; flex: none; }
.app { font-family: var(--font-display); font-weight: 700; font-size: 1.2rem; color: var(--sidebar-accent-foreground); padding-left: 12px; border-left: 1px solid color-mix(in srgb, var(--sidebar-foreground) 40%, transparent); }
.nav-item { font-family: var(--font-body); font-size: 15px; color: var(--sidebar-foreground); padding: 5px 10px; border-radius: var(--radius); display: inline-flex; align-items: center; gap: 6px; }
.nav-item.on { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
.nav a { text-decoration: none; }
.nav a:hover, .nav button:hover { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
.nav a.brand:hover, .nav a.app:hover { background: transparent; color: var(--sidebar-accent-foreground); }
.nav-sp { flex: 1; }
.nav-note { font-size: 14px; opacity: .85; }
.nav button.nav-item { background: transparent; border: 0; cursor: pointer; }
.nav-item svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.nav :focus-visible { outline-color: var(--sidebar-primary) !important; }
/* ── page + masthead: dashboard.css ── */
.page { max-width: 1480px; margin: 0 auto; padding: 3rem 1.5rem 2.5rem; }
.masthead { margin-bottom: .5rem; }
.masthead-row { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; }
.masthead-brand { display: flex; align-items: center; gap: 1rem; }
.masthead-logo { width: clamp(48px, 6vw, 80px); height: clamp(48px, 6vw, 80px); display: block; flex: 0 0 auto; }
.masthead h1 { font-weight: 800; font-size: clamp(2.5rem, 5vw, 3.5rem); line-height: 1.1; letter-spacing: -.02em; margin: 0; }
.masthead-eyebrow { font-family: var(--font-mono); font-size: .7rem; text-transform: uppercase; letter-spacing: .12em; color: var(--muted-foreground); align-self: flex-end; padding-bottom: .35rem; }
.masthead-body { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 0 2.5rem; }
.tagline { font-size: 1.125rem; color: var(--muted-foreground); max-width: 65ch; line-height: 1.65; margin: 0 0 1.5rem; text-wrap: pretty; }
.tagline em { color: var(--foreground); }
.masthead .health { margin: 0 0 1.5rem; max-width: 34rem; }
.masthead .health-v { font-size: 16px; }
.status-note { font-size: 14px; color: var(--muted-foreground); margin-top: 2px; }
/* health box: dashboard.css (label, status word, the left edge carries the state) */
.health { display: flex; flex-direction: column; margin: 0 0 .85rem; padding: 8px 10px; border: 1px solid var(--border); border-left-width: 4px; background: var(--background); }
.health-k { font-family: var(--font-mono); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted-foreground); }
.health-v { font-size: 18px; font-weight: 600; line-height: 1.2; color: var(--foreground); }
.health-ok { border-left-color: var(--chart-4); }
.health-warn { border-left-color: var(--chart-5); }
.health-bad { border-left-color: var(--destructive); }
.health-unknown { border-left-color: var(--muted-foreground); }
/* badge: dashboard.css */
.badge { font-family: var(--font-mono); color: var(--primary-foreground); background: var(--primary); font-size: .6rem; padding: 3px 7px; text-transform: uppercase; letter-spacing: .08em; white-space: nowrap; display: inline-block; line-height: 1.4; }
.badge.new { background: var(--chart-5); }
.tile > .badge { position: absolute; top: -1px; left: 1.5rem; }
.shell { display: grid; grid-template-columns: 236px minmax(0, 1fr) 330px; align-items: start; }
/* ── left rail: the book (an in-page sidebar, as marimo's .app-sidebar) ── */
.rail { position: sticky; top: 0; max-height: 100vh; overflow: auto; padding: 1rem 1.5rem 1.5rem 0; border-right: 1px solid var(--border); }
.rail label.eyebrow { display: block; margin-bottom: 6px; }
.rail input { width: 100%; }
.rail input, select.fsel { font: inherit; font-size: 15px; padding: 6px 10px; background: var(--card); color: var(--foreground); border: 1px solid var(--input); border-radius: var(--radius); }
select.fsel { font-size: 14px; padding: 4px 8px; }
.rail input:focus, select.fsel:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent); }
.rail h2 { font-size: 1.25rem; margin: 1.25rem 0 .4rem; }
.toc { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.toc li { display: flex; gap: 8px; align-items: baseline; justify-content: space-between; padding: 4px 8px; font-size: 15px; line-height: 1.4; color: var(--muted-foreground); }
.toc li.cur { background: var(--card); border-left: 3px solid var(--foreground); color: var(--foreground); }
.toc li.cur a { color: var(--foreground); font-weight: 600; text-decoration: none; }
.toc li.cur a:hover { color: var(--primary); }
.toc .tier { font-family: var(--font-mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted-foreground); white-space: nowrap; }
.toc li.planned span:first-child { color: var(--secondary-foreground); }
.rail .eyebrow + .toc, .rail .toc + .eyebrow { margin-top: 4px; }
.rail .note { font-size: 14px; color: var(--muted-foreground); margin-top: 14px; border-top: 1px solid var(--border); padding-top: 10px; }
.rail details summary { cursor: pointer; list-style: none; }
.rail details summary::-webkit-details-marker { display: none; }
.rail details:not([open]) summary h2::after { content: " +"; color: var(--muted-foreground); font-family: var(--font-mono); font-size: 14px; }
/* ── main pane ── */
.main { min-width: 0; padding: 1rem 1.5rem 4rem; }
.section-header { margin-bottom: 1.25rem; }
.section-row { display: flex; align-items: center; justify-content: space-between; gap: .5rem 1rem; flex-wrap: wrap; margin-bottom: .4rem; }
.section-eyebrow { font-family: var(--font-mono); font-size: .7rem; text-transform: uppercase; letter-spacing: .12em; color: var(--muted-foreground); display: block; }
.section-row .section-eyebrow { margin: 0; }
.section-header h2 { font-size: 1.75rem; font-weight: 700; line-height: 1.2; letter-spacing: -.01em; margin: 0; }
.section-sub { margin: .4rem 0 0; font-size: .95rem; color: var(--muted-foreground); line-height: 1.55; }
.tabs-wrap { overflow-x: auto; margin: 0 0 1.5rem; border-bottom: 1px solid var(--border); }
.tabs { display: flex; gap: 2px; flex-wrap: wrap; align-items: flex-end; }
.tabs .grp { font-family: var(--font-mono); font-size: .62rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted-foreground); opacity: .8; padding: 0 6px 11px 10px; }
.tabs a { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted-foreground); text-decoration: none; padding: 10px 10px 8px; border-bottom: 3px solid transparent; white-space: nowrap; }
.tabs a:hover { color: var(--foreground); background: var(--secondary); }
.tabs a.on { color: var(--foreground); border-bottom-color: var(--foreground); font-weight: 600; }
.tabs a.soon { border-bottom-style: dashed; }
.tabs a.soon::after { content: "coming"; margin-left: 6px; font-size: 9px; padding: 1px 4px; border: 1px dashed var(--border); color: var(--muted-foreground); vertical-align: 1px; }
.view h2 { font-size: 1.25rem; letter-spacing: -.01em; margin: 0 0 .25rem; }
.view h3 { font-size: 1.1rem; margin: 1.75rem 0 .5rem; }
.view .lede { color: var(--muted-foreground); max-width: 72ch; margin: 0 0 1rem; line-height: 1.6; }
.sec { margin-top: 1.75rem; }
.sec-head { margin: 1.75rem 0 1rem; }
.sec-head h3 { margin: 0 !important; }
/* ── tiles: dashboard.css (icon plate, eyebrow, title, description, health, port, stripe) ── */
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.25rem; }
.tile { background: var(--card); border: 1px solid var(--border); border-top: 3px solid var(--foreground); padding: 1.5rem; position: relative; overflow: hidden; transition: border-color .15s ease, background .15s ease; min-width: 0; display: flex; flex-direction: column; }
.tile > .icon, .tile > .tile-port { align-self: flex-start; }
.tile:hover { border-color: var(--primary); }
.tile[data-ev] { cursor: pointer; }
.tile .icon { width: 44px; height: 44px; flex: none; display: grid; place-items: center; border: 1px solid var(--border); color: var(--primary); margin-bottom: .9rem; transition: border-color .15s ease; }
.tile .icon svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.tile:hover .icon { border-color: var(--primary); }
.tile-eyebrow { font-family: var(--font-mono); font-size: .7rem; text-transform: uppercase; letter-spacing: .12em; color: var(--muted-foreground); margin-bottom: .25rem; }
.tile-title { font-family: var(--font-display); font-size: 1.25rem; font-weight: 700; line-height: 1.2; letter-spacing: -.01em; color: var(--foreground); margin: .25rem 0 .5rem; transition: color .15s ease; }
.tile-title a { color: inherit; text-decoration: none; }
.tile:hover .tile-title { color: var(--primary); }
.tile-desc { font-size: .95rem; color: var(--muted-foreground); line-height: 1.55; margin: 0 0 1rem; }
.tile-port { font-family: var(--font-mono); font-size: .65rem; color: var(--muted-foreground); opacity: .7; display: inline-block; }
.stripe { position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: var(--border); }
.tile .health { margin-top: auto; }
.facts { list-style: none; padding: 0; display: grid; gap: 7px; color: var(--secondary-foreground); }
.facts li { line-height: 1.5; }
.tile .codes { margin: .25rem 0 .5rem; }
.tile dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0 0 1rem; font-size: .9rem; color: var(--secondary-foreground); }
.tile dt { font-family: var(--font-mono); font-size: .65rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted-foreground); padding-top: 4px; }
.tile dd { margin: 0; min-width: 0; }
.cite { font-family: var(--font-mono); font-size: 11px; white-space: nowrap; text-decoration: none; color: var(--primary); border: 1px solid var(--border); background: var(--card); padding: 1px 5px; margin-left: 2px; line-height: 1.9; }
.cite::after { content: " \2197"; font-size: 10px; }
.cite:hover { border-color: var(--primary); color: var(--primary); }
.cite.book { color: var(--secondary-foreground); }
.cite.book::after { content: ""; }
.codes { display: flex; flex-wrap: wrap; gap: 4px; }
.code { font-family: var(--font-mono); font-size: 12px; padding: 1px 6px; border: 1px solid var(--border); background: var(--card); }
.code.del { text-decoration: line-through; color: var(--muted-foreground); border-style: dashed; }
.code.lnk { border-color: var(--primary); color: var(--primary); }
.hdr-grid { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px 16px; margin: 0 0 4px; align-items: baseline; }
.hdr-grid > .eyebrow { padding-top: 3px; }
/* panel: the health-box shape at section scale (left edge carries the state) */
.panel { border: 1px solid var(--border); border-left-width: 4px; background: var(--card); padding: 12px 16px; }
.panel.warn { border-left-color: var(--chart-5); }
.panel.info { border-left-color: var(--primary); }
.panel.soon { border-left-color: var(--muted-foreground); border-style: dashed; border-left-style: solid; }
.panel h3 { margin-top: 0 !important; }
.gaps { margin: 6px 0 0; padding-left: 18px; display: grid; gap: 5px; font-size: 15px; }
/* pills: inject.css badges/labels (mono, secondary ground, hairline); the state is in the border */
.pill { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; padding: 1px 6px; border: 1px solid var(--border); background: var(--secondary); color: var(--secondary-foreground); white-space: nowrap; display: inline-block; }
.pill.prelim { border-color: var(--chart-5); background: color-mix(in srgb, var(--chart-5) 12%, var(--card)); color: var(--foreground); }
.pill.cms { border-color: var(--primary); background: color-mix(in srgb, var(--primary) 8%, var(--card)); color: var(--foreground); }
.pill.prop { border-color: var(--s-prop); border-style: dashed; background: color-mix(in srgb, var(--s-prop) 10%, var(--card)); color: var(--foreground); }
.pill.final { border-color: var(--s-final); background: var(--card); color: var(--foreground); }
.pill.model { border-style: dotted; color: var(--muted-foreground); background: var(--card); }
.pill.soon { border-style: dashed; color: var(--muted-foreground); background: var(--card); }
/* ── tables: inject.css (navy mono header, hairline grid, zebra rows) ── */
.tbl-wrap { overflow-x: auto; background: var(--card); }
table.tbl { border-collapse: collapse; width: 100%; font-size: 13px; }
.tbl th { background: var(--primary); color: var(--primary-foreground); font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; text-align: left; padding: 10px 12px; border: 1px solid var(--sidebar-accent); white-space: nowrap; vertical-align: bottom; }
.tbl td { background: var(--card); color: var(--foreground); padding: 8px 12px; border: 1px solid var(--border); vertical-align: top; }
.tbl tbody tr:nth-child(even) td { background: var(--secondary); }
.tbl td.r, .tbl th.r { text-align: right; }
.tbl tbody tr[data-ev] { cursor: pointer; }
.tbl tbody tr[data-ev]:hover td { background: color-mix(in srgb, var(--primary) 8%, var(--background)); }
.tbl tr.sel td { background: color-mix(in srgb, var(--primary) 13%, var(--card)) !important; }
.tbl tr.sel td:first-child { box-shadow: inset 3px 0 0 var(--primary); }
.tbl .grp-row td { background: var(--muted); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--secondary-foreground); padding-top: 9px; border-top: 2px solid var(--foreground); }
.tbl td.pcol { border-left: 2px dashed var(--s-prop); font-style: italic; background-image: repeating-linear-gradient(-45deg, color-mix(in srgb, var(--s-prop) 16%, transparent) 0 1px, transparent 1px 6px); }
.tbl th.pcol { border-left: 2px dashed var(--s-prop); }
.tbl td.na { color: var(--muted-foreground); }
.chg { position: relative; }
.chg::before { content: ""; position: absolute; left: 3px; top: 50%; width: 5px; height: 5px; margin-top: -3px; background: var(--s-prop); border-radius: 50%; }
.tbl .stick { position: sticky; left: 0; z-index: 1; box-shadow: 1px 0 0 var(--border); }
/* stance: teal support, amber modify, red oppose, grey unclear (chart-4, chart-5, destructive, muted) */
.st { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 20px; font-family: var(--font-mono); font-size: 11px; font-weight: 600; color: var(--primary-foreground); }
.st.support { background: var(--st-support); }
.st.modify { background: var(--st-modify); color: var(--on-amber); }
.st.oppose { background: var(--st-oppose); }
.st.unclear { background: var(--st-unclear); }
.st.none { background: transparent; color: var(--border); font-weight: 400; }
.legend { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; font-size: 13px; color: var(--secondary-foreground); }
.legend span { display: inline-flex; gap: 6px; align-items: center; }
.sw { width: 12px; height: 12px; display: inline-block; }
.bar { display: flex; height: 22px; gap: 2px; min-width: 0; }
.bar > div { display: flex; align-items: center; justify-content: center; font-family: var(--font-mono); font-size: 11px; color: var(--primary-foreground); min-width: 18px; }
.bar > .st-modify { color: var(--on-amber); }
.two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.25rem; align-items: start; }
.col-h { display: flex; gap: 8px; align-items: center; margin: 0 0 8px; flex-wrap: wrap; }
.col-h h2, .col-h h3 { margin: 0 !important; }
/* buttons: inject.css (body face, secondary ground, primary on hover and when pressed) */
.filters { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 1rem; }
.fbtn, .drawer .close { font-family: var(--font-body); font-size: 14px; font-weight: 500; line-height: 1.4; padding: 3px 10px; border: 1px solid var(--border); background: var(--secondary); color: var(--foreground); border-radius: var(--radius); cursor: pointer; transition: all .15s ease; }
.fbtn:hover, .fbtn[aria-pressed="true"], .drawer .close:hover { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
.fbtn:active { transform: translate(1px, 1px); }
.fbtn:hover .muted, .fbtn[aria-pressed="true"] .muted { color: inherit; opacity: .75; }
button.dis { font-family: var(--font-body); font-size: 14px; padding: 3px 10px; border: 1px dashed var(--border); background: var(--muted); color: var(--muted-foreground); border-radius: var(--radius); cursor: not-allowed; }
.cms-list { display: grid; gap: 0; border: 1px solid var(--border); background: var(--card); }
.cms-list > div { padding: 10px 12px; border-bottom: 1px solid var(--border); cursor: pointer; display: grid; gap: 4px; }
.cms-list > div:last-child { border-bottom: 0; }
.cms-list > div:hover { background: var(--secondary); }
.cms-list > div.sel { background: color-mix(in srgb, var(--primary) 13%, var(--card)); box-shadow: inset 3px 0 0 var(--primary); }
.cms-list q { font-size: 14px; quotes: "\201C" "\201D"; }
.charts { display: grid; gap: 1.25rem; margin-top: 1.25rem; }
.charts .chart-box svg { max-width: 760px; }
/* timeline */
.tl-year { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 12px; border-top: 1px solid var(--border); padding: 12px 0 6px; }
.tl-year > h4 { font-size: 1.3rem; margin: 0; line-height: 1.1; }
.tl-year > h4 small { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); font-weight: 400; letter-spacing: .06em; margin-top: 4px; }
.tl-items { display: grid; gap: 8px; min-width: 0; }
.tl-item { border: 1px solid var(--border); background: var(--card); padding: 9px 13px; cursor: pointer; display: grid; gap: 4px; min-width: 0; transition: border-color .15s ease; }
.tl-item:hover { border-color: var(--primary); }
.tl-item.sel { border-color: var(--primary); box-shadow: inset 3px 0 0 var(--primary); }
.tl-item .top { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.tl-item .ttl { font-weight: 600; }
.tl-item .sum { font-size: 14px; color: var(--secondary-foreground); }
.type { font-family: var(--font-mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; padding: 1px 5px; border: 1px solid var(--border); color: var(--secondary-foreground); background: var(--secondary); white-space: nowrap; }
.type.origin { border-color: var(--s-alt); }
.type.valuation { border-color: var(--s-final); }
.type.cms { border-color: var(--primary); background: color-mix(in srgb, var(--primary) 8%, var(--card)); }
.type.link { border-color: var(--chart-4); }
/* story strip */
.steps { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0; border: 1px solid var(--border); border-top: 3px solid var(--foreground); background: var(--card); }
.steps li { padding: 12px 14px; border-right: 1px solid var(--border); min-width: 0; display: grid; gap: 6px; align-content: start; cursor: pointer; }
.steps li:last-child { border-right: 0; }
.steps li:hover { background: var(--secondary); }
.steps li.sel { box-shadow: inset 0 -3px 0 var(--primary); }
.steps .n { font-family: var(--font-mono); font-size: .7rem; text-transform: uppercase; color: var(--muted-foreground); letter-spacing: .12em; }
.steps .h { font-family: var(--font-display); font-weight: 700; font-size: 1.02rem; line-height: 1.25; }
.steps q { font-size: 14px; color: var(--secondary-foreground); quotes: "\201C" "\201D"; }
.chart { width: 100%; height: auto; display: block; }
.chart text { fill: var(--muted-foreground); font-family: var(--font-mono); font-size: 11px; }
.chart .ax { stroke: var(--muted-foreground); stroke-width: 1; }
.chart .grid { stroke: var(--border); stroke-width: 1; }
.chart-box { border: 1px solid var(--border); background: var(--card); padding: 1rem 1rem .5rem; min-width: 0; }
.tip { position: fixed; pointer-events: none; z-index: 30; background: var(--primary); color: var(--primary-foreground); font-family: var(--font-mono); font-size: 11.5px; padding: 5px 8px; max-width: 260px; }
/* evidence drawer */
.drawer { position: sticky; top: 0; max-height: 100vh; overflow: auto; padding: 1rem 0 2rem 1.5rem; border-left: 1px solid var(--border); min-width: 0; }
.drawer .d-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.drawer .close { display: none; }
.drawer h3 { font-size: 1.25rem; margin: 6px 0 10px; line-height: 1.25; }
.ev-src { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 12px; display: grid; gap: 6px; }
.ev-src .src-t { font-size: 13px; color: var(--muted-foreground); }
.ev-src blockquote { margin: 0; padding: 8px 12px; background: var(--card); border-left: 3px solid var(--primary); font-size: 14px; line-height: 1.55; }
.ev-src blockquote.cmt { border-left-color: var(--s-prop); }
.ev-dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 3px 10px; font-size: 14px; margin: 0; }
.ev-dl dt { font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted-foreground); padding-top: 3px; }
.ev-dl dd { margin: 0; min-width: 0; }
.sketch { border: 1px dashed var(--border); padding: 1.25rem 1rem 1rem; background: var(--card); position: relative; }
.sketch::before { content: "Sketch with real rows"; position: absolute; top: -9px; left: 10px; background: var(--background); padding: 0 6px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted-foreground); }
.ph { border: 1px dashed var(--border); color: var(--muted-foreground); font-style: italic; padding: 6px 8px; font-size: 14px; }
/* footer: dashboard.css */
.ftr { margin-top: 3rem; padding-top: 1rem; border-top: 3px solid var(--foreground); color: var(--muted-foreground); font-family: var(--font-mono); font-size: .65rem; letter-spacing: .08em; }
.ftr .site { color: var(--chart-5); }
.ftr a { color: inherit; }
@media (max-width: 1180px) {
.shell { grid-template-columns: 214px minmax(0, 1fr); }
.drawer { position: fixed; left: 0; right: 0; bottom: 0; top: auto; max-height: 52vh; z-index: 20; background: var(--background); border-left: 0; border-top: 3px solid var(--foreground); padding: 14px max(1.5rem, env(safe-area-inset-right, 0px)) calc(16px + env(safe-area-inset-bottom, 0px)) max(1.5rem, env(safe-area-inset-left, 0px)); box-shadow: 0 -8px 24px color-mix(in srgb, var(--foreground) 18%, transparent); }
.drawer:not(.open) { display: none; }
.drawer .close { display: inline-block; }
body.drawer-open .page { padding-bottom: 54vh; }
}
@media (max-width: 760px) {
.page { padding-top: 2rem; }
.shell { grid-template-columns: minmax(0, 1fr); }
.rail { position: static; max-height: none; border-right: 0; border-bottom: 1px solid var(--border); padding: 0 0 1rem; }
.main { padding: 1.25rem 0 3rem; }
.two { grid-template-columns: minmax(0, 1fr); }
.tabs { flex-wrap: nowrap; min-width: max-content; }
.nav-note { display: none; }
.nav-theme span { display: none; }
.tl-year { grid-template-columns: minmax(0, 1fr); gap: 6px; }
.steps { grid-template-columns: minmax(0, 1fr); }
.steps li { border-right: 0; border-bottom: 1px solid var(--border); }
.hdr-grid { grid-template-columns: minmax(0, 1fr); gap: 2px 0; }
.hdr-grid > .eyebrow { padding-top: 8px; }
}
@media (max-width: 480px) {
.nav-in { padding-inline: 1rem; }
.nav-explore { display: none; } /* the logo already links to the Gitea home */
}
</style>
</head>
<body>
<!-- Glyphs: Tabler Icons outline (MIT), the set the dashboard sprite uses; stroke and size come from the
.tile .icon and .nav-item rules. #fw-logo is the fhirworx logo (assets/icons/logo.svg), inlined by build.py. -->
<svg class="sprite" aria-hidden="true" focusable="false">
<!--__FW_LOGO__-->
<symbol id="i-home" viewBox="0 0 24 24"><path d="M5 12l-2 0l9 -9l9 9l-2 0" /><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" /><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" /></symbol>
<symbol id="i-stethoscope" viewBox="0 0 24 24"><path d="M6 4h-1a2 2 0 0 0 -2 2v3.5a5.5 5.5 0 0 0 11 0v-3.5a2 2 0 0 0 -2 -2h-1" /><path d="M8 15a6 6 0 1 0 12 0v-3" /><path d="M11 3v2" /><path d="M6 3v2" /><path d="M18 10a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /></symbol>
<symbol id="i-versions" viewBox="0 0 24 24"><path d="M10 7a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2l0 -10" /><path d="M7 7l0 10" /><path d="M4 8l0 8" /></symbol>
<symbol id="i-timeline" viewBox="0 0 24 24"><path d="M4 16l6 -7l5 5l5 -6" /><path d="M14 14a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M9 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M3 16a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M19 8a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></symbol>
<symbol id="i-scale" viewBox="0 0 24 24"><path d="M7 20l10 0" /><path d="M6 6l6 -1l6 1" /><path d="M12 3l0 17" /><path d="M9 12l-3 -6l-3 6a3 3 0 0 0 6 0" /><path d="M21 12l-3 -6l-3 6a3 3 0 0 0 6 0" /></symbol>
<symbol id="i-chart-dots" viewBox="0 0 24 24"><path d="M3 3v18h18" /><path d="M7 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M17 7a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M12 15a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M10.16 10.62l2.34 2.88" /><path d="M15.088 13.328l2.837 -4.586" /></symbol>
<symbol id="i-coin" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M14.8 9a2 2 0 0 0 -1.8 -1h-2a2 2 0 1 0 0 4h2a2 2 0 1 1 0 4h-2a2 2 0 0 1 -1.8 -1" /><path d="M12 7v10" /></symbol>
<symbol id="i-messages" viewBox="0 0 24 24"><path d="M21 14l-3 -3h-7a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1h9a1 1 0 0 1 1 1v10" /><path d="M14 15v2a1 1 0 0 1 -1 1h-7l-3 3v-10a1 1 0 0 1 1 -1h2" /></symbol>
<symbol id="i-books" viewBox="0 0 24 24"><path d="M5 5a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1l0 -14" /><path d="M9 5a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1l0 -14" /><path d="M5 8h4" /><path d="M9 16h4" /><path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041" /><path d="M14 9l4 -1" /><path d="M16 16l3.923 -.98" /></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454l0 .008" /></symbol>
<symbol id="i-sun" viewBox="0 0 24 24"><path d="M8 12a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></symbol>
</svg>
<header class="nav"><div class="nav-in">
<span class="brand-group"><a class="brand" href="https://git./*__DOMAIN__*//"><svg role="img" aria-label="fhirworx logo"><use href="#fw-logo"/></svg>fhirworx</a><a class="app" href="/" aria-current="page">lex</a></span>
<a class="nav-item nav-explore" href="https://git./*__DOMAIN__*//explore/repos">Explore</a>
<span class="nav-item on">Dossier</span>
<span class="nav-sp"></span>
<span class="nav-item nav-note">Policy evidence &middot; CMS payment and quality</span>
<button class="nav-item nav-theme" type="button" id="theme-btn" aria-pressed="false" title="Switch to the fhirworx dark palette"><svg aria-hidden="true"><use href="#i-moon"/></svg><span>Dark</span></button>
</div></header>
<div class="page">
<div class="masthead">
<div class="rule-line"></div>
<div class="masthead-row">
<div class="masthead-brand"><svg class="masthead-logo" role="img" aria-label="fhirworx logo"><use href="#fw-logo"/></svg><h1>lex</h1></div>
<span class="masthead-eyebrow">lex./*__DOMAIN__*/</span>
</div>
<div class="masthead-body">
<p class="tagline">Latin for <em>law</em>. A legal-epidemiology workbench for CMS payment and quality rules: for each code family it traces what the rules said, how they changed, who argued for and against each change, and what happened after&nbsp;— to inform predictions, policy work and comment letters.</p>
<div class="health health-warn" role="note"><span class="health-k">Preview</span><span class="health-v">Static snapshot of live data, built /*__BUILT__*/. Not the finished app.</span><span class="status-note">Every value below comes from a stored row; each carries its source.</span></div>
</div>
<div class="rule-line-thin"></div>
</div>
<div class="shell">
<aside class="rail" aria-label="The book">
<label class="eyebrow" for="q">Search the book</label>
<input id="q" type="search" placeholder="Family, code or name" autocomplete="off">
<details id="toc-d" open>
<summary><h2>The book</h2></summary>
<div class="eyebrow">Reviewed</div>
<ul class="toc" id="toc-rev"></ul>
<div class="eyebrow" style="margin-top:12px">Planned &middot; primary and home-based care</div>
<ul class="toc" id="toc-plan"></ul>
<p class="note" id="toc-note"></p>
</details>
</aside>
<main class="main" id="main">
<div id="mast"></div>
<div class="tabs-wrap"><nav class="tabs" id="tabs" aria-label="Views"></nav></div>
<section class="view" data-view="contents" hidden></section>
<section class="view" data-view="dossier" hidden></section>
<section class="view" data-view="timeline" hidden></section>
<section class="view" data-view="valuation" hidden></section>
<section class="view" data-view="payment" hidden></section>
<section class="view" data-view="politics" hidden></section>
<section class="view" data-view="sources" hidden></section>
<section class="view" data-view="grid" hidden></section>
<section class="view" data-view="evaluate" hidden></section>
<section class="view" data-view="predict" hidden></section>
<section class="view" data-view="draft" hidden></section>
<section class="view" data-view="review" hidden></section>
</main>
<aside class="drawer" id="drawer" aria-label="Evidence" aria-live="polite"></aside>
</div>
<footer class="ftr"><span class="site">lex./*__DOMAIN__*/</span> &mdash; legal-epidemiology workbench &mdash; snapshot /*__BUILT__*/ &mdash; part of <a href="https://dashboard./*__DOMAIN__*//">fhirworx</a></footer>
</div>
<div class="tip" id="tip" hidden></div>
<script id="data" type="application/json">/*__DATA__*/{}</script>
<script>
(function () {
"use strict";
const D = JSON.parse(document.getElementById("data").textContent);
const $ = (s, r) => (r || document).querySelector(s);
const $$ = (s, r) => Array.from((r || document).querySelectorAll(s));
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const money = (v) => (v == null ? "—" : "$" + v.toFixed(2));
const n2 = (v) => (v == null ? "—" : v.toFixed(2));
const int = (v) => (v == null ? "—" : Math.round(v).toLocaleString("en-US"));
const uniq = (a) => Array.from(new Set(a));
const M = D.meta;
const BASE = M.base, PRED = M.pred, LINKED = M.linked;
const HOST = "lex./*__DOMAIN__*/";
const icon = (id) => `<span class="icon" aria-hidden="true"><svg><use href="#${id}"/></svg></span>`;
const health = (k, v, state) => `<span class="health health-${state}"><span class="health-k">${esc(k)}</span><span class="health-v">${esc(v)}</span></span>`;
function compact(codes) {
const cs = uniq(codes).sort();
const out = [];
let i = 0;
while (i < cs.length) {
let j = i;
while (j + 1 < cs.length && /^\d+$/.test(cs[j]) && /^\d+$/.test(cs[j + 1]) && +cs[j + 1] === +cs[j] + 1) j++;
if (j - i >= 2) out.push(cs[i] + "–" + cs[j]); else out.push(...cs.slice(i, j + 1));
i = j + 1;
}
return out.join(", ");
}
const srcOf = (k) => D.sources[k] || null;
const ruleName = (k) => { const s = srcOf(k); return s ? (s.label || s.title) : k; };
function citeA(c) {
if (!c) return "";
return `<a class="cite" href="${esc(c.url)}" target="_blank" rel="noopener" title="${esc(ruleName(c.k))}">${esc(c.label)}</a>`;
}
function bookCite(b) {
if (!b) return "";
return `<span class="cite book" title="Licensed book on file; cited by edition and page">${esc(b.title)}${b.page ? ", p. " + esc(b.page) : ""}</span>`;
}
function rvuCite(r) {
if (!r) return "";
return `<a class="cite" href="${esc(r.url)}" target="_blank" rel="noopener" title="${esc(r.label)}">${esc(r.label.replace(" (RVU file)", ""))}</a>`;
}
const linkA = (url, text) => `<a href="${esc(url)}" target="_blank" rel="noopener">${esc(text)}</a>`;
/* ── evidence registry and drawer ─────────────────────────────── */
const EV = [];
const ev = (o) => { EV.push(o); return EV.length - 1; };
const drawer = $("#drawer");
function paraBlock(c) {
if (!c) return "";
const s = srcOf(c.k);
const t = c.p ? D.paras[c.k + ":" + c.p] : "";
const meta = s ? [s.label || s.title, s.fr ? "FR Doc " + s.fr.doc : "", s.date ? "published " + s.date : ""].filter(Boolean).join(" · ") : "";
return `<div class="ev-src"><div>${citeA(c)}</div><div class="src-t">${esc(meta)}</div>` +
(t ? `<blockquote>${esc(t)}</blockquote>` : `<div class="small muted">${c.p ? "" : "Printed-page link. This page carries a table, which has no paragraph anchors."}</div>`) + `</div>`;
}
function renderEvidence(e) {
let h = `<div class="d-head"><span class="eyebrow">Evidence${e.kind ? " · " + esc(e.kind) : ""}</span><button class="close" type="button" id="d-close">Close</button></div>`;
h += `<h3>${esc(e.title)}</h3>`;
if (e.badges) h += `<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">${e.badges}</div>`;
if (e.fields && e.fields.length) h += `<dl class="ev-dl">${e.fields.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join("")}</dl>`;
if (e.html) h += e.html;
(e.cites || []).filter(Boolean).forEach((c) => { h += paraBlock(c); });
(e.extra || []).forEach((x) => { h += `<div class="ev-src">${x}</div>`; });
return h;
}
let selEls = [];
function showEvidence(id, el) {
const e = EV[id];
if (!e) return;
drawer.innerHTML = renderEvidence(e);
drawer.classList.add("open");
document.body.classList.add("drawer-open");
selEls.forEach((x) => x.classList.remove("sel"));
selEls = el ? [el] : [];
selEls.forEach((x) => x.classList.add("sel"));
const b = $("#d-close");
if (b) b.addEventListener("click", () => { drawer.classList.remove("open"); document.body.classList.remove("drawer-open"); });
}
document.addEventListener("click", (evt) => {
if (evt.target.closest("a, button, select, input, label")) return;
const el = evt.target.closest("[data-ev]");
if (el) showEvidence(+el.dataset.ev, el);
});
document.addEventListener("keydown", (evt) => {
if (evt.key !== "Enter" && evt.key !== " ") return;
const el = evt.target.closest && evt.target.closest("[data-ev]");
if (el && evt.target === el) { evt.preventDefault(); showEvidence(+el.dataset.ev, el); }
});
const evAttr = (id) => `data-ev="${id}" tabindex="0"`;
/* tooltip for charts */
const tip = $("#tip");
document.addEventListener("pointermove", (evt) => {
const t = evt.target.closest && evt.target.closest("[data-tip]");
if (!t) { tip.hidden = true; return; }
tip.textContent = t.getAttribute("data-tip");
tip.hidden = false;
const x = Math.min(evt.clientX + 14, window.innerWidth - 270);
tip.style.left = x + "px";
tip.style.top = evt.clientY + 14 + "px";
});
/* ── derived facts ─────────────────────────────────────────────── */
const ruleSources = Object.values(D.sources).filter((s) => s.fr);
const latestRule = ruleSources.slice().sort((a, b) => (a.date < b.date ? 1 : -1))[0];
const earliestCY = Math.min(...D.summaries.map((s) => s.cy), ...D.events.map((e) => e.year), ...D.rounds.map((r) => r.cy));
const pay = (code, cy, prop) => D.payment.find((p) => p.code === code && p.cy === cy && p.proposed === !!prop);
const memberCite = (code) => (D.members.find((m) => m.code === code) || {}).cite;
const linkOf = (t) => D.links.find((l) => l.target === t);
const cfOf = (cy, prop) => D.cfs.find((c) => c.cy === cy && c.proposed === !!prop);
const rounds23 = D.rounds.filter((r) => r.cy === 2023);
const r23f = rounds23.filter((r) => r.kind === "final");
const r23p = rounds23.filter((r) => r.kind === "proposed");
const r08f = D.rounds.filter((r) => r.cy === 2008 && r.kind === "final");
const r08p = D.rounds.filter((r) => r.cy === 2008 && r.kind === "proposed");
const pctCount = (rows, p) => rows.filter((r) => r.pct === p).length;
const acceptN = (rows) => rows.filter((r) => r.fin != null && r.ruc != null && Math.abs(r.fin - r.ruc) < 1e-9).length;
const C = (k, p) => {
const all = [];
D.rounds.forEach((r) => { if (r.cite) all.push(r.cite); Object.values(r.cites || {}).forEach((a) => a.forEach((c) => c && all.push(c))); });
D.summaries.forEach((s) => { all.push(s.comment, s.response); });
D.links.forEach((l) => { all.push(l.cite, l.alias_cite); });
D.members.forEach((m) => all.push(m.cite));
D.events.forEach((e) => all.push(e.cite));
D.origins.forEach((o) => { all.push(o.cite, o.ref_cite); });
D.guidance.forEach((g) => all.push(g.src));
return all.find((c) => c && c.k === k && c.p === p) || null;
};
const C1676 = C("PETM225D", 1676), C1685 = C("PETM225D", 1685), C878 = C("MZ24MX5S", 878), C1672 = C("PETM225D", 1672);
const C1679 = C("PETM225D", 1679), C1681 = C("PETM225D", 1681), C2241 = C("54GTKR5G", 2241), C2242 = C("54GTKR5G", 2242);
const C877 = C("MZ24MX5S", 877);
const pageCite = (k, pg) => { const s = srcOf(k); return s && s.fr ? { k, p: 0, pg, label: `${s.fr.vol} FR ${pg}`, url: `${s.fr.html}#page-${pg}` } : null; };
const C08p = r08p.length ? r08p[0].cite : null;
const STANCES = ["support", "modify", "oppose", "unclear"];
const ST_LETTER = { support: "S", modify: "M", oppose: "O", unclear: "U" };
const EVENT_ORDER = ["valuation", "coding", "telehealth", "G0318", "other"];
const EVENT_LABEL = { valuation: "Work RVU valuation", coding: "Coding changes", telehealth: "Telehealth list", G0318: "G0318 prolonged add-on (new)", other: "Other aspects of the family's codes" };
const evSlug = (k) => k.split(":").slice(2).join(":");
const docketRow = D.reaction.find((r) => r.period === M.docket);
const stancesBy = {};
D.stances.forEach((s) => { (stancesBy[s.key] = stancesBy[s.key] || []).push(s); });
const withPos = Object.keys(stancesBy).length;
const named = D.commenters.filter((c) => c.name).length;
const TYPE_LABEL = { specialty_society: "Specialty society", health_system: "Health system", individual_clinician: "Individual clinician", other: "Other organization", patient_group: "Patient group", payer: "Payer", vendor: "Vendor" };
const titleCase = (s) => s ? s.toLowerCase().replace(/\b[a-z]/g, (m) => m.toUpperCase()) : "";
const ACTION = { finalize_as_proposed: "finalized as proposed", finalize_modified: "finalized with changes", not_finalized: "not finalized", acknowledged: "acknowledged", unclear: "response not classified" };
const quant = (q) => (q === "unspecified" ? "commenters (number not stated)" : q === "one" ? "one commenter" : q + " commenters");
const stancePill = (s) => s ? `<span class="pill">${esc(s)}</span>` : "";
const PLANNED = [
["Office/outpatient E/M", ""],
["G2211 and modifiers B1/B2", "G2211, B1, B2"],
["Chronic care management (CCM)", "99490, 99491, 99487–99489, 99437, 99439, G2058"],
["Principal care management (PCM)", "99424–99427, G2064, G2065"],
["Transitional care management (TCM)", "99495–99496"],
["Advanced primary care management (APCM)", "G0556–G0558"],
["Care plan oversight and home health certification", "G0179–G0182, 99339, 99340, 99374–99380"],
["Behavioral health integration and CoCM", "99484, 99492–99494"],
["Remote physiologic and therapeutic monitoring (RPM/RTM)", ""],
["Advance care planning (ACP)", "99497–99498"],
["Prolonged services", "99354–99357, 99417, 99418, G0316–G0318"],
["Virtual check-ins and communication-technology services", "G2010, G2012, G2252"],
];
/* ── rail ──────────────────────────────────────────────────────── */
function renderRail() {
$("#toc-rev").innerHTML = `<li class="cur" data-name="home visits home or residence services ${BASE.join(" ")} ${PRED.join(" ")} g0318"><a href="#dossier">Home or Residence Services</a><span class="tier">Built</span></li>`;
$("#toc-plan").innerHTML = PLANNED.map(([n, c]) => `<li class="planned" data-name="${esc((n + " " + c).toLowerCase())}"><span>${esc(n)}</span><span class="tier">Planned</span></li>`).join("");
$("#toc-note").innerHTML = `The machine-built tier covers everything else: ${M.derived_families.toLocaleString("en-US")} derived code families, ${M.derived_single.toLocaleString("en-US")} of them single codes, built unattended and labeled “machine-built, not reviewed”.`;
$("#q").addEventListener("input", (e) => {
const q = e.target.value.trim().toLowerCase();
$$(".toc li").forEach((li) => { li.hidden = !!q && !li.dataset.name.includes(q); });
});
if (window.matchMedia("(max-width: 760px)").matches) $("#toc-d").open = false;
}
/* ── masthead and tabs ─────────────────────────────────────────── */
const VIEWS = [
["book", [["contents", "Contents"]]],
["HOME-VISITS", [["dossier", "Dossier"], ["timeline", "Timeline"], ["valuation", "Valuation"], ["payment", "Payment"], ["politics", "Politics"], ["sources", "Sources"]]],
["coming", [["grid", "Grid"], ["evaluate", "Evaluate"], ["predict", "Predict"], ["draft", "Draft"], ["review", "Review"]]],
];
function renderTabs() {
$("#tabs").innerHTML = VIEWS.map(([g, vs]) => `<span class="grp">${esc(g)}</span>` + vs.map(([id, t]) => `<a href="#${id}" data-tab="${id}" class="${g === "coming" ? "soon" : ""}">${t}</a>`).join("")).join("");
}
function renderMast(view) {
if (view === "contents") {
$("#mast").innerHTML = `<div class="section-header"><span class="section-eyebrow">The book · CMS payment and quality regulation</span>
<h2>Contents</h2></div>`;
return;
}
$("#mast").innerHTML = `<div class="section-header">
<div class="section-row"><span class="section-eyebrow">${esc(M.family)} · code family dossier · phase 1</span><span class="badge">Reviewed dossier</span></div>
<h2>${esc(M.name)}</h2>
<p class="section-sub">Home or residence E/M visits ${compact(BASE)} · domiciliary codes deleted for CY2023 · linked add-on G0318</p></div>`;
}
/* ── #contents ─────────────────────────────────────────────────── */
function domains() {
const st = D.origins.find((o) => o.kind === "statute" && /1833\(x\)/.test(o.ref) && o.codes.includes("99350"));
const mssp = D.origins.find((o) => o.kind === "regulation" && /425\.400/.test(o.ref));
const tele = D.events.filter((e) => e.kind === "telehealth_list" && e.codes.some((c) => BASE.includes(c)));
const out = [
["Home-based care", "The family's curated name: " + M.name, null],
["Primary care", st ? `${st.ref} (${st.summaries[0] ? st.summaries[0].text : ""})` : "", st && st.cite],
["APM linkage", mssp ? `${mssp.ref}, Medicare Shared Savings Program` : "", mssp && mssp.cite],
["Telehealth", tele.length ? `Telehealth list events in CY${uniq(tele.map((t) => t.year)).join(", CY")}` : "", tele.length ? tele[tele.length - 1].cite : null],
];
return out.filter((d) => d[1]);
}
function renderContents() {
const dm = domains();
const lr = latestRule;
const sec = ["Timeline", "Valuation CY2008, CY2023", `Payment ${M.rvu_years[0]}–${M.rvu_years[1]}`, "Politics CY2023 docket", "Sources"];
const homeId = ev({ kind: "family", title: M.name, fields: [["Key", esc(M.family)], ["Tier", "Reviewed"], ["Codes", esc(compact(BASE))], ["Deleted CY2023", esc(compact(PRED))]], extra: dm.map((d) => `<div><b>${esc(d[0])}</b>: ${esc(d[1])} ${citeA(d[2])}</div>`) });
// one tile per family, as the dashboard's service tiles: icon plate, eyebrow, title, description, health, address, stripe
let h = `<p class="lede">One dossier per code family. Reviewed dossiers are hand-curated and pass the review queue; everything else is machine-built and labeled so.</p>
<div class="sec-head"><span class="section-eyebrow">Reviewed tier · hand-curated</span><h3>Built</h3></div>
<div class="tiles"><div class="tile" ${evAttr(homeId)}><span class="badge">Reviewed</span>${icon("i-home")}
<div class="tile-eyebrow">${esc(M.family)} · phase 1</div>
<div class="tile-title"><a href="#dossier">${esc(M.name)}</a></div>
<div class="codes">${BASE.map((c) => `<span class="code">${c}</span>`).join("")}</div>
<dl>
<dt>Value domains</dt><dd><div class="codes">${dm.map((d) => `<span class="pill" title="${esc(d[1])}">${esc(d[0])}</span>`).join("")}</div></dd>
<dt>Coverage</dt><dd>${sec.map(esc).join(" · ")}<div class="muted">gaps stated on the dossier</div></dd>
<dt>Last rule</dt><dd>${esc(lr.label)} ${lr.fr ? `<a class="cite" href="${esc(lr.fr.html)}" target="_blank" rel="noopener">${esc(lr.cite)}</a>` : ""} <span class="muted">${esc(lr.date)}</span></dd>
<dt>Comment period</dt><dd class="muted">Tracker comes in S6</dd>
</dl>
${health("status", "built · reviewed", "ok")}
<span class="tile-port">${esc(HOST)}/#dossier</span><div class="stripe"></div></div></div>
<div class="sec-head"><span class="section-eyebrow">Reviewed tier · primary care and home-based care</span><h3>Planned</h3></div>
<div class="tiles">`;
PLANNED.forEach(([n, c]) => {
h += `<div class="tile">${icon("i-stethoscope")}<div class="tile-eyebrow">Planned · phase 1</div><div class="tile-title">${esc(n)}</div>
<p class="tile-desc"><span class="mono small">${esc(c || "codes set at curation")}</span><br>Not built. Membership and value domains are set at curation.</p>
${health("status", "planned", "unknown")}<div class="stripe"></div></div>`;
});
h += `</div>
<div class="sec-head"><span class="section-eyebrow">Everything else</span><h3>Machine-built tier</h3></div>
<div class="tiles"><div class="tile">${icon("i-versions")}<div class="tile-eyebrow">Machine-built · not reviewed</div>
<div class="tile-title">${M.derived_families.toLocaleString("en-US")} derived code families</div>
<p class="tile-desc">${M.derived_single.toLocaleString("en-US")} of them single codes that fold into their CPT heading. Built in nightly batches and labeled “machine-built, not reviewed”, with gaps stated.</p>
${health("status", "nightly batches", "unknown")}<div class="stripe"></div></div></div>
<div class="filters" style="margin-top:1.25rem"><button class="dis" disabled>Propose a new family</button><span class="small muted">Goes to curation. Comes with S2.</span></div>`;
$('[data-view="contents"]').innerHTML = h;
}
/* ── #dossier ──────────────────────────────────────────────────── */
function factLi(text, cites) { return `<li>${text} ${(cites || []).filter(Boolean).map(citeA).join(" ")}</li>`; }
function renderDossier() {
const g0318 = linkOf("G0318"), g2211 = linkOf("G2211"), b1 = linkOf("B1"), b2 = linkOf("B2");
const st27 = BASE.map((c) => (pay(c, 2027, false) || {}).status);
const allA = st27.every((s) => s === "A");
const rf27 = pay(BASE[0], 2027, false);
const predFr = PRED.filter((c) => (D.members.find((m) => m.code === c) || {}).cite);
const predRvu = PRED.filter((c) => !(D.members.find((m) => m.code === c) || {}).cite);
const hdr = `
<div class="hdr-grid">
<span class="eyebrow">Active codes</span><div class="codes">${BASE.map((c) => `<span class="code">${c}</span>`).join("")} ${citeA(memberCite(BASE[0]))}</div>
<span class="eyebrow">Deleted for CY2023</span><div class="codes">${PRED.map((c) => `<span class="code del">${c}</span>`).join("")}</div>
<span></span><div class="small">${esc(compact(predFr))}: deleted by the CPT Editorial Panel, cited ${citeA(C1672)}. ${esc(compact(predRvu))}: last RVU file year 2022.</div>
<span class="eyebrow">Linked</span><div class="small" style="display:grid;gap:3px">
<div><span class="code lnk">G0318</span> prolonged add-on from CY${g0318.since}, proposed as ${esc(g0318.aliases.join(", "))} ${citeA(g0318.alias_cite)} finalized ${citeA(g0318.cite)}</div>
<div><span class="code lnk">G2211</span> add-on billable with the family from CY${g2211.since} ${citeA(g2211.cite)}</div>
<div><span class="code lnk">B1</span> <span class="code lnk">B2</span> modifiers from CY${b1.since} ${citeA(b1.cite)} ${citeA(b2.cite)}</div>
</div>
<span class="eyebrow">Status</span><div class="small">Payment status ${allA ? "A (active) for all 8 active codes" : esc(st27.join(", "))} in the CY2027 final RVU file ${rvuCite({ label: "PFS CY2027 Addendum B", url: rf27 ? rf27.url : "" })}</div>
<span class="eyebrow">Years</span><div class="small">Rules cited CY${earliestCY}–CY${latestRule.cy} · RVU files ${M.rvu_years[0]}–${M.rvu_years[1]} · utilization ${M.util_years[0]}–${M.util_years[1]}</div>
</div>`;
// section summaries, every line from rows
const tlCount = TL.length;
const statutes = uniq(D.origins.filter((o) => o.kind === "statute").map((o) => o.ref.replace(/ \(as added.*\)/, "").replace(/ of the Act$/, "")));
const regs = uniq(D.origins.filter((o) => o.kind === "regulation").map((o) => o.ref));
const statCite = uniq(D.origins.filter((o) => o.kind === "statute").map((o) => o.ref.replace(/ \(as added.*\)/, "").replace(/ of the Act$/, ""))).map((r) => (D.origins.find((o) => o.kind === "statute" && o.ref.startsWith(r)) || {}).cite);
const p15 = pay("99350", 2015, false), p27 = pay("99350", 2027, false), p27p = pay("99350", 2027, true);
const cf27 = cfOf(2027, false);
const cms23 = D.summaries.filter((s) => s.cy === 2023);
const nRules = ruleSources.length;
const cfrG = uniq(D.guidance.filter((g) => g.kind === "cfr" && !g.unresolved).map((g) => g.locator));
const iomG = uniq(D.guidance.filter((g) => g.kind === "iom").map((g) => g.locator));
const cyRounds = uniq(D.rounds.map((r) => r.cy)).sort((a, b) => a - b).map((y) => "CY" + y).join(" · ");
// [anchor, title, glyph, facts, [health label, value, state]]
const tiles = [
["timeline", "Timeline", "i-timeline", [
factLi(`${tlCount} dated entries from lineage, origin, valuation rounds, CMS's comment summaries and links, CY${earliestCY}–CY${latestRule.cy}.`),
factLi(`Latest: ${esc(latestRule.label)}, B1 and B2 modifiers finalized.`, [b1.cite]),
], ["entries", `${tlCount} · CY${earliestCY}–CY${latestRule.cy}`, "ok"]],
["timeline", "Origin", "i-scale", [
factLi(`CPT Editorial Panel, February 2021 meeting: domiciliary codes ${esc(compact(predFr.filter((c) => c !== "99343")))} and home code 99343 deleted, effective 2023.`, [C1672]),
factLi(`Statute cited: ${statutes.map(esc).join("; ")}.`, statCite),
factLi(`Regulation cited: ${regs.length} CFR sections (${regs.map(esc).join(", ")}).`),
], ["cited", `${statutes.length} statutes · ${regs.length} CFR sections`, "ok"]],
["valuation", "Valuation", "i-chart-dots", [
factLi(`CY2023: the RUC recommended the survey 25th percentile for ${pctCount(r23f, "25th")} codes and the median for ${esc(r23f.filter((r) => r.pct === "median").map((r) => r.code).join(", "))}.`, [C1676]),
factLi(`CMS proposed the RUC values for ${r23p.length} of 8 codes and finalized them as proposed: final equals the RUC recommendation for ${acceptN(r23f)} of ${r23f.length}.`, [C878, C1685]),
factLi(`CY2008: CMS proposed the RUC values (“Agree”), then refined all ${r08f.length} in the final rule; final equals the RUC recommendation for ${acceptN(r08f)} of ${r08f.length}.`, [C08p, C2242]),
], ["rounds", cyRounds, "ok"]],
["payment", "Payment", "i-coin", [
factLi(`99350 national non-facility payment: ${money(p15 && p15.pay_nf)} (CY2015 final) → ${money(p27 && p27.pay_nf)} (CY2027 final); CY2027 proposed ${money(p27p && p27p.pay_nf)}.`, []).replace("</li>", [p15, p27, p27p].filter(Boolean).map((r) => ` <a class="cite" href="${esc(r.url)}" target="_blank" rel="noopener" title="${esc(r.label)}">${esc(r.cite)}</a>`).join("") + "</li>"),
factLi(`Conversion factor CY2027 final $${cf27.cf.toFixed(4)}.`).replace("</li>", ` <a class="cite" href="https://www.federalregister.gov/citation/${cf27.cite.replace(/ /g, "-")}" target="_blank" rel="noopener">${esc(cf27.cite)}</a></li>`),
factLi(`Final RVU files ${M.rvu_years[0]}–${M.rvu_years[1]}; proposed RVUs for the CY2026 and CY2027 proposed rules.`),
], ["RVU files", `${M.rvu_years[0]}–${M.rvu_years[1]}`, "ok"]],
["politics", "Politics", "i-messages", [
factLi(`${D.commenters.length} commenters identified on the CY2023 docket ${linkA("https://www.regulations.gov/docket/" + M.docket, M.docket)}; ${withPos} took a position on at least one event <span class="pill prelim">preliminary</span>.`),
factLi(`CMS's summary: “${esc(firstSentence(D.paras["PETM225D:1679"] || ""))}”`, [C1679]),
factLi(`${D.summaries.length} CMS Comment/Response pairs about the family, CY${Math.min(...D.summaries.map((s) => s.cy))}–CY${Math.max(...D.summaries.map((s) => s.cy))}; ${cms23.length} in the CY2023 final rule.`),
], ["stances", "preliminary", "warn"]],
["sources", "Guidance and sources", "i-books", [
factLi(`CFR pointers: ${cfrG.length} locators, e.g. ${esc(cfrG.slice(0, 2).join(", "))}. Manual pointers: ${esc(iomG.join(", "))}.`),
factLi(`${nRules} Federal Register rules with paragraph anchors, ${D.commenters.length} public comments, ${Object.values(D.sources).filter((s) => s.type === "source" && /Medicare Physician/.test(s.title)).length} utilization files.`),
], ["on file", `${nRules} rules · ${D.commenters.length} comments`, "ok"]],
];
const gaps = [
"Federal Register PFS rules before CY2002 are not captured yet.",
`RVU files before 2015 are not captured yet; the payment table starts at CY${M.rvu_years[0]}.`,
"Comment dockets before the CY2018 rule are not captured yet; earlier rules show CMS's own summaries only.",
`Stance counts are preliminary. ${evalLine(docketRow)}`,
"Commenters are identified on all 23 PFS dockets; stances are classified for the CY2023 docket only so far (other dockets pending).",
"Proposed-rule RVUs are captured for the CY2026 and CY2027 proposed rules only.",
`regulations.gov posts a personal name for ${named} of the ${D.commenters.length} CY2023 commenters; the rest are shown by organization as posted.`,
`${D.review.length} extracted rows are held for review (see Review).`,
];
$('[data-view="dossier"]').innerHTML = hdr + `
<div class="sec-head"><span class="section-eyebrow">Dossier</span><h3>Sections</h3></div>
<div class="tiles">${tiles.map(([href, t, glyph, facts, [k, v, st]]) => `<div class="tile">${st === "warn" ? '<span class="badge new">Preliminary</span>' : ""}${icon(glyph)}<div class="tile-eyebrow">Section</div><div class="tile-title"><a href="#${href}">${t}</a></div><ul class="facts tile-desc">${facts.join("")}</ul>${health(k, v, st)}<span class="tile-port">${esc(HOST)}/#${href}</span><div class="stripe"></div></div>`).join("")}</div>
<div class="sec panel warn"><h3>Gaps</h3><ul class="gaps">${gaps.map((g) => `<li>${esc(g)}</li>`).join("")}</ul></div>
<div class="filters" style="margin-top:14px"><button class="dis" disabled>Flag this</button><span class="small muted">Sends a row to the review queue. Comes with S2.</span></div>`;
}
/* ── timeline assembly ─────────────────────────────────────────── */
const TL = [];
const KIND_TITLE = {
deleted: "Deletion named in the rule", cpt_panel: "CPT Editorial Panel action named in the rule", telehealth_list: "Medicare telehealth list paragraph",
cpt_deleted: "Absent from the CPT book", cpt_changed: "CPT change noted in the CPT book", appeared: "First year in the RVU file", disappeared: "No longer in the RVU file",
descriptor_change: "RVU-file short descriptor changed", revalued: "Non-facility total RVUs changed by more than 10% from the prior year", status_change: "Payment status changed", created: "Created", crosswalk: "Crosswalk",
};
const ORIGIN_TITLE = { statute: "Statute", regulation: "Regulation", coverage: "Manual and coverage", cpt_panel: "CPT Editorial Panel" };
function buildTimeline() {
// links
D.links.forEach((l) => {
const kind = l.kind === "prolonged-add-on" ? "prolonged add-on code" : l.kind === "add-on" ? "add-on code" : "modifier";
const t = `${l.target} linked as ${kind}` + (l.aliases.length ? ` (proposed as ${l.aliases.join(", ")})` : "");
TL.push({ cy: l.since, type: "link", title: t, codes: [l.target], cites: [l.alias_cite, l.cite].filter(Boolean) });
});
// lineage: FR rows merged per paragraph; RVU notes grouped per year and kind
const fr = {}, rvu = {};
D.events.forEach((e) => {
if (e.source === "fr") {
const k = e.year + "|" + e.cite.label;
(fr[k] = fr[k] || { cy: e.year, kinds: [], codes: [], cite: e.cite }).kinds.push(e.kind);
fr[k].codes.push(...e.codes);
} else if (e.source === "rvu") {
const k = e.year + "|" + e.kind;
(rvu[k] = rvu[k] || { cy: e.year, kind: e.kind, rows: [], rvu: e.rvu }).rows.push(e);
} else {
TL.push({ cy: e.year, type: "lineage", title: KIND_TITLE[e.kind] || e.kind, sum: e.note ? `Book note: “${e.note}”` : "", codes: e.codes, book: e.book });
}
});
Object.values(fr).forEach((g) => TL.push({ cy: g.cy, type: "lineage", title: uniq(g.kinds).map((k) => KIND_TITLE[k] || k).join("; "), codes: uniq(g.codes), cites: [g.cite] }));
Object.values(rvu).forEach((g) => {
const codes = uniq(g.rows.flatMap((r) => r.codes));
const notes = g.rows.filter((r) => r.note).map((r) => `${r.codes.join(", ")}: ${r.note}`);
TL.push({ cy: g.cy, type: "lineage", title: KIND_TITLE[g.kind] || g.kind, sum: notes.join(" · "), codes, rvu: g.rvu });
});
// origins
D.origins.forEach((o) => {
const s = o.cite ? srcOf(o.cite.k) : null;
const cy = o.year || (s ? s.cy : null);
if (!cy) return;
const sums = o.summaries.map((x) => x.text).filter(Boolean);
TL.push({ cy, type: "origin", title: `${ORIGIN_TITLE[o.kind] || o.kind}: ${o.ref}`, undated: !o.year, sum: sums.join(" · "), model: sums.length > 0, codes: o.codes, cites: [o.cite, o.ref_cite].filter(Boolean), book: o.book, target: o.target, extractors: o.extractors });
});
// valuation rounds
const rg = {};
D.rounds.forEach((r) => { const k = r.cy + "|" + r.kind; (rg[k] = rg[k] || { cy: r.cy, kind: r.kind, rows: [] }).rows.push(r); });
Object.values(rg).forEach((g) => {
const dec = {};
g.rows.forEach((r) => { dec[r.decision] = (dec[r.decision] || 0) + 1; });
const pcts = {};
g.rows.forEach((r) => { if (r.pct) pcts[r.pct] = (pcts[r.pct] || 0) + 1; });
const ptxt = Object.keys(pcts).length ? "; RUC survey percentile " + Object.entries(pcts).map(([p, n]) => `${p} ×${n}`).join(", ") : "";
const cites = uniq(g.rows.map((r) => r.cite && r.cite.label)).map((l) => g.rows.find((r) => r.cite && r.cite.label === l).cite);
TL.push({ cy: g.cy, type: "valuation", title: `Work RVU round, ${ruleName(g.rows[0].key)}`, sum: `${g.rows.length} codes; CMS decision ${Object.entries(dec).map(([d, n]) => `“${d}” ×${n}`).join(", ")}${ptxt}.`, codes: g.rows.map((r) => r.code), cites, round: g });
});
// CMS summaries
D.summaries.forEach((s) => {
const q = firstSentence(s.ctext);
TL.push({ cy: s.cy, type: "cms", title: `CMS's summary: ${quant(s.q)}, stance “${s.stance}”; CMS ${ACTION[s.action] || s.action}`, sum: q ? "“" + (q.length > 220 ? q.slice(0, 220).replace(/\s+\S*$/, "") + " …" : q) + "”" : "", codes: s.codes, cites: [s.comment, s.response], cms: s });
});
const ORDER = { origin: 0, link: 1, lineage: 2, valuation: 3, cms: 4 };
TL.sort((a, b) => a.cy - b.cy || ORDER[a.type] - ORDER[b.type]);
TL.forEach((t) => {
const fields = [["Year", "CY" + t.cy + (t.undated ? " (year of the citing rule)" : "")], ["Codes", esc(compact(t.codes || [])) || "—"]];
if (t.sum) fields.push([t.model ? "Model summary" : "Detail", esc(t.sum)]);
if (t.extractors) fields.push(["Extracted by", esc(t.extractors.join(", "))]);
const extra = [];
if (t.book) extra.push(bookCite(t.book) + `<div class="small muted">Licensed book on file. Cited by edition and page; text not reproduced.</div>`);
if (t.rvu) extra.push(rvuCite(t.rvu) + `<div class="small muted">${esc(t.rvu.label)}, published with the ${esc(t.rvu.cite)} final rule.</div>`);
if (t.target) extra.push(linkA(t.target.url, t.target.label + " on eCFR"));
t.ev = ev({ kind: t.type === "cms" ? "CMS's summary" : t.type, title: t.title, fields, cites: t.cites, extra, badges: t.cms ? `<span class="pill cms">CMS's summary</span><span class="pill model">read by local model</span>` : "" });
});
}
/* ── #timeline ─────────────────────────────────────────────────── */
const TL_TYPES = [["all", "All"], ["lineage", "Lineage"], ["origin", "Origin"], ["valuation", "Valuation"], ["cms", "CMS's summaries"], ["link", "Links"]];
const TYPE_NAME = { lineage: "Lineage", origin: "Origin", valuation: "Valuation", cms: "CMS's summary", link: "Link" };
let tlType = "all", tlCode = "";
function renderTimeline() {
const host = $('[data-view="timeline"]');
if (!host.dataset.init) {
const counts = {};
TL.forEach((t) => { counts[t.type] = (counts[t.type] || 0) + 1; });
host.innerHTML = `<h2>Timeline</h2><p class="lede">Dated entries from the family's lineage, origin, valuation rounds and CMS's own comment summaries. Select an entry to read its source.</p>
<div class="filters" id="tl-f">${TL_TYPES.map(([k, t]) => `<button class="fbtn" type="button" data-k="${k}" aria-pressed="${k === tlType}">${t} <span class="muted">${k === "all" ? TL.length : counts[k] || 0}</span></button>`).join("")}
<label class="eyebrow" for="tl-code" style="margin-left:8px">Code</label>
<select class="fsel" id="tl-code"><option value="">All codes</option>${[...BASE, ...PRED, ...LINKED, "G2211", "B1", "B2"].map((c) => `<option>${c}</option>`).join("")}</select></div>
<div id="tl-list"></div>`;
host.dataset.init = "1";
$("#tl-f").addEventListener("click", (e) => { const b = e.target.closest("[data-k]"); if (!b) return; tlType = b.dataset.k; $$("#tl-f [data-k]").forEach((x) => x.setAttribute("aria-pressed", x.dataset.k === tlType)); renderTimelineList(); });
$("#tl-code").addEventListener("change", (e) => { tlCode = e.target.value; renderTimelineList(); });
}
renderTimelineList();
}
function renderTimelineList() {
const items = TL.filter((t) => (tlType === "all" || t.type === tlType) && (!tlCode || (t.codes || []).includes(tlCode)));
const years = uniq(items.map((t) => t.cy));
$("#tl-list").innerHTML = years.length ? years.map((y) => {
const its = items.filter((t) => t.cy === y);
const rule = ruleSources.filter((s) => s.cy === y).map((s) => s.kind).join(" + ");
return `<div class="tl-year"><h4>CY${y}<small>${its.length} ${its.length === 1 ? "entry" : "entries"}${rule ? " · " + esc(rule) + " rule" : ""}</small></h4><div class="tl-items">${its.map((t) => `
<div class="tl-item" ${evAttr(t.ev)}><div class="top"><span class="type ${t.type}">${TYPE_NAME[t.type]}</span><span class="ttl">${esc(t.title)}</span></div>
${t.sum ? `<div class="sum">${t.model ? '<span class="pill model">model summary</span> ' : ""}${esc(t.sum)}</div>` : ""}
<div class="top small"><span class="mono muted">${esc(compact(t.codes || []))}</span>${(t.cites || []).map(citeA).join("")}${t.book ? bookCite(t.book) : ""}${t.rvu ? rvuCite(t.rvu) : ""}${t.target ? `<a class="cite" href="${esc(t.target.url)}" target="_blank" rel="noopener">${esc(t.target.label)}</a>` : ""}</div></div>`).join("")}</div></div>`;
}).join("") : `<p class="muted">No entries match.</p>`;
}
/* ── #valuation ────────────────────────────────────────────────── */
function step(n, head, quote, cites, id) {
return `<li ${evAttr(id)}><span class="n">${n}</span><span class="h">${head}</span>${quote ? `<q>${esc(quote)}</q>` : ""}<span>${cites.filter(Boolean).map(citeA).join(" ")}</span></li>`;
}
function firstSentence(t, kw) {
if (!t) return "";
let s = t.replace(/^… /, "").replace(/^(Comment|Response):\s*/, "");
if (kw) { const i = s.indexOf(kw); if (i >= 0) s = s.slice(i); }
const m = s.match(/^(.+?\.)(\s|$)/);
const out = m ? m[1] : s;
return out.length > 300 ? out.slice(0, 300).replace(/\s+\S*$/, "") + " …" : out;
}
function dotPlot() {
const codes = uniq([...BASE, ...PRED].filter((c) => D.rounds.some((r) => r.code === c)));
const W = 660, rowH = 24, top = 46, left = 64, right = 24, H = top + codes.length * rowH + 30;
const xmax = 4.5, X = (v) => left + (v / xmax) * (W - left - right);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Work RVUs by code: CY2008 RUC recommendation and final, CY2023 RUC recommendation and final">`;
for (let v = 0; v <= 4; v++) s += `<line class="grid" x1="${X(v)}" x2="${X(v)}" y1="${top - 8}" y2="${H - 26}"/><text x="${X(v)}" y="${H - 10}" text-anchor="middle">${v.toFixed(1)}</text>`;
s += `<text x="${left}" y="${H - 10}" text-anchor="end" dx="-8">work RVU</text>`;
// legend
s += `<g transform="translate(${left},14)"><circle cx="6" cy="0" r="4.5" fill="var(--card)" stroke="var(--s-ruc)" stroke-width="2"/><text x="16" y="4">CY2008 RUC recommendation</text>
<rect x="200" y="-5" width="10" height="10" fill="var(--s-prop)"/><text x="216" y="4">CY2008 final</text>
<circle cx="316" cy="0" r="5" fill="var(--s-final)"/><text x="326" y="4">CY2023 final (= RUC recommendation)</text></g>`;
codes.forEach((c, i) => {
const y = top + i * rowH + rowH / 2;
const deleted = PRED.includes(c);
s += `<text x="${left - 10}" y="${y + 4}" text-anchor="end"${deleted ? ' style="text-decoration:line-through"' : ""}>${c}</text>`;
s += `<line class="grid" x1="${left}" x2="${W - right}" y1="${y}" y2="${y}" stroke-dasharray="2 4"/>`;
const a = D.rounds.find((r) => r.code === c && r.cy === 2008 && r.kind === "final");
const b = D.rounds.find((r) => r.code === c && r.cy === 2023 && r.kind === "final");
if (a && a.ruc != null && a.fin != null) {
s += `<line x1="${X(a.ruc)}" x2="${X(a.fin)}" y1="${y}" y2="${y}" stroke="var(--s-prop)" stroke-width="2"/>`;
s += `<circle cx="${X(a.ruc)}" cy="${y}" r="4.5" fill="var(--card)" stroke="var(--s-ruc)" stroke-width="2" data-tip="${c} · CY2008 RUC recommendation ${n2(a.ruc)}"/>`;
s += `<rect x="${X(a.fin) - 5}" y="${y - 5}" width="10" height="10" fill="var(--s-prop)" stroke="var(--card)" stroke-width="1.5" data-tip="${c} · CY2008 final ${n2(a.fin)} (refined)"/>`;
}
if (b && b.fin != null) s += `<circle cx="${X(b.fin)}" cy="${y}" r="5.5" fill="var(--s-final)" stroke="var(--card)" stroke-width="2" data-tip="${c} · CY2023 final ${n2(b.fin)} · RUC ${n2(b.ruc)} (${b.pct} percentile)"/>`;
});
return s + `</svg>`;
}
function roundTable(cy) {
const codes = uniq(D.rounds.filter((r) => r.cy === cy).map((r) => r.code)).sort();
const rowsP = D.rounds.filter((r) => r.cy === cy && r.kind === "proposed");
const rowsF = D.rounds.filter((r) => r.cy === cy && r.kind === "final");
let h = `<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Code</th><th class="r">RUC rec.</th><th>Survey pct.</th>${cy === 2008 ? '<th class="r">Requested</th>' : '<th class="r">Survey total min</th>'}<th class="r pcol">CMS proposed</th><th class="pcol">Decision</th><th class="r">CMS final</th><th>Decision</th><th>Sources</th></tr></thead><tbody>`;
codes.forEach((c) => {
const p = rowsP.find((r) => r.code === c) || {}, f = rowsF.find((r) => r.code === c) || {};
const del = PRED.includes(c);
const cites = uniq([p.cite, f.cite, ...(f.cites ? Object.values(f.cites).flat() : []), ...(p.cites ? Object.values(p.cites).flat() : [])].filter(Boolean).map((x) => x.label)).map((l) => [p.cite, f.cite, ...(f.cites ? Object.values(f.cites).flat() : []), ...(p.cites ? Object.values(p.cites).flat() : [])].find((x) => x && x.label === l));
const id = ev({ kind: "valuation round", title: `${c}, CY${cy} work RVU round`, badges: `<span class="pill prop">proposed</span><span class="pill final">final</span>`,
fields: [["RUC recommendation", n2(f.ruc != null ? f.ruc : p.ruc)], ["Survey percentile", esc(f.pct || p.pct || "not stated")], ["Requested", n2(f.req != null ? f.req : p.req)], ["CMS proposed", n2(p.prop != null ? p.prop : f.prop)], ["Proposed decision", esc(p.decision || "—")], ["CMS final", n2(f.fin)], ["Final decision", esc(f.decision || "—")], ["Final rationale", esc(f.quote || "—")], ["Table", esc(f.table || p.table || "—")], ["Extracted by", esc(uniq([p.extractor, f.extractor].filter(Boolean)).join("; "))]],
cites });
const total = p.total != null ? p.total : f.total;
h += `<tr ${evAttr(id)}><td class="mono"${del ? ' style="text-decoration:line-through"' : ""}>${c}</td><td class="r num">${n2(f.ruc != null ? f.ruc : p.ruc)}</td><td class="small">${esc(f.pct || p.pct || "—")}</td>
<td class="r num">${cy === 2008 ? n2(f.req != null ? f.req : p.req) : total != null ? int(total) : "—"}</td>
<td class="r num pcol">${n2(p.prop)}</td><td class="pcol small">${esc(p.decision || "—")}</td><td class="r num">${n2(f.fin)}</td><td class="small">${esc(f.decision || "—")}</td>
<td class="nowrap">${[p.cite, f.cite].filter(Boolean).map(citeA).join(" ")}${cites.length > 2 ? ` <span class="small muted">+${cites.length - 2}</span>` : ""}</td></tr>`;
});
return h + `</tbody></table></div>`;
}
function renderValuation() {
const t1676 = D.paras["PETM225D:1676"] || "", t878 = D.paras["MZ24MX5S:878"] || "", t1685 = D.paras["PETM225D:1685"] || "", t1679 = D.paras["PETM225D:1679"] || "", t1681 = D.paras["PETM225D:1681"] || "";
const t2241 = D.paras["54GTKR5G:2241"] || "", t2242 = D.paras["54GTKR5G:2242"] || "";
const sId = (title, cites) => ev({ kind: "valuation", title, cites });
const p08 = r08p[0] || {};
const cy23 = [
step("1 · RUC", "RUC recommended the survey 25th percentile, the median for 99350", firstSentence(t1676, "The RUC recommended"), [C1676], sId("RUC recommendation, CY2023", [C1676])),
step("2 · CMS proposed", `CMS proposed the RUC values for all ${r23p.length} codes`, firstSentence(t878), [C878], sId("CMS proposed rule, CY2023", [C878, C877])),
step("3 · Commenters", "CMS's summary of the comments", firstSentence(t1679), [C1679, C1681], sId("CMS's summary of comments on the RVUs, CY2023 final rule", [C1679, C1681])),
step("4 · CMS final", "CMS finalized the work RVUs as proposed", firstSentence(t1685), [C1685], sId("CMS final rule, CY2023", [C1685])),
].join("");
const cy08 = [
step("1 · CMS proposed", `CMS proposed the RUC values (“${esc(p08.quote || "")}”)`, "", [C08p], ev({ kind: "valuation", title: "CMS proposed rule, CY2008", fields: [["Table", esc(p08.table || "")], ["Decision", esc(p08.quote || "")]], cites: [C08p] })),
step("2 · Commenters", "CMS's summary of the comments", firstSentence(t2241), [C2241], sId("CMS's summary of comments, CY2008 final rule", [C2241])),
step("3 · CMS final", `CMS refined all ${r08f.length} codes`, firstSentence(t2242), [C2242], sId("CMS final rule, CY2008", [C2242])),
].join("");
$('[data-view="valuation"]').innerHTML = `<h2>Valuation</h2>
<p class="lede">RUC recommendation against CMS's proposed and final work RVUs, per code per round. Two rounds are captured: CY2008 and CY2023. Proposed columns are hatched.</p>
<div class="legend" style="margin-bottom:12px"><span><span class="pill prop">proposed</span> from the proposed rule</span><span><span class="pill final">final</span> from the final rule</span></div>
<h3>CY2023: the 25th-percentile round</h3>
<ol class="steps">${cy23}</ol>
<div class="sec">${roundTable(2023)}</div>
<h3>CY2008: CMS refined the RUC values</h3>
<ol class="steps">${cy08}</ol>
<div class="sec">${roundTable(2008)}</div>
<h3>Work RVUs across the two rounds</h3>
<div class="chart-box">${dotPlot()}</div>
<p class="small muted">CY2008 codes not revalued in CY2023 were deleted for CY2023 (struck through). 99341 and 99342 are not in the CY2008 table.</p>`;
}
/* ── #payment ──────────────────────────────────────────────────── */
let payMetric = "pay_nf", payGroup = "base", payCode = "99350";
const YEARS = uniq(D.payment.map((p) => p.cy)).sort((a, b) => a - b);
const PCOLS = [];
YEARS.forEach((y) => {
if (D.payment.some((p) => p.cy === y && p.proposed)) PCOLS.push([y, true]);
PCOLS.push([y, false]);
});
const fmtM = (v) => (payMetric === "pay_nf" ? money(v) : n2(v));
function payChart(code) {
const rows = D.payment.filter((p) => p.code === code);
const fin = rows.filter((p) => !p.proposed && p.pay_nf != null);
const pro = rows.filter((p) => p.proposed && p.pay_nf != null);
const W = 640, H = 230, L = 58, R = 64, T = 18, B = 30;
const ys = [...fin, ...pro].map((p) => p.pay_nf);
if (!ys.length) return `<p class="small muted">No RVU-based payment for ${code} (status ${esc((rows[0] || {}).status || "")}).</p>`;
const x0 = YEARS[0], x1 = YEARS[YEARS.length - 1];
const step = ys.length ? Math.max(...ys) > 150 ? 50 : Math.max(...ys) > 60 ? 25 : 10 : 10;
const ymax = Math.ceil((Math.max(...ys) * 1.08) / step) * step;
const X = (y) => L + ((y - x0) / (x1 - x0)) * (W - L - R), Y = (v) => T + (1 - v / ymax) * (H - T - B);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="National non-facility payment for ${code}, final and proposed">`;
for (let v = 0; v <= ymax + 1e-9; v += step) s += `<line class="grid" x1="${L}" x2="${W - R}" y1="${Y(v)}" y2="${Y(v)}"/><text x="${L - 8}" y="${Y(v) + 4}" text-anchor="end">$${v}</text>`;
YEARS.forEach((y) => { if ((y - x0) % 2 === 0 || y === x1) s += `<text x="${X(y)}" y="${H - 10}" text-anchor="middle">${y}</text>`; });
s += `<line class="ax" x1="${L}" x2="${W - R}" y1="${Y(0)}" y2="${Y(0)}"/>`;
// final line segments (break where a year is missing)
let path = "", prev = null;
fin.forEach((p) => { path += (prev != null && p.cy === prev + 1 ? "L" : "M") + X(p.cy).toFixed(1) + " " + Y(p.pay_nf).toFixed(1); prev = p.cy; });
s += `<path d="${path}" fill="none" stroke="var(--s-final)" stroke-width="2"/>`;
fin.forEach((p) => { s += `<circle cx="${X(p.cy)}" cy="${Y(p.pay_nf)}" r="4" fill="var(--s-final)" stroke="var(--card)" stroke-width="2" data-tip="${code} · CY${p.cy} final · ${money(p.pay_nf)} · CF $${p.cf.toFixed(4)}"/>`; });
pro.forEach((p) => { s += `<circle cx="${X(p.cy)}" cy="${Y(p.pay_nf)}" r="6.5" fill="none" stroke="var(--s-prop)" stroke-width="2" data-tip="${code} · CY${p.cy} proposed · ${money(p.pay_nf)} · CF $${p.cf.toFixed(4)}"/>`; });
const last = fin[fin.length - 1];
if (last) s += `<text x="${X(last.cy) + 10}" y="${Y(last.pay_nf) + 4}" style="fill:var(--foreground)">${money(last.pay_nf)}</text>`;
return s + `</svg><div class="legend"><span><span class="sw" style="background:var(--s-final)"></span>Final (Addendum B × CF)</span><span><span class="sw" style="border:2px solid var(--s-prop);border-radius:50%"></span>Proposed (proposed-rule Addendum B × proposed CF)</span></div>`;
}
function volChart(code) {
const rows = D.util.filter((u) => u.code === code);
if (!rows.length) return `<p class="small muted">No national utilization rows for ${code}.</p>`;
const yrs = []; for (let y = M.util_years[0]; y <= M.util_years[1]; y++) yrs.push(y);
const W = 640, H = 150, L = 58, R = 64, T = 12, B = 26;
const vmax = Math.max(...rows.map((r) => r.services));
const mag = Math.pow(10, Math.floor(Math.log10(vmax)));
const step = vmax / mag > 5 ? mag * 2 : vmax / mag > 2 ? mag : mag / 2;
const ymax = Math.ceil(vmax / step) * step;
const bw = (W - L - R) / yrs.length;
const X = (y) => L + (y - yrs[0]) * bw, Y = (v) => T + (1 - v / ymax) * (H - T - B);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Medicare allowed services for ${code} by year">`;
for (let v = 0; v <= ymax + 1e-9; v += step) s += `<line class="grid" x1="${L}" x2="${W - R}" y1="${Y(v)}" y2="${Y(v)}"/><text x="${L - 8}" y="${Y(v) + 4}" text-anchor="end">${v >= 1e6 ? (v / 1e6).toFixed(1) + "M" : v >= 1000 ? Math.round(v / 1000) + "k" : v}</text>`;
yrs.forEach((y) => {
const r = rows.find((x) => x.cy === y);
if (r) { const h = Y(0) - Y(r.services); s += `<path d="M${X(y) + 3} ${Y(0)} V${Y(r.services) + Math.min(4, h)} q0 -4 4 -4 H${X(y) + bw - 7} q4 0 4 4 V${Y(0)} Z" fill="var(--s-final)" data-tip="${code} · ${y} · ${int(r.services)} services"/>`; }
if ((y - yrs[0]) % 2 === 0 || y === yrs[yrs.length - 1]) s += `<text x="${X(y) + bw / 2}" y="${H - 8}" text-anchor="middle">${y}</text>`;
});
return s + `<line class="ax" x1="${L}" x2="${W - R}" y1="${Y(0)}" y2="${Y(0)}"/></svg>`;
}
function renderPayment() {
const host = $('[data-view="payment"]');
if (!host.dataset.init) {
host.innerHTML = `<h2>Payment</h2>
<p class="lede">National unadjusted non-facility payment = non-facility total RVUs × conversion factor, for every year on file. Final values come from each final rule's RVU file; proposed values from the proposed rule's file. They sit in separate columns and are never combined.</p>
<div class="filters" id="pay-f">
<span class="eyebrow">Show</span>
<button class="fbtn" type="button" data-m="pay_nf">Payment $</button><button class="fbtn" type="button" data-m="work">Work RVU</button><button class="fbtn" type="button" data-m="total_nf">Total RVU (non-facility)</button>
<span class="eyebrow" style="margin-left:10px">Codes</span>
<button class="fbtn" type="button" data-g="base">Active (8)</button><button class="fbtn" type="button" data-g="pred">Deleted CY2023 (12)</button><button class="fbtn" type="button" data-g="linked">Linked G0318</button>
</div>
<div class="legend" style="margin-bottom:8px"><span><span class="pill prop">proposed</span> hatched columns</span><span><span class="pill final">final</span> plain columns</span><span class="muted">Select a code to chart it.</span></div>
<div id="pay-tbl"></div>
<div class="charts"><div class="chart-box"><div class="col-h"><h3 id="pay-ch-h"></h3></div><div id="pay-ch"></div></div>
<div class="chart-box"><div class="col-h"><h3 id="vol-ch-h"></h3></div><div id="vol-ch"></div><p class="small muted" id="vol-src"></p></div></div>
<div class="sec" id="cf-notes"></div>`;
host.dataset.init = "1";
$("#pay-f").addEventListener("click", (e) => {
const b = e.target.closest("button"); if (!b) return;
if (b.dataset.m) payMetric = b.dataset.m;
if (b.dataset.g) { payGroup = b.dataset.g; const first = { base: "99350", pred: "99343", linked: "G0318" }[payGroup]; payCode = first; }
renderPayment();
});
}
$$("#pay-f [data-m]").forEach((b) => b.setAttribute("aria-pressed", b.dataset.m === payMetric));
$$("#pay-f [data-g]").forEach((b) => b.setAttribute("aria-pressed", b.dataset.g === payGroup));
const codes = payGroup === "base" ? BASE : payGroup === "pred" ? PRED : LINKED;
let h = `<div class="tbl-wrap" id="pay-scroll"><table class="tbl"><thead><tr><th class="stick">Code</th>${PCOLS.map(([y, p]) => `<th class="r${p ? " pcol" : ""}">CY${y}${p ? "<br>proposed" : "<br>final"}</th>`).join("")}</tr></thead><tbody>`;
codes.forEach((c) => {
const id = ev({ kind: "payment", title: `${c}: RVUs and national payment`, fields: [], html: payEvidence(c) });
h += `<tr ${evAttr(id)} data-code="${c}" class="${c === payCode ? "sel" : ""}"><td class="mono stick">${c}</td>${PCOLS.map(([y, p]) => {
const r = pay(c, y, p);
if (!r) return `<td class="r na${p ? " pcol" : ""}">—</td>`;
const v = r[payMetric];
const nopay = payMetric === "pay_nf" && v == null;
return `<td class="r num${p ? " pcol" : ""}${nopay ? " na" : ""}" title="${esc(nopay ? r.status_note : r.label)}">${nopay ? esc(r.status) : fmtM(v)}</td>`;
}).join("")}</tr>`;
});
h += `<tr><td class="mono stick small">Conv. factor</td>${PCOLS.map(([y, p]) => { const c = cfOf(y, p); return `<td class="r num${p ? " pcol" : ""}" title="${esc(c ? c.cite : "")}">${c ? c.cf.toFixed(2) : "—"}</td>`; }).join("")}</tr>`;
h += `<tr><td class="mono small stick">Source</td>${PCOLS.map(([y, p]) => { const c = cfOf(y, p); return `<td class="r${p ? " pcol" : ""}">${c ? `<a class="cite" href="https://www.federalregister.gov/citation/${c.cite.replace(/ /g, "-")}" target="_blank" rel="noopener">${esc(c.cite)}</a>` : ""}</td>`; }).join("")}</tr>`;
$("#pay-tbl").innerHTML = h + `</tbody></table></div><p class="small muted">Scroll the table sideways for earlier years. A status letter in place of a dollar value means the status is not paid from RVUs × CF (hover for the reason). A dash means the code is not in that year's file.</p>`;
const sc = $("#pay-scroll"); if (sc) sc.scrollLeft = sc.scrollWidth;
$$("#pay-tbl tr[data-code]").forEach((tr) => tr.addEventListener("click", () => { payCode = tr.dataset.code; drawPay(); $$("#pay-tbl tr[data-code]").forEach((x) => x.classList.toggle("sel", x === tr)); }));
drawPay();
const notes = D.cfs.filter((c) => c.note && c.note !== "non-APM standard CF");
$("#cf-notes").innerHTML = `<h3>Conversion factor notes</h3><ul class="gaps">${notes.map((c) => `<li><b>CY${c.cy} ${c.proposed ? "proposed" : "final"}</b> $${c.cf.toFixed(4)}: ${esc(c.note)} <span class="cite book">${esc(c.cite)}</span></li>`).join("")}</ul>`;
}
function payEvidence(c) {
const rows = D.payment.filter((p) => p.code === c);
return `<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Year</th><th>Vintage</th><th>St.</th><th class="r">Work</th><th class="r">PE NF</th><th class="r">MP</th><th class="r">Total NF</th><th class="r">CF</th><th class="r">Pay NF</th></tr></thead><tbody>${rows.map((r) => `<tr><td class="num">${r.cy}</td><td>${r.proposed ? '<span class="pill prop">proposed</span>' : '<span class="pill final">final</span>'}</td><td class="mono">${esc(r.status)}</td><td class="r num">${n2(r.work)}</td><td class="r num">${n2(r.pe_nf)}</td><td class="r num">${n2(r.mp)}</td><td class="r num">${n2(r.total_nf)}</td><td class="r num">${r.cf.toFixed(4)}</td><td class="r num">${money(r.pay_nf)}</td></tr>`).join("")}</tbody></table></div>
<p class="small muted">Each row: ${esc((rows[0] || {}).label || "")} style RVU file of that vintage; final files cite the final rule, proposed files the proposed rule.</p>
<div class="small">${uniq(rows.map((r) => r.cite)).map((ct) => { const r = rows.find((x) => x.cite === ct); return `<a class="cite" href="${esc(r.url)}" target="_blank" rel="noopener">${esc(ct)}</a>`; }).join(" ")}</div>`;
}
function drawPay() {
$("#pay-ch-h").textContent = `${payCode}: national non-facility payment`;
$("#pay-ch").innerHTML = payChart(payCode);
$("#vol-ch-h").textContent = `${payCode}: Medicare allowed services`;
$("#vol-ch").innerHTML = volChart(payCode);
const keys = uniq(D.util.filter((u) => u.code === payCode).map((u) => u.key));
$("#vol-src").innerHTML = keys.length ? `National totals, all places of service. Source: Medicare Physician &amp; Other Practitioners by Geography and Service, ${keys.length} annual files ${keys.slice(-2).map((k) => linkA(srcOf(k).url, srcOf(k).title.replace(/.*(CY\d{4}).*/, "$1"))).join(", ")} …` : "";
}
/* ── #politics ─────────────────────────────────────────────────── */
function stanceBar(counts, total) {
const n = STANCES.reduce((a, s) => a + (counts[s] || 0), 0);
return `<div class="bar" role="img" aria-label="${STANCES.map((s) => `${s} ${counts[s] || 0}`).join(", ")}">${STANCES.filter((s) => counts[s]).map((s) => `<div class="st-${s}" style="flex:${counts[s]};background:var(--st-${s})" data-tip="${s}: ${counts[s]} of ${n}">${counts[s]}</div>`).join("")}</div>`;
}
function evalLine(dr) {
const e = dr && dr.eval;
if (!e) return "Accuracy not yet measured.";
const pos = Math.round(e.recall * e.n_positions);
const where = esc((e.dockets || [e.docket]).filter(Boolean).join(", "));
return `Measured ${e.in_sample ? "on the tuning set" : "on a held-out set the prompt was never tuned on"} (${where}; ${e.n_items} hand-coded comments, ${e.n_positions} positions): the model finds ${Math.round(e.recall * 100)}% of positions (${pos} of ${e.n_positions}); of those it finds, the direction is right ${Math.round(e.direction * 100)}% of the time.`;
}
function renderPolitics() {
const dr = docketRow || {};
const evs = EVENT_ORDER.map((e) => `${M.family}:2023:${e}`).filter((k) => (dr.events || {})[k]);
const ours = evs.map((k) => {
const c = dr.events[k];
const n = STANCES.reduce((a, s) => a + (c[s] || 0), 0);
const id = ev({ kind: "our count", title: `${EVENT_LABEL[evSlug(k)]}: our count`, badges: `<span class="pill prelim">preliminary</span>`, fields: STANCES.map((s) => [s, String(c[s] || 0)]).concat([["Commenters with a position", String(n)], ["Of identified commenters", String(D.commenters.length)]]),
extra: D.stances.filter((s) => s.event === k).slice(0, 40).map((s) => { const cm = D.commenters.find((x) => x.key === s.key) || {}; return `<div><span class="st ${s.stance}">${ST_LETTER[s.stance]}</span> <b>${esc(cm.name || cm.org || cm.reg_org || "Individual")}</b></div><blockquote class="cmt">${esc(s.evidence)}</blockquote>`; }) });
return `<tr ${evAttr(id)}><td>${esc(EVENT_LABEL[evSlug(k)])}</td><td class="r num">${n}</td><td style="min-width:160px">${stanceBar(c)}</td></tr>`;
}).join("");
const cms23 = D.summaries.filter((s) => s.cy === 2023);
const cmsList = cms23.map((s) => {
const id = ev({ kind: "CMS's summary", title: `CMS's summary: ${quant(s.q)}`, badges: `<span class="pill cms">CMS's summary</span><span class="pill model">quantifier and stance read by local model</span>`, fields: [["Quantifier", esc(s.q)], ["Stance", esc(s.stance)], ["CMS response", esc(ACTION[s.action] || s.action)], ["Codes named", esc(compact(s.codes)) || "—"]], cites: [s.comment, s.response] });
const q = firstSentence(s.ctext);
return `<div ${evAttr(id)}><q>${esc(q.length > 170 ? q.slice(0, 170).replace(/\s+\S*$/, "") + " …" : q)}</q><div class="small muted">${esc(quant(s.q))} · stance “${esc(s.stance)}” · CMS ${esc(ACTION[s.action] || s.action)} ${citeA(s.comment)}</div></div>`;
}).join("");
// stakeholder matrix: organization type × specialty, events across
const types = uniq(D.commenters.map((c) => c.type)).sort((a, b) => D.commenters.filter((c) => c.type === b).length - D.commenters.filter((c) => c.type === a).length);
let mx = `<div class="tbl-wrap"><table class="tbl"><thead><tr><th style="min-width:190px">Commenter</th><th>Name as posted</th><th>Specialty</th>${evs.map((k) => `<th title="${esc(EVENT_LABEL[evSlug(k)])}">${esc(evSlug(k) === "G0318" ? "G0318" : EVENT_LABEL[evSlug(k)].split(" ")[0])}</th>`).join("")}<th>Comment</th></tr></thead><tbody>`;
types.forEach((t) => {
const cs = D.commenters.filter((c) => c.type === t).sort((a, b) => (a.specialty || "~").localeCompare(b.specialty || "~") || (a.org || a.reg_org).localeCompare(b.org || b.reg_org));
mx += `<tr class="grp-row"><td colspan="${evs.length + 4}">${esc(TYPE_LABEL[t] || t)} · ${cs.length}</td></tr>`;
cs.forEach((c) => {
const mine = stancesBy[c.key] || [];
const who = c.org || c.reg_org || "";
const id = ev({ kind: "commenter", title: who || c.name || "Individual commenter", badges: `<span class="pill prelim">stances preliminary</span>`,
fields: [["Name as posted", esc(c.name || "not posted")], ["Organization", esc(c.org || "—")], ["On regulations.gov", esc(c.reg_org || "—")], ["Type", esc(TYPE_LABEL[c.type] || c.type)], ["Specialty", esc(titleCase(c.specialty) || "not stated")], ["regulations.gov category", esc(c.reg_category || "—")], ["Posted", esc(c.posted)], ["Comment", linkA(c.url, c.cid)], ["Identified by", esc(c.extractor)]],
extra: mine.map((s) => `<div><span class="st ${s.stance}">${ST_LETTER[s.stance]}</span> <b>${esc(EVENT_LABEL[evSlug(s.event)] || s.event)}</b>: ${esc(s.stance)}</div><div class="small muted">Model's reason: ${esc(s.reason)}</div><blockquote class="cmt">${esc(s.evidence)}</blockquote>`) });
mx += `<tr ${evAttr(id)}><td>${esc(who || "Individual")}</td><td class="small">${c.name ? esc(c.name) : '<span class="muted">—</span>'}</td><td class="small">${esc(titleCase(c.specialty)) || '<span class="muted">not stated</span>'}</td>${evs.map((k) => { const s = mine.find((x) => x.event === k); return `<td>${s ? `<span class="st ${s.stance}" title="${esc(s.stance)}">${ST_LETTER[s.stance]}</span>` : '<span class="st none">·</span>'}</td>`; }).join("")}<td><a class="cite" href="${esc(c.url)}" target="_blank" rel="noopener">${esc(c.cid.replace("CMS-2022-0113-", "…-"))}</a></td></tr>`;
});
});
mx += `</tbody></table></div>`;
// dockets over time
const dk = D.reaction.filter((r) => r.pk === "docket" && r.n > 0);
const frp = D.reaction.filter((r) => r.pk === "fr-pairs");
const dkt = `<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Docket</th><th>Rule</th><th>Comment period closed</th><th class="r">Commenters naming a family code</th><th class="r">All commenters</th></tr></thead><tbody>${dk.map((r) => `<tr><td>${linkA(r.url, r.period)}</td><td class="mono small">${esc(r.rule || "—")}</td><td class="num">${esc(r.close || "")}</td><td class="r num">${int(r.n)}</td><td class="r num">${int(r.total)}</td></tr>`).join("")}</tbody></table></div>
<p class="small muted">Before regulations.gov text is on file, the count is CMS's own Comment/Response pairs that name a family code: ${frp.map((r) => `CY${r.year} ${r.n} of ${int(r.total)} ${citeA(r.cite)}`).join(" · ")}</p>`;
$('[data-view="politics"]').innerHTML = `<h2>Politics</h2>
<p class="lede">Who commented on the CY2023 rule (docket ${linkA("https://www.regulations.gov/docket/" + M.docket, M.docket)}), by organization type and specialty, with each one's position per event. Our counts and CMS's own summaries sit side by side and are never merged.</p>
<div class="panel warn small" style="margin-bottom:16px"><b>Preliminary.</b> Stances are read by a local model. ${evalLine(dr)} ${D.commenters.length} commenters identified for this family; ${withPos} took a position on at least one event; ${dr.classified != null ? dr.classified : "—"} took a firm one (support, modify or oppose). Names: regulations.gov posts a personal name for ${named}; organizations are shown as posted.</div>
<div class="two">
<div><div class="col-h"><h3>Our count</h3><span class="pill prelim">preliminary</span>${dr.eval ? `<span class="pill">finds ${Math.round(dr.eval.recall * 100)}% · direction ${Math.round(dr.eval.direction * 100)}%</span>` : ""}</div>
<div class="legend" style="margin-bottom:8px">${STANCES.map((s) => `<span><span class="st ${s}">${ST_LETTER[s]}</span>${s}</span>`).join("")}</div>
<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Event, CY2023</th><th class="r">n</th><th>Stance</th></tr></thead><tbody>${ours}</tbody></table></div>
<p class="small muted">n = commenters with a position on that event; each counted once per event.</p></div>
<div><div class="col-h"><h3>CMS's summary</h3><span class="pill cms">CY2023 final rule</span></div>
<div class="cms-list">${cmsList}</div>
<p class="small muted">Quantifier and stance are read from CMS's text by a local model. Earlier years' summaries are on the Timeline.</p></div>
</div>
<h3>Stakeholders: organization type × specialty</h3>
<p class="small muted" style="margin-top:-4px">Select a commenter to read the model's reason and the verbatim quote behind each position. Columns: ${evs.map((k) => esc(EVENT_LABEL[evSlug(k)])).join(" · ")}.</p>
${mx}
<h3>Commenters per docket</h3>${dkt}
<div class="panel soon small" style="margin-top:16px"><b>Coming with S5:</b> expected stance from money and power stakes beside each actual stance, with a mark where they disagree, and the stakes panel.</div>`;
}
/* ── #sources ──────────────────────────────────────────────────── */
function renderSources() {
const uses = {};
const bump = (k) => { if (k) uses[k] = (uses[k] || 0) + 1; };
TL.forEach((t) => (t.cites || []).forEach((c) => bump(c.k)));
D.rounds.forEach((r) => bump(r.key));
D.guidance.forEach((g) => g.src && bump(g.src.k));
const groups = [
["Federal Register rules", (s) => !!s.fr, (s) => `<a class="cite" href="${esc(s.fr.html)}" target="_blank" rel="noopener">${esc(s.cite)}</a> <span class="small muted">FR Doc ${esc(s.fr.doc)} · ${esc(s.date)}</span>`, "Full text on file"],
["Regulations (eCFR)", (s) => s.type === "regulation", (s) => linkA(s.url, "eCFR"), "Link"],
["CMS manuals", (s) => s.type === "manual", (s) => linkA(s.url, "cms.gov PDF"), "Link"],
["Data files", (s) => s.type === "source" && /data\.cms\.gov/.test(s.url), (s) => linkA(s.url, "data.cms.gov"), "On file"],
["Books (licensed)", (s) => s.type === "source" && /openlibrary/.test(s.url), (s) => linkA(s.url, "catalog record"), "On file; paraphrased and cited by page"],
];
const all = Object.values(D.sources);
let h = `<h2>Sources</h2><p class="lede">Every source this dossier cites, with its status. ${all.length + D.commenters.length} in all.</p>`;
groups.forEach(([name, test, link, status]) => {
const ss = all.filter(test).sort((a, b) => (a.date || a.title).localeCompare(b.date || b.title));
if (!ss.length) return;
h += `<h3>${name} <span class="muted small">${ss.length}</span></h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Title</th><th>Link</th><th>Status</th><th class="r">Cited</th></tr></thead><tbody>${ss.map((s) => `<tr><td>${s.label ? `<b>${esc(s.label)}</b><div class="small muted">${esc(s.title)}</div>` : esc(s.title)}</td><td style="min-width:150px">${link(s)}</td><td class="small" style="min-width:110px">${esc(status)}</td><td class="r num">${uses[s.key] || ""}</td></tr>`).join("")}</tbody></table></div>`;
});
h += `<h3>Public comments, CY2023 docket <span class="muted small">${D.commenters.length}</span></h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Comment</th><th>Commenter</th><th>Posted</th><th>Status</th></tr></thead><tbody>${D.commenters.slice().sort((a, b) => a.cid.localeCompare(b.cid)).map((c) => `<tr><td>${linkA(c.url, c.cid)}</td><td>${esc(c.org || c.reg_org || c.name || "Individual")}${c.name && (c.org || c.reg_org) ? ` <span class="muted small">(${esc(c.name)})</span>` : ""}</td><td class="num">${esc(c.posted)}</td><td class="small">${c.mirror_ok ? "Record on file" : "Record not read"}</td></tr>`).join("")}</tbody></table></div>
<div class="filters" style="margin-top:14px"><button class="dis" disabled>Add source</button><span class="small muted">Paste a URL, DOI, FR citation, docket or comment id. Comes with S2.</span></div>`;
$('[data-view="sources"]').innerHTML = h;
}
/* ── coming views ──────────────────────────────────────────────── */
function soonHead(title, slice, does) {
return `<div class="col-h"><h2>${title}</h2><span class="pill soon">Coming · ${slice}</span></div><p class="lede">${does}</p>`;
}
function renderGrid() {
const codes = ["99341", "99343", "99347"], yrs = [2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027];
const vars = [["Payable status", (r) => (r ? r.status : "absent")], ["Work RVU", (r) => (r ? n2(r.work) : "—")]];
let h = soonHead("Surveillance grid", "S3", "Legal variables down, years across, per code. Each cell holds the coded value and its citation; changed cells are marked; selecting a cell opens the cited paragraph. The analyst codes or corrects a cell, and exports CSV with the codebook.");
h += `<div class="sketch"><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Variable</th><th>Code</th>${yrs.map((y) => `<th class="r">CY${y}</th>`).join("")}</tr></thead><tbody>`;
vars.forEach(([v, f]) => {
codes.forEach((c, i) => {
let prev = null;
h += `<tr>${i === 0 ? `<td rowspan="${codes.length}"><b>${v}</b></td>` : ""}<td class="mono">${c}</td>${yrs.map((y) => { const r = pay(c, y, false); const val = f(r); const chg = prev != null && val !== prev; prev = val; return `<td class="r num${chg ? " chg" : ""}" title="${r ? esc(r.label + " · " + r.cite) : "not in the RVU file"}">${esc(val)}</td>`; }).join("")}</tr>`;
});
});
h += `</tbody></table></div><p class="small muted"><span class="chg" style="padding-left:12px"></span> marks a value that changed from the prior year. Values from each year's final RVU file; “absent” means the code is not in that file. The full codebook adds telehealth eligibility, supervision, place of service, frequency limits and the rest.</p></div>`;
$('[data-view="grid"]').innerHTML = h;
}
function renderEvaluate() {
const yrs = []; for (let y = M.util_years[0]; y <= M.util_years[1]; y++) yrs.push(y);
const sum = (codes, y) => D.util.filter((u) => u.cy === y && codes.includes(u.code)).reduce((a, u) => a + u.services, 0);
const data = yrs.map((y) => ({ y, a: sum(BASE, y), b: sum(PRED, y) }));
const W = 640, H = 230, L = 62, R = 20, T = 30, B = 26;
const vmax = Math.max(...data.map((d) => d.a + d.b));
const mag = Math.pow(10, Math.floor(Math.log10(vmax)));
const step = vmax / mag > 5 ? mag * 2 : vmax / mag > 2 ? mag : mag / 2, ymax = Math.ceil(vmax / step) * step;
const bw = (W - L - R) / yrs.length, X = (y) => L + (y - yrs[0]) * bw, Y = (v) => T + (1 - v / ymax) * (H - T - B);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Allowed services: active home codes and deleted domiciliary codes by year">`;
for (let v = 0; v <= ymax; v += step) s += `<line class="grid" x1="${L}" x2="${W - R}" y1="${Y(v)}" y2="${Y(v)}"/><text x="${L - 8}" y="${Y(v) + 4}" text-anchor="end">${(v / 1e6).toFixed(1)}M</text>`;
s += `<g transform="translate(${L},12)"><rect width="10" height="10" y="-6" fill="var(--s-final)"/><text x="16" y="3">Active home codes (8)</text><rect x="190" width="10" height="10" y="-6" fill="var(--s-alt)"/><text x="206" y="3">Deleted for CY2023 (12)</text></g>`;
data.forEach((d) => {
const x = X(d.y) + 4, w = bw - 8;
if (d.a) s += `<rect x="${x}" y="${Y(d.a)}" width="${w}" height="${Y(0) - Y(d.a)}" fill="var(--s-final)" data-tip="${d.y} · ${compact(BASE)} · ${int(d.a)} services"/>`;
if (d.b) s += `<rect x="${x}" y="${Y(d.a + d.b)}" width="${w}" height="${Math.max(0, Y(d.a) - Y(d.a + d.b) - 2)}" fill="var(--s-alt)" data-tip="${d.y} · deleted codes · ${int(d.b)} services"/>`;
if ((d.y - yrs[0]) % 2 === 1 || d.y === yrs[yrs.length - 1]) s += `<text x="${X(d.y) + bw / 2}" y="${H - 8}" text-anchor="middle">${d.y}</text>`;
});
const xe = X(2023);
s += `<line x1="${xe}" x2="${xe}" y1="${T - 4}" y2="${Y(0)}" stroke="var(--foreground)" stroke-width="1.5" stroke-dasharray="4 3"/><text x="${xe - 6}" y="${T + 8}" text-anchor="end" style="fill:var(--foreground)">exposure: CY2023</text>`;
s += `<line class="ax" x1="${L}" x2="${W - R}" y1="${Y(0)}" y2="${Y(0)}"/></svg>`;
const exps = [
["CY2023 · domiciliary codes deleted and merged into the home codes", C1672],
["CY2023 · work RVUs revalued at the RUC recommendation", C1685],
["CY2021 · telehealth list paragraph naming 99334, 99335, 99347, 99348", (D.events.find((e) => e.year === 2021 && e.kind === "telehealth_list") || {}).cite],
["CY2027 · telehealth list paragraph naming the eight home codes", (D.events.find((e) => e.year === 2027 && e.kind === "telehealth_list" && e.cite && e.cite.k === "JCWARTJD") || {}).cite],
];
$('[data-view="evaluate"]').innerHTML = soonHead("Evaluate", "S4", "Pick an exposure from the calendar and an outcome series. The event study runs with control codes and shows the effect with its interval and its caveats: censoring and missing post-period years. A result can be saved as a finding.") +
`<div class="sketch"><div><div><h3 style="margin-top:0">Exposure</h3><ul class="facts">${exps.map(([t, c], i) => `<li><label><input type="radio" name="exp" ${i === 0 ? "checked" : ""} disabled> ${esc(t)}</label> ${citeA(c)}</li>`).join("")}</ul>
<h3>Outcome</h3><ul class="facts"><li><label><input type="radio" checked disabled> Medicare allowed services, national</label></li><li class="muted"><label><input type="radio" disabled> Payment and spending</label></li><li class="muted"><label><input type="radio" disabled> Workforce</label></li></ul></div>
<h3>Outcome series</h3><div class="chart-box">${s}<p class="small muted">Allowed services, national, all places of service. The outcome series only. The effect, its interval and the caveats are computed in S4; nothing here is an estimate.</p></div></div></div>`;
}
function renderPredict() {
const actions = {};
D.summaries.forEach((s) => { actions[s.action] = (actions[s.action] || 0) + 1; });
$('[data-view="predict"]').innerHTML = soonHead("Predict", "S5", "For each pending proposal that touches the family: base rates, expected stances, and this family's record of past predictions, scored. A prediction is stored with its evidence before the final rule and scored when the final rule lands.") +
`<div class="sketch"><h3 style="margin-top:0">Base rates from this family's rows</h3>
<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Rate</th><th>Value</th><th>Source</th></tr></thead><tbody>
<tr><td>CMS final equals the RUC recommendation, CY2023 round</td><td class="num">${acceptN(r23f)} of ${r23f.length} codes</td><td>${citeA(C1685)}</td></tr>
<tr><td>CMS final equals the RUC recommendation, CY2008 round</td><td class="num">${acceptN(r08f)} of ${r08f.length} codes</td><td>${citeA(C2242)}</td></tr>
<tr><td>CMS response to family comment pairs, CY${Math.min(...D.summaries.map((s) => s.cy))}–CY${Math.max(...D.summaries.map((s) => s.cy))}</td><td class="small">${Object.entries(actions).sort((a, b) => b[1] - a[1]).map(([a, n]) => `${esc(ACTION[a] || a)} ${n}`).join(" · ")}</td><td class="small muted">CMS's summaries, read by local model</td></tr>
</tbody></table></div>
<h3>Prediction record</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Proposal</th><th>Prediction</th><th>Recorded</th><th>Outcome</th><th class="r">Brier</th></tr></thead><tbody><tr><td colspan="5" class="muted">No predictions recorded. The first can be made against the CY2028 proposed rule.</td></tr></tbody></table></div>
<div class="filters" style="margin-top:12px"><button class="dis" disabled>Record a prediction</button></div></div>`;
}
function renderDraft() {
const r08 = D.rounds.find((r) => r.code === "99344" && r.cy === 2008 && r.kind === "final");
const r23 = D.rounds.find((r) => r.code === "99344" && r.cy === 2023 && r.kind === "final");
const gu = D.util.filter((u) => u.code === "G0318");
const g = (dr) => (dr.events || {})[`${M.family}:2023:G0318`] || {};
const gc = g(docketRow || {});
$('[data-view="draft"]').innerHTML = soonHead("Synthesize and draft", "S6", "Findings link claims to quantitative and qualitative evidence in a joint display. Positions build on findings; a draft for an open docket starts from them, and the local model drafts only from cited rows. The app never submits a comment.") +
`<div class="sketch"><h3 style="margin-top:0">Joint display</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Claim</th><th>Numbers</th><th>Coded text</th></tr></thead><tbody>
<tr><td><div class="ph">The analyst writes the claim</div></td><td class="small">99344 work RVU ${n2(r08 && r08.fin)} CY2008 final ${citeA(C2242)} → ${n2(r23 && r23.fin)} CY2023 final ${citeA(C1685)}</td><td class="small"><span class="pill cms">CMS's summary</span> “${esc(firstSentence(D.paras["PETM225D:1681"] || ""))}” ${citeA(C1681)}</td></tr>
<tr><td><div class="ph">The analyst writes the claim</div></td><td class="small">G0318 allowed services ${gu.map((u) => `${u.cy}: ${int(u.services)} ${srcOf(u.key) ? `<a class="cite" href="${esc(srcOf(u.key).url)}" target="_blank" rel="noopener" title="${esc(srcOf(u.key).title)}">CMS data ${u.cy}</a>` : ""}`).join(", ")}</td><td class="small"><span class="pill prelim">our count, preliminary</span> CY2023 positions on G0318: ${STANCES.map((s) => `${s} ${gc[s] || 0}`).join(", ")}</td></tr>
</tbody></table></div>
<h3>Draft checks</h3><ul class="facts"><li><span class="pill soon">S6</span> Every number and factual claim carries a citation that passes the citation check.</li><li><span class="pill soon">S6</span> American English.</li><li><span class="pill soon">S6</span> Every source relied on has full text on file.</li></ul>
<p class="small muted">The CY2027 final rule has no comment period; the next PFS venue is the CY2028 proposed rule. ${latestRule.fr ? `<a class="cite" href="${esc(latestRule.fr.html)}" target="_blank" rel="noopener">${esc(latestRule.cite)}</a>` : ""}</p></div>`;
}
function renderReview() {
const ext = (rows, f) => { const m = {}; rows.forEach((r) => { const x = f(r); m[x] = (m[x] || 0) + 1; }); return Object.entries(m).map(([k, n]) => `${esc(k)} ${n}`).join(" · "); };
const flagged = D.review.map((r) => {
const id = ev({ kind: "held for review", title: r.what, fields: [["Why held", esc(r.why)]], cites: [r.cite], extra: r.quote ? [`<blockquote>${esc(r.quote)}</blockquote>`] : [] });
return `<tr ${evAttr(id)}><td>${esc(r.what)}</td><td class="small">${esc(r.why)}</td><td>${citeA(r.cite)}</td><td style="white-space:nowrap"><button class="dis" disabled>Accept</button> <button class="dis" disabled>Correct</button> <button class="dis" disabled>Reject</button></td></tr>`;
}).join("");
const queue = [
["Comment stances", D.stances.length, ext(D.stances, (s) => s.extractor)],
["Commenter identities", D.commenters.length, ext(D.commenters, (c) => c.extractor)],
["Valuation rounds", D.rounds.length, ext(D.rounds, (r) => r.extractor)],
["Origin rows", D.origins.reduce((a, o) => a + o.n_rows, 0), ext(D.origins.flatMap((o) => Array.from({ length: o.n_rows }, () => ({ x: o.extractors.length === 1 ? o.extractors[0] : "mixed" }))), (r) => r.x)],
["CMS's summaries (quantifier, stance)", D.summaries.length, ext(D.summaries, (s) => s.extractor)],
];
$('[data-view="review"]').innerHTML = soonHead("Review queue", "S2", "Model-extracted rows that are low-confidence or not yet reviewed: stances, identities, origin rows, valuation rounds and coded cells. Accept, correct or reject; a correction is its own row and never overwrites the extracted value. The agreement rate per extractor is the double-coding check.") +
`<div class="sketch"><h3 style="margin-top:0">Held while building this preview</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Row</th><th>Why held</th><th>Source</th><th>Action</th></tr></thead><tbody>${flagged}</tbody></table></div>
<h3>Extracted rows behind this dossier</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Kind</th><th class="r">Rows</th><th>Extractor</th><th>Agreement rate</th></tr></thead><tbody>${queue.map(([k, n, x]) => `<tr><td>${k}</td><td class="r num">${n}</td><td class="small mono">${x}</td><td class="small muted">computed in S2</td></tr>`).join("")}</tbody></table></div></div>`;
}
/* ── router ────────────────────────────────────────────────────── */
const ALL = VIEWS.flatMap(([, vs]) => vs.map(([id]) => id));
function route() {
const h = (location.hash || "").replace(/^#/, "");
const view = ALL.includes(h) ? h : "dossier";
$$(".view").forEach((s) => { s.hidden = s.dataset.view !== view; });
$$("#tabs a").forEach((a) => { a.classList.toggle("on", a.dataset.tab === view); if (a.dataset.tab === view) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current"); });
renderMast(view);
if (view === "payment") {
const end = () => { const sc = $("#pay-scroll"); if (sc) sc.scrollLeft = sc.scrollWidth; };
end(); requestAnimationFrame(end);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(end);
}
const on = $(`#tabs a[data-tab="${view}"]`);
if (on && on.scrollIntoView && h) on.scrollIntoView({ block: "nearest", inline: "nearest" });
}
/* ── theme: light by default; the fhirworx dark palette on request ── */
const themeBtn = $("#theme-btn");
function syncTheme() {
const dark = document.documentElement.getAttribute("data-theme") === "dark";
themeBtn.setAttribute("aria-pressed", String(dark));
themeBtn.title = dark ? "Switch to the fhirworx light palette" : "Switch to the fhirworx dark palette";
themeBtn.querySelector("use").setAttribute("href", dark ? "#i-sun" : "#i-moon");
themeBtn.querySelector("span").textContent = dark ? "Light" : "Dark";
}
themeBtn.addEventListener("click", () => {
const dark = document.documentElement.getAttribute("data-theme") !== "dark";
if (dark) document.documentElement.setAttribute("data-theme", "dark"); else document.documentElement.removeAttribute("data-theme");
try { localStorage.setItem("lex-theme", dark ? "dark" : "light"); } catch (e) {}
syncTheme();
});
syncTheme();
buildTimeline();
renderRail();
renderTabs();
renderContents();
renderDossier();
renderTimeline();
renderValuation();
renderPayment();
renderPolitics();
renderSources();
renderGrid();
renderEvaluate();
renderPredict();
renderDraft();
renderReview();
showEvidence(ev({ kind: "key paragraph", title: "The RUC's CY2023 recommendation for the home or residence codes", cites: [C1676] }), null);
if (window.matchMedia("(max-width: 1180px)").matches) { drawer.classList.remove("open"); document.body.classList.remove("drawer-open"); }
window.addEventListener("hashchange", route);
route();
})();
</script>
</body>
</html>