filegrc 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/web.js CHANGED
@@ -94,6 +94,7 @@ let repositorySyncPollTimer = null;
94
94
  let repositorySyncPollInFlight = false;
95
95
  let mutationStateRefreshInFlight = false;
96
96
  let mutationStateRefreshTimer = null;
97
+ let programSelectionGeneration = 0;
97
98
 
98
99
  start().catch((error) => {
99
100
  root.innerHTML = '<main class="fatal"><h1>Could Not Load the Workspace</h1><pre></pre></main>';
@@ -197,7 +198,8 @@ function loadStateSection(section) {
197
198
  if (stateSectionRequests.has(section)) return stateSectionRequests.get(section);
198
199
  state.sections[section] = "loading";
199
200
  const token = state.stateToken;
200
- const request = fetchJson("/api/state/" + encodeURIComponent(section) + "?token=" + encodeURIComponent(token))
201
+ const programQuery = state.selectedProgramId ? "&programId=" + encodeURIComponent(state.selectedProgramId) : "";
202
+ const request = fetchJson("/api/state/" + encodeURIComponent(section) + "?token=" + encodeURIComponent(token) + programQuery)
201
203
  .then((result) => {
202
204
  if (state.stateToken !== result.stateToken) return;
203
205
  Object.assign(state, result.state);
@@ -300,7 +302,10 @@ function readinessStageForType(type) {
300
302
  }
301
303
 
302
304
  function activeProgram() {
303
- return resourcesOfType("program").find(({ record }) => !["retired"].includes(record.status))?.record || state.workspace;
305
+ return resourcesOfType("program").find(({ record }) => record.id === state.selectedProgramId)
306
+ ?.record
307
+ || resourcesOfType("program").find(({ record }) => !["retired"].includes(record.status))?.record
308
+ || state.workspace;
304
309
  }
305
310
 
306
311
  function reviewedRequirementIds() {
@@ -353,7 +358,11 @@ function topbar(route) {
353
358
  const validationLoading = state.sections?.repository !== "complete";
354
359
  const validationTone = repositoryError ? "warn" : validationLoading ? "neutral" : state.validation.ok ? "good" : "bad";
355
360
  const validationLabel = repositoryError ? "Data check failed" : validationLoading ? "Checking data" : state.validation.ok ? "Data valid" : state.validation.counts.errors + " validation errors";
356
- return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><div class="topbar-status">' + topbarProgramReadiness() + '<label class="search topbar-search"><span aria-hidden="true">⌕</span><input data-global-search type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><a class="validation-chip" href="#/repository"><span class="status-dot ' + validationTone + '"></span>' + validationLabel + '</a><a class="repo-chip" href="#/repository"><span class="status-dot ' + repositoryTone + '"></span>' + esc(repositoryLabel) + '</a></div>';
361
+ const programs = resourcesOfType("program").filter(({ record }) => record.status !== "retired");
362
+ const programSelect = programs.length > 1 && !state.readOnly
363
+ ? '<label class="program-select"><span class="sr-only">Current Program</span><select data-program-select>' + programs.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === state.selectedProgramId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label>'
364
+ : "";
365
+ return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><div class="topbar-status">' + programSelect + topbarProgramReadiness() + '<label class="search topbar-search"><span aria-hidden="true">⌕</span><input data-global-search type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><a class="validation-chip" href="#/repository"><span class="status-dot ' + validationTone + '"></span>' + validationLabel + '</a><a class="repo-chip" href="#/repository"><span class="status-dot ' + repositoryTone + '"></span>' + esc(repositoryLabel) + '</a></div>';
357
366
  }
358
367
 
359
368
  function topbarProgramReadiness() {
@@ -1270,8 +1279,10 @@ function stageProgress(stage) {
1270
1279
  detail: "Create an Audit only after a real engagement or customer deadline exists."
1271
1280
  };
1272
1281
  }
1273
- const complete = pages.filter((destination) => derivedStagePageState(stage, destination).complete).length;
1274
- return progressFromCounts(complete, pages.length, "page");
1282
+ const pageStates = pages.map((destination) => derivedStagePageState(stage, destination));
1283
+ const applicable = pageStates.filter((current) => current.countsTowardProgress !== false);
1284
+ const complete = applicable.filter((current) => current.complete).length;
1285
+ return progressFromCounts(complete, applicable.length, "page");
1275
1286
  }
1276
1287
 
1277
1288
  function derivedStagePageState(stage, destination) {
@@ -1282,24 +1293,29 @@ function derivedStagePageState(stage, destination) {
1282
1293
  ) {
1283
1294
  return { complete: false, label: "No engagement" };
1284
1295
  }
1285
- const blocking = stagePageItems(stage, destination);
1296
+ const items = stagePageItems(stage, destination);
1297
+ const deferredStates = new Set(["later", "scheduled", "upcoming", "waiting-external"]);
1298
+ const blocking = items.filter(({ state }) => !deferredStates.has(state));
1286
1299
  if (blocking.length) {
1287
1300
  return { complete: false, label: blocking.length + " " + pluralize("item", blocking.length) + (blocking.length === 1 ? " needs work" : " need work") };
1288
1301
  }
1302
+ if (items.some(({ state }) => deferredStates.has(state))) {
1303
+ return { complete: false, countsTowardProgress: false, label: "Later" };
1304
+ }
1305
+ const collectionReview = destination.type ? state.collectionReviews?.[destination.type] : null;
1306
+ if (collectionReview) {
1307
+ return collectionReview.status === "current"
1308
+ ? { complete: true, label: "Reviewed" }
1309
+ : { complete: false, label: "Review scope" };
1310
+ }
1289
1311
  if (destination.type && resourcesOfType(destination.type).length === 0) {
1290
- const collectionReview = state.collectionReviews?.[destination.type];
1291
- if (collectionReview) {
1292
- return collectionReview.status === "current"
1293
- ? { complete: true, label: "Reviewed" }
1294
- : { complete: false, label: "Review scope" };
1295
- }
1296
- return { complete: true, label: "Conditional" };
1312
+ return { complete: false, countsTowardProgress: false, label: "Only if needed" };
1297
1313
  }
1298
1314
  return { complete: true, label: "Ready" };
1299
1315
  }
1300
1316
 
1301
1317
  function stagePageItems(stage, destination) {
1302
- const activeStates = new Set(["blocked", "due", "open", "overdue", "ready"]);
1318
+ const activeStates = new Set(["blocked", "due", "later", "open", "overdue", "ready", "scheduled", "upcoming", "waiting-external"]);
1303
1319
  const items = [
1304
1320
  ...(state.workflow?.findings || []),
1305
1321
  ...(state.workflow?.workItems || []),
@@ -1541,6 +1557,9 @@ function renderObligations(main, params = new URLSearchParams()) {
1541
1557
  const trigger = plan.triggers.find((item) => item.eventType === button.dataset.startEvent);
1542
1558
  if (trigger) openObligationEventDialog(trigger);
1543
1559
  }));
1560
+ main.querySelectorAll("[data-activate-obligation-rule]").forEach((button) => button.addEventListener("click", () => {
1561
+ openObligationRuleActivation(button.dataset.activateObligationRule);
1562
+ }));
1544
1563
  main.querySelectorAll("[data-expand-policy-events]").forEach((button) => button.addEventListener("click", (event) => {
1545
1564
  const button = event.currentTarget;
1546
1565
  const expanded = button.getAttribute("aria-expanded") === "true";
@@ -1566,6 +1585,10 @@ function renderObligations(main, params = new URLSearchParams()) {
1566
1585
  const item = plan.items.find((candidate) => candidate.key === button.dataset.recordObligation);
1567
1586
  if (item) openObligationCompletion(item);
1568
1587
  }));
1588
+ main.querySelectorAll("[data-reconcile-obligation]").forEach((button) => button.addEventListener("click", () => {
1589
+ const item = plan.items.find((candidate) => candidate.key === button.dataset.reconcileObligation);
1590
+ if (item) openObligationReconciliation(item);
1591
+ }));
1569
1592
  main.querySelectorAll("[data-complete-action]").forEach((button) => button.addEventListener("click", () => {
1570
1593
  const item = plan.items.find((candidate) => candidate.key === button.dataset.completeAction);
1571
1594
  if (item) openActionCompletion(item);
@@ -1642,7 +1665,11 @@ function policyEventTrigger(trigger, index, collapsed = false, scope = "events")
1642
1665
  : state.readOnly
1643
1666
  ? "Open this workspace in writable mode to trigger the workflow."
1644
1667
  : trigger.steps.length + " " + pluralize("task", trigger.steps.length) + " will be added to the Work Queue.";
1645
- return '<article class="policy-event-row"' + (collapsed ? " data-collapsed hidden" : "") + '><div class="policy-event-name"><div class="policy-event-title"><strong>' + esc(policyEventName(trigger.eventType)) + '</strong><span class="policy-event-guide"><button class="guide-trigger policy-event-guide-trigger" type="button" aria-label="Show ' + esc(policyEventName(trigger.eventType)) + ' workflow steps" aria-describedby="' + tooltipId + '"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button><div class="policy-event-tooltip" id="' + tooltipId + '" role="tooltip"><strong>' + esc(proposed ? "Proposed workflow" : "Adds " + trigger.steps.length + " " + pluralize("task", trigger.steps.length) + " to the Work Queue") + '</strong><ol>' + trigger.steps.map((step) => '<li><span>' + esc(step.title) + '</span><small>' + esc(eventStepSummary(step)) + '</small></li>').join("") + '</ol></div></span></div><small>' + esc(availability) + '</small></div><button class="button primary" type="button" data-start-event="' + esc(trigger.eventType) + '"' + (unavailable ? " disabled" : "") + '>Trigger Work</button></article>';
1668
+ const proposedRule = trigger.steps.find(({ ruleStatus }) => ["proposed", "approved"].includes(ruleStatus));
1669
+ const action = proposedRule && !state.readOnly
1670
+ ? '<button class="button primary" type="button" data-activate-obligation-rule="' + esc(proposedRule.ruleId) + '">Review schedule</button>'
1671
+ : '<button class="button primary" type="button" data-start-event="' + esc(trigger.eventType) + '"' + (unavailable ? " disabled" : "") + '>Trigger Work</button>';
1672
+ return '<article class="policy-event-row"' + (collapsed ? " data-collapsed hidden" : "") + '><div class="policy-event-name"><div class="policy-event-title"><strong>' + esc(policyEventName(trigger.eventType)) + '</strong><span class="policy-event-guide"><button class="guide-trigger policy-event-guide-trigger" type="button" aria-label="Show ' + esc(policyEventName(trigger.eventType)) + ' workflow steps" aria-describedby="' + tooltipId + '"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button><div class="policy-event-tooltip" id="' + tooltipId + '" role="tooltip"><strong>' + esc(proposed ? "Proposed workflow" : "Adds " + trigger.steps.length + " " + pluralize("task", trigger.steps.length) + " to the Work Queue") + '</strong><ol>' + trigger.steps.map((step) => '<li><span>' + esc(step.title) + '</span><small>' + esc(eventStepSummary(step)) + '</small></li>').join("") + '</ol></div></span></div><small>' + esc(availability) + '</small></div>' + action + '</article>';
1646
1673
  }
1647
1674
 
1648
1675
  function policyEventName(eventType) {
@@ -1673,20 +1700,32 @@ function defaultClassificationId() {
1673
1700
  }
1674
1701
 
1675
1702
  function obligationCard(item, collapsed = false) {
1676
- const type = item.actionItemId ? "action-item" : "obligation";
1677
- const id = item.actionItemId || item.obligationId;
1703
+ const type = item.actionItemId ? "action-item" : item.occurrenceId ? "obligation-occurrence" : "obligation";
1704
+ const id = item.actionItemId || item.occurrenceId || item.obligationId;
1678
1705
  const completion = item.actionItemId ? actionCompletionPlan(item) : obligationCompletionPlan(item);
1706
+ const proposedAction = item.status === "proposed" && item.ruleId && ["proposed", "approved"].includes(item.ruleStatus)
1707
+ ? state.readOnly
1708
+ ? '<a class="obligation-action" href="#/resource/obligation-rule/' + encodeURIComponent(item.ruleId) + '">Review schedule</a>'
1709
+ : '<button class="obligation-action" type="button" data-activate-obligation-rule="' + esc(item.ruleId) + '">Review and activate</button>'
1710
+ : item.status === "proposed" && item.programBlocker?.id
1711
+ ? '<a class="obligation-action blocked" href="#/resource/' + encodeURIComponent(item.programBlocker.type) + '/' + encodeURIComponent(item.programBlocker.id) + '">' + esc(item.programBlocker.label) + '</a>'
1712
+ : "";
1679
1713
  const canAct = completion?.blocked === "Assign current owner"
1680
1714
  || !["upcoming", "proposed"].includes(item.status);
1681
- const action = !state.readOnly && canAct && completion
1715
+ const action = proposedAction || (!state.readOnly && canAct && completion
1682
1716
  ? completion.blocked
1683
1717
  ? '<a class="obligation-action blocked" href="' + completion.href + '">' + esc(completion.blocked) + '</a>'
1684
1718
  : item.actionItemId
1685
1719
  ? '<button class="obligation-action" type="button" data-complete-action="' + esc(item.key) + '">Complete task</button>'
1686
- : '<button class="obligation-action" type="button" data-record-obligation="' + esc(item.key) + '">Record work</button>'
1687
- : "";
1720
+ : item.ruleId
1721
+ ? '<button class="obligation-action" type="button" data-reconcile-obligation="' + esc(item.key) + '">' + (item.reconciliationStatus === "reconciled" ? "Correct result" : "Review population") + '</button>'
1722
+ : '<button class="obligation-action" type="button" data-record-obligation="' + esc(item.key) + '">Record work</button>'
1723
+ : "");
1688
1724
  const kind = item.kind === "event" ? "Policy Event Task" : item.kind === "action" ? "Assigned Follow-up" : properCase(item.activityType || "Recurring");
1689
- return '<article class="obligation-card status-' + esc(item.status) + '" data-work-source="' + esc(type + ":" + id) + '"' + (collapsed ? ' data-collapsed hidden' : "") + '><div class="obligation-card-head"><span>' + esc(kind) + '</span><strong>' + esc(timingText(item)) + '</strong></div><h3><a href="#/resource/' + type + '/' + encodeURIComponent(id) + '">' + esc(titleCase(item.title)) + '</a></h3><p>' + esc(windowText(item)) + '</p><div class="obligation-card-foot"><div class="obligation-links">' + (item.policyIds || []).map(formatReference).join("") + '</div>' + action + '</div></article>';
1725
+ const population = item.ruleId
1726
+ ? '<p><strong>' + esc(String(item.completedCount || 0)) + ' of ' + esc(String(item.expectedCount || 0)) + '</strong> expected members passed' + (item.membershipFinal ? '' : '; population still open') + '.</p>'
1727
+ : '';
1728
+ return '<article class="obligation-card status-' + esc(item.status) + '" data-work-source="' + esc(type + ":" + id) + '"' + (collapsed ? ' data-collapsed hidden' : "") + '><div class="obligation-card-head"><span>' + esc(kind) + '</span><strong>' + esc(timingText(item)) + '</strong></div><h3><a href="#/resource/' + type + '/' + encodeURIComponent(id) + '">' + esc(titleCase(item.title)) + '</a></h3><p>' + esc(windowText(item)) + '</p>' + population + '<div class="obligation-card-foot"><div class="obligation-links">' + (item.policyIds || []).map(formatReference).join("") + '</div>' + action + '</div></article>';
1690
1729
  }
1691
1730
 
1692
1731
  function actionCompletionPlan(item) {
@@ -1773,6 +1812,103 @@ function openObligationCompletion(item) {
1773
1812
  });
1774
1813
  }
1775
1814
 
1815
+ async function openObligationReconciliation(item) {
1816
+ try {
1817
+ const response = await localFetch("/api/obligation-occurrences/scaffold", {
1818
+ method: "POST",
1819
+ headers: { "content-type": "application/json" },
1820
+ body: JSON.stringify({
1821
+ obligationId: item.obligationId,
1822
+ windowStart: item.dueWindowStart,
1823
+ asOf: currentDate(),
1824
+ correctFinalized: item.reconciliationStatus === "reconciled"
1825
+ })
1826
+ });
1827
+ const scaffold = await response.json();
1828
+ if (!response.ok) throw new Error(scaffold.error || "Could not scaffold the occurrence reconciliation.");
1829
+ const current = scaffold.operation === "update"
1830
+ ? state.resources.find(({ record }) => record.id === scaffold.record.id)
1831
+ : null;
1832
+ const entry = current ? { ...current, record: scaffold.record } : null;
1833
+ openEditor("obligation-occurrence", entry, {
1834
+ seed: entry ? null : scaffold.record,
1835
+ description: "Review each member and its proof. Counts and the conclusion are calculated from the rows below.",
1836
+ occurrenceReview: true,
1837
+ membershipFinal: scaffold.membershipFinal,
1838
+ occurrenceReconciliation: { revision: scaffold.revision },
1839
+ saveLabel: scaffold.operation === "update" ? "Save reconciliation" : scaffold.operation === "supersede" ? "Create correction" : "Create population review"
1840
+ });
1841
+ } catch (error) {
1842
+ showError(error.message);
1843
+ }
1844
+ }
1845
+
1846
+ async function openObligationRuleActivation(ruleId) {
1847
+ try {
1848
+ const response = await localFetch("/api/obligation-rule-activations/scaffold", {
1849
+ method: "POST",
1850
+ headers: { "content-type": "application/json" },
1851
+ body: JSON.stringify({ ruleId })
1852
+ });
1853
+ const scaffold = await response.json();
1854
+ if (!response.ok) throw new Error(scaffold.error || "Could not prepare this schedule for activation.");
1855
+ const dialog = document.createElement("dialog");
1856
+ dialog.className = "editor rule-activation";
1857
+ const recurrence = scaffold.review.recurrence;
1858
+ const selector = scaffold.review.selector;
1859
+ const scheduleReview = recurrence.mode === "event"
1860
+ ? '<div><span>Trigger</span><strong>' + esc(policyEventName(recurrence.eventType)) + '</strong><small>Applies to events after the effective time</small></div>'
1861
+ : '<div><span>Cadence</span><strong>Every ' + esc(String(recurrence.interval)) + ' ' + esc(recurrence.unit) + (recurrence.interval === 1 ? "" : "s") + '</strong><small>Anchored ' + esc(recurrence.anchorDate) + ' · first affected <b data-first-affected>' + esc(scaffold.review.firstAffectedOn || "none") + '</b></small></div>';
1862
+ const reviewSummary = '<section class="activation-review">' + scheduleReview + '<div><span>Population</span><strong>' + esc(selector ? properCase(selector.resourceType) : "Obligation scope") + '</strong><small>' + esc(selector ? selector.cutoff && "cutoff " + properCase(selector.cutoff) : "No selector") + '</small></div><div class="full"><span>Rationale</span><strong>' + esc(scaffold.review.rationale) + '</strong></div></section>';
1863
+ const cutoverOption = (value, label) => '<option value="' + value + '" ' + (scaffold.payload.cutoverDecision === value ? "selected" : "") + '>' + label + '</option>';
1864
+ dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Obligation schedule</p><h2>' + esc(scaffold.rule.title) + '</h2></div><button type="button" class="icon-button" data-dismiss aria-label="Close">×</button></div><p>Activation binds this reviewed schedule to ' + esc(scaffold.obligation.title) + ' in one write. <a href="#/resource/obligation-rule/' + encodeURIComponent(scaffold.rule.id) + '">Open the full rule</a>.</p>' + reviewSummary + (scaffold.openOccurrences.length ? '<p class="cutover-note">' + scaffold.openOccurrences.length + ' open ' + pluralize("occurrence", scaffold.openOccurrences.length) + ' need a cutover decision.</p>' : "") + '<div class="form-grid"><label class="field-group"><span>Approved on <span class="required-mark">Required</span></span><input name="approvedOn" type="date" required value="' + esc(scaffold.payload.approvedOn) + '"></label><label class="field-group"><span>Effective at <span class="required-mark">Required</span></span><input name="effectiveAt" type="datetime-local" required value="' + esc(scaffold.payload.effectiveLocal) + '"></label>' + (scaffold.priorRule ? '<label class="field-group"><span>Cutover</span><select name="cutoverDecision">' + cutoverOption("new-windows-only", "New windows only") + cutoverOption("keep-open-window", "Keep open occurrences") + cutoverOption("supersede-open-window", "Supersede open occurrences") + '</select></label>' : "") + '<fieldset class="field-group full"><legend>Approved by <span class="required-mark">Required</span></legend><div class="checkbox-list">' + scaffold.reviewerCandidates.map((person) => '<label><input type="checkbox" name="approver" value="' + esc(person.id) + '"><span>' + esc(person.title) + '<small>' + esc(person.id) + '</small></span></label>').join("") + '</div></fieldset><label class="field-group full confirmation"><input type="checkbox" name="confirmRevision" required><span>I reviewed revision <code>' + esc(scaffold.review.revision.slice(0, 12)) + '</code> and approve these terms.</span></label></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><button type="button" class="button" data-dismiss>Cancel</button><button type="submit" class="button primary">Activate schedule</button></div></form>';
1865
+ document.body.append(dialog);
1866
+ dialog.showModal();
1867
+ dialog.addEventListener("close", () => dialog.remove());
1868
+ dialog.querySelectorAll("[data-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
1869
+ const effectiveInput = dialog.querySelector('input[name="effectiveAt"]');
1870
+ effectiveInput.addEventListener("input", () => {
1871
+ const target = dialog.querySelector("[data-first-affected]");
1872
+ if (target) target.textContent = effectiveInput.value ? nextCalendarOccurrence(recurrence, effectiveInput.value.slice(0, 10)) : "none";
1873
+ });
1874
+ dialog.querySelector("form").addEventListener("submit", async (event) => {
1875
+ event.preventDefault();
1876
+ const form = event.currentTarget;
1877
+ const error = dialog.querySelector(".dialog-error");
1878
+ const approvedByIds = [...form.querySelectorAll('input[name="approver"]:checked')].map(({ value }) => value);
1879
+ if (!approvedByIds.length) return void (error.textContent = "Select at least one approver.");
1880
+ const button = form.querySelector('button[type="submit"]');
1881
+ button.disabled = true;
1882
+ button.textContent = "Activating…";
1883
+ error.textContent = "";
1884
+ try {
1885
+ const saved = await localFetch("/api/obligation-rule-activations", {
1886
+ method: "POST",
1887
+ headers: { "content-type": "application/json" },
1888
+ body: JSON.stringify({
1889
+ ...scaffold.payload,
1890
+ confirmedRevision: scaffold.review.revision,
1891
+ approvedByIds,
1892
+ approvedOn: form.elements.approvedOn.value,
1893
+ effectiveLocal: form.elements.effectiveAt.value,
1894
+ ...(form.elements.cutoverDecision ? { cutoverDecision: form.elements.cutoverDecision.value } : {})
1895
+ })
1896
+ });
1897
+ if (!saved.ok) throw new Error(await responseMessage(saved));
1898
+ applyMutationState(await saved.json());
1899
+ dialog.close();
1900
+ render();
1901
+ } catch (caught) {
1902
+ error.textContent = caught.message;
1903
+ button.disabled = false;
1904
+ button.textContent = "Activate schedule";
1905
+ }
1906
+ });
1907
+ } catch (error) {
1908
+ showError(error.message);
1909
+ }
1910
+ }
1911
+
1776
1912
  function obligationCompletionSeed(type, item, obligation) {
1777
1913
  const date = currentDate();
1778
1914
  const timestamp = new Date().toISOString();
@@ -2172,7 +2308,9 @@ function openApplicabilityReviewDialog(type, entries) {
2172
2308
  }
2173
2309
 
2174
2310
  function renderAuditPacket(main, params = new URLSearchParams()) {
2175
- const audits = resourcesOfType("audit");
2311
+ const audits = resourcesOfType("audit").filter(({ record }) => (
2312
+ !state.selectedProgramId || !record.programId || record.programId === state.selectedProgramId
2313
+ ));
2176
2314
  const evidence = resourcesOfType("evidence");
2177
2315
  const filegrcRecordTypes = new Set((state.model.evidenceSourceFamilies || [])
2178
2316
  .filter((family) => family.filegrcManaged === true)
@@ -2242,7 +2380,8 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
2242
2380
  body: JSON.stringify({
2243
2381
  start: form.elements.start.value,
2244
2382
  end: form.elements.end?.value || form.elements.start.value,
2245
- auditId: form.elements.auditId.value || undefined
2383
+ auditId: form.elements.auditId.value || undefined,
2384
+ programId: state.selectedProgramId || undefined
2246
2385
  })
2247
2386
  });
2248
2387
  if (!response.ok) throw new Error(await responseMessage(response));
@@ -2416,7 +2555,8 @@ function renderList(main, type, params = new URLSearchParams()) {
2416
2555
  const guideTrigger = '<button class="guide-trigger" id="resource-guide-trigger" type="button" aria-label="About ' + esc(definition.pluralTitle) + '" aria-haspopup="dialog" aria-controls="resource-guide" aria-expanded="false"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button>';
2417
2556
  const listTools = '<div class="list-tools list-header-tools"><label><span class="sr-only">Filter list</span><input id="list-search" type="search" placeholder="Filter ' + esc(definition.pluralTitle.toLowerCase()) + '"></label>' +
2418
2557
  filters.map(({ name, label, values }) => '<select class="field-filter" data-field="' + esc(name) + '" aria-label="Filter by ' + esc(label.toLowerCase()) + '"><option value="">Any ' + esc(properCase(label)) + '</option>' + values.map((value) => '<option value="' + esc(value) + '">' + esc(filterOptionLabel(value)) + '</option>').join("") + '</select>').join("") + '<span id="result-count" aria-live="polite">' + entries.length + ' records</span>' + applicabilityButton + createButton + '</div>';
2419
- main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">' + esc(listStage?.title || groupTitle(definition.group)) + '</p><div class="page-title-line"><h2>' + esc(titleCase(listTitle)) + '</h2>' + guideTrigger + '</div></div>' + listTools + '</div>' + resourceGuide(type) +
2558
+ const pageSummary = STAGE_PAGE_SUMMARIES[type] || definition.description;
2559
+ main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">' + esc(listStage?.title || groupTitle(definition.group)) + '</p><div class="page-title-line"><h2>' + esc(titleCase(listTitle)) + '</h2>' + guideTrigger + '</div><p>' + esc(pageSummary) + '</p></div>' + listTools + '</div>' + resourceGuide(type) +
2420
2560
  collectionReviewPanel(type) +
2421
2561
  '<section class="record-table-wrap"><table class="record-table"><thead><tr><th>' + esc(fieldLabel(type, "title")) + '</th>' + fields.map((name) => '<th>' + esc(fieldLabel(type, name)) + '</th>').join("") + '<th>Next action</th><th>Git file</th></tr></thead><tbody id="record-rows"></tbody></table></section>' +
2422
2562
  '<nav class="pagination list-pagination" aria-label="' + esc(definition.pluralTitle) + ' pages" hidden><button class="button" type="button" data-page="previous">Previous</button><span class="page-status" aria-live="polite"></span><button class="button" type="button" data-page="next">Next</button></nav></div>';
@@ -2573,6 +2713,21 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2573
2713
  && entry.record.status === "complete"
2574
2714
  ? '<button class="button primary" type="button" data-next-audit-cycle>' + (entry.record.auditKind === "soc-2-type-1" ? "Start Type 2 period" : "Start next cycle") + '</button>'
2575
2715
  : "";
2716
+ const auditPopulationCorrectionAction = !state.readOnly
2717
+ && type === "audit-population"
2718
+ && ["reconciled", "not-applicable"].includes(entry.record.status)
2719
+ ? '<button class="button" type="button" data-correct-audit-population>Correct population</button>'
2720
+ : "";
2721
+ const reportingRouteSetActions = !state.readOnly && type === "reporting-route-set"
2722
+ ? entry.record.status === "draft"
2723
+ ? '<button class="button primary" type="button" data-propose-route-set>' + (state.repository?.mode === "trunk" && !state.repository?.developmentOverride ? "Commit proposal" : "Propose (commit required)") + '</button>'
2724
+ : entry.record.status === "proposed"
2725
+ ? '<button class="button primary" type="button" data-approve-route-set ' + (!state.git.clean || !state.git.commit ? 'disabled title="Commit the proposal before approval"' : "") + '>' + (state.repository?.mode === "trunk" && !state.repository?.developmentOverride ? "Approve and commit" : "Approve (commit required)") + '</button>'
2726
+ : entry.record.status === "approved"
2727
+ ? '<button class="button" type="button" data-cancel-route-set>Cancel route set</button>'
2728
+ : ""
2729
+ : "";
2730
+ const routeSetLocked = type === "reporting-route-set" && entry.record.status !== "draft";
2576
2731
  const detailMain = hasRecordBody
2577
2732
  ? '<section class="panel detail-main">' + narrativeContent + markdownContent + addRecordContent + '</section>'
2578
2733
  : "";
@@ -2590,10 +2745,33 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2590
2745
  connectionsPanel: resourceConnections(entry),
2591
2746
  historyPanel
2592
2747
  });
2593
- main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + auditCycleAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence &amp; Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly ? '<button class="button" id="edit-resource">Edit</button>' + (!definition.singleton ? '<button class="button danger" id="delete-resource">Delete</button>' : "") : "") + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + '">' + detailMain + supportPanels + '</div></div>';
2748
+ main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + reportingRouteSetActions + auditCycleAction + auditPopulationCorrectionAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence &amp; Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly && !routeSetLocked ? '<button class="button" id="edit-resource">Edit</button>' + (!definition.singleton ? '<button class="button danger" id="delete-resource">Delete</button>' : "") : "") + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + '">' + detailMain + supportPanels + '</div></div>';
2594
2749
  main.querySelector("#edit-resource")?.addEventListener("click", () => openEditor(type, entry));
2595
2750
  main.querySelector("[data-external-reviewer-governance]")?.addEventListener("click", openExternalReviewerGovernanceDialog);
2596
2751
  main.querySelector("[data-next-audit-cycle]")?.addEventListener("click", () => openNextAuditCycleDialog(entry.record));
2752
+ main.querySelector("[data-correct-audit-population]")?.addEventListener("click", async () => {
2753
+ try {
2754
+ const scaffold = await fetchJson("/api/audit-population-corrections/scaffold", {
2755
+ method: "POST",
2756
+ headers: { "content-type": "application/json" },
2757
+ body: JSON.stringify({ populationId: entry.record.id })
2758
+ });
2759
+ openEditor("audit-population", null, {
2760
+ seed: scaffold.record,
2761
+ auditPopulationCorrection: scaffold,
2762
+ saveLabel: "Save correction",
2763
+ description: scaffold.instructions
2764
+ });
2765
+ } catch (error) {
2766
+ showError(error.message);
2767
+ }
2768
+ });
2769
+ main.querySelector("[data-propose-route-set]")?.addEventListener("click", () => mutateReportingRouteSet("propose", {
2770
+ routeSetId: entry.record.id,
2771
+ expectedRevision: entry.revision
2772
+ }));
2773
+ main.querySelector("[data-approve-route-set]")?.addEventListener("click", () => openReportingRouteApproval(entry));
2774
+ main.querySelector("[data-cancel-route-set]")?.addEventListener("click", () => openReportingRouteCancellation(entry));
2597
2775
  main.querySelector("[data-record-finding]")?.addEventListener("click", () => openEditor("finding", null, {
2598
2776
  seed: issueSeed("finding", entry.record),
2599
2777
  description: "Record only a confirmed gap that needs separate remediation tracking. Keep the report details in this source record’s Markdown."
@@ -2925,7 +3103,7 @@ function renderRepository(main) {
2925
3103
  ? "Add a Git remote before pushing"
2926
3104
  : "";
2927
3105
  const pullButton = !state.readOnly && state.git.available && hasRemote
2928
- ? '<button class="button" type="button" data-git-action="pull" ' + (pullDisabled ? 'disabled title="' + esc(pullDisabled) + '"' : "") + '>Pull with rebase</button>'
3106
+ ? '<button class="button" type="button" data-git-action="pull" ' + (pullDisabled ? 'disabled title="' + esc(pullDisabled) + '"' : "") + '>Check incoming commits</button>'
2929
3107
  : "";
2930
3108
  const commitButton = !state.readOnly && state.git.available && !state.git.clean
2931
3109
  ? '<button class="button primary" type="button" id="commit-workspace" ' + (!state.git.branch
@@ -2938,7 +3116,7 @@ function renderRepository(main) {
2938
3116
  const repositoryInstructions = !state.git.branch
2939
3117
  ? "This workspace is on a detached HEAD. Check out a branch before using browser commit, pull, or push."
2940
3118
  : hasRemote
2941
- ? "Pull remote changes with rebase, review the workspace diff, then commit and push together."
3119
+ ? "Fetch remote refs without changing this branch. If commits are incoming, integrate them with Git, then reload FileGRC."
2942
3120
  : "Review the workspace diff, then commit it locally. Add a Git remote when you want browser pull and push.";
2943
3121
  const validationBody = state.validation.diagnostics.length
2944
3122
  ? '<div class="diagnostics">' + state.validation.diagnostics.map((item) => '<div><span class="badge ' + item.severity + '">' + esc(properCase(item.severity)) + '</span><code>' + esc(item.path) + '</code><p>' + esc(item.message) + '</p></div>').join("") + '</div>'
@@ -2996,14 +3174,14 @@ async function runRepositoryGitAction(action) {
2996
3174
  const currentStatus = document.querySelector(".repository-sync-status");
2997
3175
  if (currentStatus) currentStatus.textContent = action === "pull"
2998
3176
  ? result.updated
2999
- ? "Pulled " + result.upstream + " with rebase at " + result.shortCommit + "."
3177
+ ? "No incoming commits from " + result.upstream + ". This branch remains at " + result.shortCommit + "."
3000
3178
  : result.branch + " is current with " + result.upstream + "."
3001
3179
  : action === "retry-sync"
3002
3180
  ? "Synchronized " + result.shortCommit + " with " + result.upstream + "."
3003
3181
  : "Pushed " + result.shortCommit + " to " + result.upstream + ".";
3004
3182
  } catch (cause) {
3005
3183
  buttons.forEach((button, index) => { button.disabled = disabled[index]; });
3006
- if (active) active.textContent = action === "pull" ? "Pull with rebase" : action === "retry-sync" ? "Retry sync" : "Push";
3184
+ if (active) active.textContent = action === "pull" ? "Check incoming commits" : action === "retry-sync" ? "Retry sync" : "Push";
3007
3185
  if (status) {
3008
3186
  status.textContent = cause.message;
3009
3187
  status.classList.add("error");
@@ -3060,6 +3238,169 @@ function openCommitDialog() {
3060
3238
  dialog.querySelector('input[name="message"]').focus();
3061
3239
  }
3062
3240
 
3241
+ async function mutateReportingRouteSet(action, payload, dialog) {
3242
+ try {
3243
+ if (dialog) setMutationBusy(dialog, true, action === "approve" ? "Approving…" : action === "cancel" ? "Canceling…" : "Proposing…");
3244
+ const response = await localFetch("/api/reporting-route-sets/" + action, {
3245
+ method: "POST",
3246
+ headers: { "content-type": "application/json" },
3247
+ body: JSON.stringify(payload)
3248
+ });
3249
+ if (!response.ok) throw new Error(await responseMessage(response));
3250
+ applyMutationState(await response.json());
3251
+ dialog?.close();
3252
+ render();
3253
+ } catch (error) {
3254
+ if (dialog) {
3255
+ setMutationBusy(dialog, false, "");
3256
+ dialog.querySelector(".dialog-error").textContent = error.message;
3257
+ } else showError(error.message);
3258
+ }
3259
+ }
3260
+
3261
+ function reportingRouteAppointmentOptions(entry) {
3262
+ return resourcesOfType("appointment").filter(({ record }) => (
3263
+ ["active", "ended"].includes(record.status)
3264
+ && record.holderId
3265
+ && record.appointmentKind === entry.record.approvalAppointmentKind
3266
+ && record.scopeResourceIds?.some((id) => [entry.record.id, entry.record.programId, state.workspace.id].includes(id))
3267
+ ));
3268
+ }
3269
+
3270
+ function reportingRouteEvidenceField(subjectId, name, label) {
3271
+ const evidence = resourcesOfType("evidence").filter(({ record }) => (
3272
+ record.status === "verified"
3273
+ && record.sourceResourceIds?.includes(subjectId)
3274
+ && record.sourceDescription?.trim()
3275
+ && record.collectedOn
3276
+ && record.verifiedOn
3277
+ && record.collectorIds?.length
3278
+ && record.verifierIds?.length
3279
+ && (
3280
+ (record.sourceKind === "file" && record.filePaths?.length)
3281
+ || (record.sourceKind === "rendered-page" && record.artifactKind === "rendered-page" && record.capture && record.sourceCommit)
3282
+ )
3283
+ ));
3284
+ if (!evidence.length) {
3285
+ return '<div class="field-group full"><span>' + esc(label) + ' <span class="required-mark">Required</span></span><p class="field-help">Add verified, fixed Evidence linked to this channel set before recording the event.</p><a class="button" href="#/resources/evidence">Open Evidence</a></div>';
3286
+ }
3287
+ return '<fieldset class="field-group full"><legend>' + esc(label) + ' <span class="required-mark">Required</span></legend>' + evidence.map(({ record }) => '<label class="confirmation"><input type="checkbox" name="' + esc(name) + '" value="' + esc(record.id) + '"><span>' + esc(record.title) + '</span></label>').join("") + '<p class="field-help">Only likely candidates are shown. FileGRC checks retained material and event-date coverage when you submit.</p></fieldset>';
3288
+ }
3289
+
3290
+ function checkedFormValues(form, name) {
3291
+ return [...form.querySelectorAll('input[name="' + name + '"]:checked')].map((input) => input.value);
3292
+ }
3293
+
3294
+ function openReportingRouteApproval(entry) {
3295
+ const appointments = reportingRouteAppointmentOptions(entry);
3296
+ const predecessor = entry.record.predecessorId
3297
+ ? state.resources.find(({ record }) => record.id === entry.record.predecessorId && record.type === "reporting-route-set" && record.status === "approved")
3298
+ : null;
3299
+ const predecessorAppointments = predecessor ? reportingRouteAppointmentOptions(predecessor) : [];
3300
+ const nowLocal = localDateTimeInput(new Date(), state.workspace.timezone);
3301
+ const tomorrow = new Date(Date.now() + 86_400_000);
3302
+ const defaultEffective = predecessor
3303
+ ? nowLocal
3304
+ : localDateTimeInput(tomorrow, state.workspace.timezone, true);
3305
+ const approvalActionLabel = state.repository?.mode === "trunk" && !state.repository?.developmentOverride
3306
+ ? "Approve and commit"
3307
+ : "Approve (commit required)";
3308
+ const dialog = document.createElement("dialog");
3309
+ dialog.className = "editor";
3310
+ dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Reporting channel approval</p><h2>' + esc(approvalActionLabel) + '</h2></div><button type="button" class="icon-button" data-dismiss aria-label="Close">×</button></div><p>Approve the exact normal and fallback channels proposed at <code>' + esc(state.git.shortCommit) + '</code>. FileGRC records the actual approval and effective times; Git records when this approval is committed.</p><div class="form-grid"><label class="field-group full"><span>Approval authority <span class="required-mark">Required</span></span><select name="approvalAppointmentId" required><option value="">Select an Appointment</option>' + appointments.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label>' + reportingRouteEvidenceField(entry.record.id, "evidenceIds", "Approval evidence") + '<label class="field-group"><span>Approved at <span class="required-mark">Required</span></span><input name="approvedAt" type="datetime-local" step="1" required value="' + esc(nowLocal) + '"></label><label class="field-group"><span>Effective at <span class="required-mark">Required</span></span><input name="effectiveAt" type="datetime-local" step="1" required value="' + esc(defaultEffective) + '"></label>' + (predecessor ? '<div class="field-group full"><span>Predecessor cutover</span><p class="field-help">Approving this successor also cancels ' + esc(predecessor.record.title) + ' at the effective time.</p></div><label class="field-group full"><span>Predecessor cancellation authority <span class="required-mark">Required</span></span><select name="predecessorCancellationAppointmentId" required><option value="">Select an Appointment</option>' + predecessorAppointments.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label>' + reportingRouteEvidenceField(predecessor.record.id, "predecessorCancellationEvidenceIds", "Predecessor cancellation evidence") : "") + '<label class="field-group full confirmation"><input type="checkbox" name="confirm" required><span>I reviewed this proposal and confirm these are the actual approval and effective times in ' + esc(state.workspace.timezone) + '.</span></label></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status" role="status"></span><button type="button" class="button" data-dismiss>Cancel</button><button type="submit" class="button primary">' + esc(approvalActionLabel) + '</button></div></form>';
3311
+ document.body.append(dialog);
3312
+ dialog.showModal();
3313
+ dialog.querySelectorAll("[data-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
3314
+ dialog.addEventListener("close", () => dialog.remove());
3315
+ dialog.querySelector("form").addEventListener("submit", (event) => {
3316
+ event.preventDefault();
3317
+ const form = event.currentTarget;
3318
+ if (!form.reportValidity()) return;
3319
+ const evidenceIds = checkedFormValues(form, "evidenceIds");
3320
+ const predecessorEvidenceIds = checkedFormValues(form, "predecessorCancellationEvidenceIds");
3321
+ if (!evidenceIds.length || (predecessor && !predecessorEvidenceIds.length)) {
3322
+ form.querySelector(".dialog-error").textContent = "Select fixed, verified Evidence for each approval or cancellation event.";
3323
+ return;
3324
+ }
3325
+ mutateReportingRouteSet("approve", {
3326
+ routeSetId: entry.record.id,
3327
+ proposalCommit: state.git.commit,
3328
+ approvalAppointmentId: form.elements.approvalAppointmentId.value,
3329
+ evidenceIds,
3330
+ approvedAt: zonedTimestampFromLocal(form.elements.approvedAt.value, state.workspace.timezone),
3331
+ effectiveAt: zonedTimestampFromLocal(form.elements.effectiveAt.value, state.workspace.timezone),
3332
+ timezone: state.workspace.timezone,
3333
+ expectedRevision: entry.revision,
3334
+ ...(predecessor ? {
3335
+ predecessorCancellationAppointmentId: form.elements.predecessorCancellationAppointmentId.value,
3336
+ predecessorCancellationEvidenceIds: predecessorEvidenceIds,
3337
+ predecessorExpectedRevision: predecessor.revision
3338
+ } : {})
3339
+ }, dialog);
3340
+ });
3341
+ }
3342
+
3343
+ function openReportingRouteCancellation(entry) {
3344
+ const appointments = reportingRouteAppointmentOptions(entry);
3345
+ const dialog = document.createElement("dialog");
3346
+ dialog.className = "editor";
3347
+ dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Reporting channel cancellation</p><h2>Cancel this revision</h2></div><button type="button" class="icon-button" data-dismiss aria-label="Close">×</button></div><p>Use cancellation for a stopped revision. Create a successor when either channel changes.</p><div class="form-grid"><label class="field-group full"><span>Cancellation authority <span class="required-mark">Required</span></span><select name="approvalAppointmentId" required><option value="">Select an Appointment</option>' + appointments.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label>' + reportingRouteEvidenceField(entry.record.id, "evidenceIds", "Cancellation evidence") + '<label class="field-group"><span>Canceled at <span class="required-mark">Required</span></span><input name="canceledAt" type="datetime-local" step="1" required value="' + esc(localDateTimeInput(new Date(), state.workspace.timezone)) + '"></label><label class="field-group full"><span>Reason <span class="required-mark">Required</span></span><textarea name="reason" required></textarea></label></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status" role="status"></span><button type="button" class="button" data-dismiss>Cancel</button><button type="submit" class="button danger">Cancel channel set</button></div></form>';
3348
+ document.body.append(dialog);
3349
+ dialog.showModal();
3350
+ dialog.querySelectorAll("[data-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
3351
+ dialog.addEventListener("close", () => dialog.remove());
3352
+ dialog.querySelector("form").addEventListener("submit", (event) => {
3353
+ event.preventDefault();
3354
+ const form = event.currentTarget;
3355
+ if (!form.reportValidity()) return;
3356
+ const evidenceIds = checkedFormValues(form, "evidenceIds");
3357
+ if (!evidenceIds.length) {
3358
+ form.querySelector(".dialog-error").textContent = "Select fixed, verified Evidence for the cancellation event.";
3359
+ return;
3360
+ }
3361
+ mutateReportingRouteSet("cancel", {
3362
+ routeSetId: entry.record.id,
3363
+ approvalAppointmentId: form.elements.approvalAppointmentId.value,
3364
+ evidenceIds,
3365
+ canceledAt: zonedTimestampFromLocal(form.elements.canceledAt.value, state.workspace.timezone),
3366
+ timezone: state.workspace.timezone,
3367
+ reason: form.elements.reason.value,
3368
+ expectedRevision: entry.revision
3369
+ }, dialog);
3370
+ });
3371
+ }
3372
+
3373
+ function localDateTimeInput(date, timezone, midnight = false) {
3374
+ const parts = Object.fromEntries(new Intl.DateTimeFormat("en-CA", {
3375
+ timeZone: timezone,
3376
+ year: "numeric",
3377
+ month: "2-digit",
3378
+ day: "2-digit",
3379
+ hour: "2-digit",
3380
+ minute: "2-digit",
3381
+ second: "2-digit",
3382
+ hourCycle: "h23"
3383
+ }).formatToParts(date).map(({ type, value }) => [type, value]));
3384
+ return parts.year + "-" + parts.month + "-" + parts.day + "T" + (midnight ? "00:00:00" : parts.hour + ":" + parts.minute + ":" + parts.second);
3385
+ }
3386
+
3387
+ function zonedTimestampFromLocal(value, timezone) {
3388
+ const desired = new Date(value + "Z");
3389
+ if (Number.isNaN(desired.getTime())) throw new Error("A valid local date and time is required.");
3390
+ let instant = desired;
3391
+ for (let attempt = 0; attempt < 4; attempt += 1) {
3392
+ const represented = new Date(localDateTimeInput(instant, timezone) + "Z");
3393
+ instant = new Date(instant.getTime() + desired.getTime() - represented.getTime());
3394
+ }
3395
+ if (localDateTimeInput(instant, timezone) !== value) throw new Error("The selected local time does not exist in " + timezone + ".");
3396
+ const zoneLocalAsUtc = new Date(localDateTimeInput(instant, timezone) + "Z");
3397
+ const offsetMinutes = Math.round((zoneLocalAsUtc.getTime() - instant.getTime()) / 60_000);
3398
+ const sign = offsetMinutes < 0 ? "-" : "+";
3399
+ const absolute = Math.abs(offsetMinutes);
3400
+ const offset = sign + String(Math.floor(absolute / 60)).padStart(2, "0") + ":" + String(absolute % 60).padStart(2, "0");
3401
+ return value + offset;
3402
+ }
3403
+
3063
3404
  function resourceGuide(type) {
3064
3405
  const definition = state.model.resources[type];
3065
3406
  const guidance = definition?.guidance;
@@ -3593,7 +3934,7 @@ function openEditor(type, entry = null, options = {}) {
3593
3934
  const markdownDefinitions = dedicatedMarkdownDefinitions(type);
3594
3935
  if (!entry && options.seed) {
3595
3936
  Object.assign(record, options.seed);
3596
- record.id = createResourceId(type, record.title, state.resources.map(({ record: existing }) => existing.id));
3937
+ record.id ||= createResourceId(type, record.title, state.resources.map(({ record: existing }) => existing.id));
3597
3938
  }
3598
3939
  const required = new Set([
3599
3940
  ...Object.entries(state.model.commonFields).filter(([, field]) => field.required).map(([name]) => name),
@@ -3614,7 +3955,11 @@ function openEditor(type, entry = null, options = {}) {
3614
3955
  ...Object.entries(fields).filter(([, field]) => field.relation || field.relationGroup).map(([name]) => name),
3615
3956
  ...Object.entries(fields).filter(([, field]) => field.requiredWhen).map(([name]) => name),
3616
3957
  ...oneOf
3617
- ])].filter((name) => !["id", "type"].includes(name) && fields[name]);
3958
+ ])].filter((name) => (
3959
+ !["id", "type"].includes(name)
3960
+ && fields[name]
3961
+ && !(options.occurrenceReview && ["members", "expectedCount", "completedCount", "conclusion"].includes(name))
3962
+ ));
3618
3963
  const dialog = document.createElement("dialog");
3619
3964
  dialog.className = "editor";
3620
3965
  dialog.setAttribute("aria-labelledby", "resource-editor-title");
@@ -3626,8 +3971,8 @@ function openEditor(type, entry = null, options = {}) {
3626
3971
  const editorDescription = options.description
3627
3972
  || implementationEditorDescription(type)
3628
3973
  || conciseResourceDescription(definition)
3629
- || "Add the facts known now.";
3630
- dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + (entry ? "Edit record" : options.actionCompletion ? "Complete assigned work" : options.obligationCompletion ? "Record obligation work" : "Create record") + '</p><h2 id="resource-editor-title">' + esc(titleCase(entry?.record.title || record.title || definition.title)) + '</h2></div><button type="button" class="icon-button" data-editor-dismiss aria-label="Close">×</button></div><p>' + esc(editorDescription) + '</p>' + resourceReviewCriteria(type, true) + '<div class="form-grid">' + names.map((name) => editorField(type, name, fields[name], record[name], required.has(name) || conditionMatches(record, fields[name].requiredWhen), Boolean(entry), oneOf.has(name), activeOneOf.has(name))).join("") + '</div>' +
3974
+ || "Fill the core fields below. Git will record the author, time, reason, and diff when you commit this file.";
3975
+ dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + (entry ? "Edit record" : options.actionCompletion ? "Complete assigned work" : options.obligationCompletion ? "Record obligation work" : "Create record") + '</p><h2 id="resource-editor-title">' + esc(titleCase(entry?.record.title || record.title || definition.title)) + '</h2></div><button type="button" class="icon-button" data-editor-dismiss aria-label="Close">×</button></div><p>' + esc(editorDescription) + '</p>' + resourceReviewCriteria(type, true) + (options.occurrenceReview ? occurrenceMemberReview(record, options.membershipFinal) : "") + '<div class="form-grid">' + names.map((name) => editorField(type, name, fields[name], record[name], required.has(name) || conditionMatches(record, fields[name].requiredWhen), Boolean(entry), oneOf.has(name), activeOneOf.has(name), record.id)).join("") + '</div>' +
3631
3976
  activeMarkdown.map((markdown) => {
3632
3977
  const generated = !entry?.content?.[markdown.name];
3633
3978
  const source = entry?.content?.[markdown.name]?.source
@@ -3680,6 +4025,7 @@ function openEditor(type, entry = null, options = {}) {
3680
4025
  const updated = advanced
3681
4026
  ? JSON.parse(dialog.querySelector(".advanced-editor textarea").value)
3682
4027
  : readGuidedRecord(dialog, record, fields);
4028
+ if (!advanced && options.occurrenceReview) applyOccurrenceMemberReview(dialog, updated, record);
3683
4029
  if (type === "policy" && updated.status === "active" && entry?.record.status !== "active") {
3684
4030
  throw new Error("Approve the Policy here, then activate it from the Step 3 Controls-page cutover.");
3685
4031
  }
@@ -3700,7 +4046,11 @@ function openEditor(type, entry = null, options = {}) {
3700
4046
  content[path] = recordContentSource.value;
3701
4047
  }
3702
4048
  }
3703
- const url = entry
4049
+ const url = options.occurrenceReconciliation
4050
+ ? "/api/obligation-occurrences"
4051
+ : options.auditPopulationCorrection
4052
+ ? "/api/audit-population-corrections"
4053
+ : entry
3704
4054
  ? "/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(entry.record.id)
3705
4055
  : options.actionCompletion
3706
4056
  ? "/api/action-completions"
@@ -3711,12 +4061,12 @@ function openEditor(type, entry = null, options = {}) {
3711
4061
  ].filter(([path, revision]) => path && revision));
3712
4062
  setMutationBusy(dialog, true, "Saving…", options.saveLabel || "Save file");
3713
4063
  const response = await localFetch(url, {
3714
- method: entry ? "PUT" : "POST",
4064
+ method: options.occurrenceReconciliation ? "POST" : entry ? "PUT" : "POST",
3715
4065
  headers: { "content-type": "application/json" },
3716
4066
  body: JSON.stringify({
3717
4067
  record: updated,
3718
4068
  content,
3719
- revision: entry?.revision || options.actionCompletion?.revision || options.obligationCompletion?.revision,
4069
+ revision: entry?.revision || options.actionCompletion?.revision || options.obligationCompletion?.revision || options.occurrenceReconciliation?.revision || options.auditPopulationCorrection?.revision,
3720
4070
  contentRevisions,
3721
4071
  obligationId: options.obligationCompletion?.obligationId,
3722
4072
  actionItemId: options.actionCompletion?.actionItemId,
@@ -3735,6 +4085,71 @@ function openEditor(type, entry = null, options = {}) {
3735
4085
  });
3736
4086
  }
3737
4087
 
4088
+ function occurrenceMemberReview(record, membershipFinal = true) {
4089
+ const members = record.members || [];
4090
+ const pending = membershipFinal
4091
+ ? ""
4092
+ : '<p class="occurrence-pending">Population remains open through ' + esc(record.membershipCutoffAt) + '. Review now, then reconcile after the cutoff.</p>';
4093
+ if (!members.length) {
4094
+ return '<section class="occurrence-review"><div class="occurrence-review-head"><strong>Population</strong><span>0 members</span></div>' + pending + '<p class="occurrence-empty">Confirm the zero population, then set the record to reconciled.</p></section>';
4095
+ }
4096
+ const rows = members.map((member) => {
4097
+ const source = state.resources.find(({ record: candidate }) => candidate.id === member.resourceId)?.record;
4098
+ const title = source?.title || member.resourceId;
4099
+ const result = member.disposition === "not-applicable"
4100
+ ? "not-applicable"
4101
+ : member.disposition === "exception" ? "exception" : member.result || "pending";
4102
+ return '<div class="occurrence-member" data-occurrence-member="' + esc(member.resourceId) + '"><div class="occurrence-member-name"><strong>' + esc(title) + '</strong><small>' + esc(member.resourceId) + '</small></div>'
4103
+ + '<label><span>Result</span><select data-member-result><option value="pending" ' + (result === "pending" ? "selected" : "") + '>Pending</option><option value="passed" ' + (result === "passed" ? "selected" : "") + '>Passed</option><option value="failed" ' + (result === "failed" ? "selected" : "") + '>Failed</option><option value="exception" ' + (result === "exception" ? "selected" : "") + '>Approved exception</option><option value="not-applicable" ' + (result === "not-applicable" ? "selected" : "") + '>Not applicable</option></select></label>'
4104
+ + '<label class="occurrence-proof"><span>Completion IDs</span><input data-member-completions value="' + esc((member.completionResourceIds || []).join(", ")) + '" placeholder="attestation-id"></label>'
4105
+ + '<label class="occurrence-proof"><span>Exception ID</span><input data-member-exception value="' + esc(member.exceptionId || "") + '" placeholder="exception-id"></label>'
4106
+ + '<label class="occurrence-rationale"><span>Rationale</span><input data-member-rationale value="' + esc(member.rationale || "") + '" placeholder="Required when not applicable"></label></div>';
4107
+ }).join("");
4108
+ return '<section class="occurrence-review"><div class="occurrence-review-head"><strong>Population</strong><span>' + members.length + ' members</span></div>' + pending + '<div class="occurrence-members">' + rows + '</div></section>';
4109
+ }
4110
+
4111
+ function applyOccurrenceMemberReview(dialog, updated, original) {
4112
+ updated.members = [...dialog.querySelectorAll("[data-occurrence-member]")].map((row) => {
4113
+ const resourceId = row.dataset.occurrenceMember;
4114
+ const originalMember = (original.members || []).find((member) => member.resourceId === resourceId) || { resourceId };
4115
+ const selectedResult = row.querySelector("[data-member-result]").value;
4116
+ const completionResourceIds = row.querySelector("[data-member-completions]").value.split(",").map((value) => value.trim()).filter(Boolean);
4117
+ const exceptionId = row.querySelector("[data-member-exception]").value.trim();
4118
+ const rationale = row.querySelector("[data-member-rationale]").value.trim();
4119
+ if (selectedResult === "not-applicable" && !rationale) {
4120
+ throw new Error("A rationale is required when " + resourceId + " is not applicable.");
4121
+ }
4122
+ if (selectedResult === "exception" && !exceptionId) {
4123
+ throw new Error("An approved Exception ID is required for " + resourceId + ".");
4124
+ }
4125
+ const disposition = ["not-applicable", "exception"].includes(selectedResult) ? selectedResult : "expected";
4126
+ return {
4127
+ ...originalMember,
4128
+ disposition,
4129
+ result: ["not-applicable", "exception"].includes(selectedResult) ? "pending" : selectedResult,
4130
+ ...(completionResourceIds.length ? { completionResourceIds } : { completionResourceIds: [] }),
4131
+ ...(exceptionId && disposition === "exception" ? { exceptionId } : { exceptionId: undefined }),
4132
+ ...(rationale ? { rationale } : {})
4133
+ };
4134
+ });
4135
+ const expected = updated.members.filter(({ disposition }) => disposition === "expected");
4136
+ updated.expectedCount = expected.length;
4137
+ updated.completedCount = expected.filter(({ result }) => result === "passed").length;
4138
+ if (updated.status === "open") {
4139
+ delete updated.conclusion;
4140
+ delete updated.reconciledAt;
4141
+ delete updated.reviewedByIds;
4142
+ return;
4143
+ }
4144
+ updated.conclusion = updated.members.length === 0
4145
+ ? "zero-population"
4146
+ : updated.completedCount !== updated.expectedCount
4147
+ ? "incomplete"
4148
+ : updated.members.some(({ disposition }) => disposition !== "expected")
4149
+ ? "complete-with-exceptions"
4150
+ : "complete";
4151
+ }
4152
+
3738
4153
  function implementationEditorDescription(type) {
3739
4154
  if (type === "person") {
3740
4155
  return "Record someone who owns, reviews, approves, or performs program work.";
@@ -3874,7 +4289,7 @@ function recordContentPlaceholder(type) {
3874
4289
  return "Document the work, results, decisions, and follow-up.";
3875
4290
  }
3876
4291
 
3877
- function editorField(type, name, field, value, required, editing, oneOfRequired = false, oneOfActive = oneOfRequired) {
4292
+ function editorField(type, name, field, value, required, editing, oneOfRequired = false, oneOfActive = oneOfRequired, currentRecordId = null) {
3878
4293
  const label = fieldLabel(type, name);
3879
4294
  const requiredMark = required || field.requiredWhen || oneOfRequired
3880
4295
  ? '<span class="required-mark" ' + (required || oneOfActive ? "" : "hidden") + '>' + (oneOfRequired ? "One Required" : "Required") + '</span>'
@@ -3898,7 +4313,9 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
3898
4313
  return fieldWrap(name, "relation-array", label, requiredMark, control, help, required);
3899
4314
  }
3900
4315
  if (relation) {
3901
- const candidates = relationCandidates(relationField, type, name);
4316
+ const candidates = relationCandidates(relationField, type, name).filter(({ record }) => (
4317
+ name !== "predecessorId" || record.id !== currentRecordId
4318
+ ));
3902
4319
  control = candidates.length
3903
4320
  ? '<select><option value="">Select ' + esc(relationTypeLabel(relationField).toLowerCase()) + '</option>' + candidates.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (value === record.id ? "selected" : "") + '>' + esc(record.title) + ' · ' + esc(state.model.resources[record.type].title) + '</option>').join("") + '</select>'
3904
4321
  : required ? '<select><option value="">No matching ' + esc(relationTypeLabel(relationField, true).toLowerCase()) + ' yet</option></select>' : '<div class="missing-options">No matching ' + esc(relationTypeLabel(relationField, true).toLowerCase()) + ' yet.</div>';
@@ -3914,9 +4331,11 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
3914
4331
  const values = field.values || (field.type === "rating" ? state.model.primitives.rating : state.model.primitives.outcome) || [];
3915
4332
  const activationManagedType = ["policy", "document"].includes(type)
3916
4333
  || (type === "training" && Boolean(state.model.resources.training?.fields?.activatedContentRevisions));
3917
- const availableValues = activationManagedType && name === "status" && value !== "active"
3918
- ? values.filter((item) => item !== "active")
3919
- : values;
4334
+ const availableValues = type === "reporting-route-set" && name === "status" && editing
4335
+ ? values.filter((item) => item === value)
4336
+ : activationManagedType && name === "status" && value !== "active"
4337
+ ? values.filter((item) => item !== "active")
4338
+ : values;
3920
4339
  control = '<select><option value="">Select</option>' + availableValues.map((item) => '<option value="' + esc(item) + '" ' + (value === item ? "selected" : "") + '>' + esc(properCase(item)) + '</option>').join("") + '</select>';
3921
4340
  const enumHelp = activationManagedType && name === "status"
3922
4341
  ? type === "document"
@@ -4030,7 +4449,7 @@ function objectPropertyFields(schema, value = {}) {
4030
4449
  ? '<textarea>' + esc(value[name] || "") + '</textarea>'
4031
4450
  : '<input type="' + (property.type === "date" ? "date" : property.type === "timestamp" ? "datetime-local" : ["integer", "number"].includes(property.type) ? "number" : "text") + '" value="' + esc(property.type === "timestamp" && value[name] ? String(value[name]).slice(0, 16) : value[name] ?? "") + '"' + (property.minimum !== undefined ? ' min="' + esc(property.minimum) + '"' : "") + '>';
4032
4451
  }
4033
- return '<div class="object-property' + (stringMap ? " string-map-property" : "") + '" data-object-field="' + esc(name) + '" data-object-kind="' + esc(property.type) + '" data-object-required="' + (required.has(name) || property.requiredWhen ? "true" : "false") + '"' + (property.requiredWhen ? ' data-object-required-when="' + esc(JSON.stringify(property.requiredWhen)) + '"' : "") + (property.allowedWhen ? ' data-object-allowed-when="' + esc(JSON.stringify(property.allowedWhen)) + '"' : "") + '><span class="object-property-label">' + esc(humanize(name)) + mark + '</span>' + input + '</div>';
4452
+ return '<div class="object-property' + (stringMap ? " string-map-property" : "") + '" data-object-field="' + esc(name) + '" data-object-kind="' + esc(property.type) + '" data-object-required="' + (required.has(name) || property.requiredWhen ? "true" : "false") + '"' + (property.requiredWhen ? ' data-object-required-when="' + esc(JSON.stringify(property.requiredWhen)) + '"' : "") + (property.allowedWhen ? ' data-object-allowed-when="' + esc(JSON.stringify(property.allowedWhen)) + '"' : "") + '><span class="object-property-label">' + esc(property.label || humanize(name)) + mark + '</span>' + input + '</div>';
4034
4453
  }).join("");
4035
4454
  }
4036
4455
 
@@ -4430,6 +4849,17 @@ function openContentEditor(entry, name) {
4430
4849
  }
4431
4850
 
4432
4851
  function bindCommon() {
4852
+ root.querySelector("[data-program-select]")?.addEventListener("change", async (event) => {
4853
+ const programId = event.currentTarget.value;
4854
+ const selectionGeneration = ++programSelectionGeneration;
4855
+ const response = await fetch("/api/state/bootstrap?programId=" + encodeURIComponent(programId));
4856
+ if (!response.ok) throw new Error(await responseMessage(response));
4857
+ if (selectionGeneration !== programSelectionGeneration) return;
4858
+ state = normalizeAppState(await response.json());
4859
+ stateSectionRequests.clear();
4860
+ render();
4861
+ loadStateForRoute();
4862
+ });
4433
4863
  root.querySelectorAll(".nav-toggle, .nav-subgroup-toggle").forEach((button) => button.addEventListener("click", () => {
4434
4864
  const group = button.closest(".nav-group");
4435
4865
  const open = group.classList.toggle("open");
@@ -4766,7 +5196,7 @@ function formatObjectArray(items, objectType, compact = false) {
4766
5196
  notes.push(String(value));
4767
5197
  return [];
4768
5198
  }
4769
- const label = humanize(name.replace(/Ids?$/, ""));
5199
+ const label = property.label || humanize(name.replace(/Ids?$/, ""));
4770
5200
  let display;
4771
5201
  if (property.relation && property.type === "id") display = formatReference(value);
4772
5202
  else if (property.relation && property.type === "array") display = value.map((id) => formatReference(id)).join(" ");
@@ -4930,7 +5360,8 @@ async function refreshMutationState() {
4930
5360
  mutationStateRefreshInFlight = true;
4931
5361
  let retry = false;
4932
5362
  try {
4933
- const response = await fetch("/api/state/bootstrap");
5363
+ const programQuery = state.selectedProgramId ? "?programId=" + encodeURIComponent(state.selectedProgramId) : "";
5364
+ const response = await fetch("/api/state/bootstrap" + programQuery);
4934
5365
  if (!response.ok) throw new Error(await responseMessage(response));
4935
5366
  state = normalizeAppState(await response.json());
4936
5367
  stateSectionRequests.clear();
@@ -5066,19 +5497,24 @@ async function responseMessage(response) {
5066
5497
  try { return JSON.parse(source).error || source; } catch { return source; }
5067
5498
  }
5068
5499
  async function localFetch(url, options) {
5500
+ const requestUrl = new URL(url, location.origin);
5501
+ if (state?.selectedProgramId && !requestUrl.searchParams.has("programId")) {
5502
+ requestUrl.searchParams.set("programId", state.selectedProgramId);
5503
+ }
5504
+ const scopedUrl = requestUrl.pathname + requestUrl.search + requestUrl.hash;
5069
5505
  const method = String(options?.method || "GET").toUpperCase();
5070
5506
  const synchronizing = state?.repository?.mode === "trunk"
5071
5507
  && ["POST", "PUT", "DELETE"].includes(method)
5072
- && !["/api/evidence-packet", "/api/git/prefetch"].includes(url);
5508
+ && !["/api/evidence-packet", "/api/git/prefetch"].includes(requestUrl.pathname);
5073
5509
  const chip = synchronizing ? document.querySelector(".repo-chip") : null;
5074
5510
  const previousChip = chip?.innerHTML;
5075
5511
  let repositoryRefreshed = false;
5076
5512
  if (chip) chip.innerHTML = '<span class="status-dot neutral"></span>Syncing';
5077
5513
  try {
5078
- const response = await fetch(url, options);
5514
+ const response = await fetch(scopedUrl, options);
5079
5515
  if (synchronizing && !response.ok) {
5080
5516
  try {
5081
- const stateResponse = await fetch("/api/state");
5517
+ const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(state.selectedProgramId || ""));
5082
5518
  if (stateResponse.ok) {
5083
5519
  state = await stateResponse.json();
5084
5520
  if (chip?.isConnected) {
@@ -5104,7 +5540,7 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
5104
5540
  export const APP_STYLES = String.raw`
5105
5541
  :root{--ink:#151827;--muted:#5d6475;--line:#dfe3ef;--paper:#f6f7fb;--panel:#fff;--accent:#0000a5;--accent-soft:#eef1ff;--accent-light:#8aa1ff;--focus:#0000e0;--amber:#8a5200;--red:#a13a31;--sidebar:linear-gradient(135deg,#000070 0%,#000035 60%);--primary-gradient:linear-gradient(135deg,#000070 0%,#000035 60%);--surface-soft:#f2f4fa;--surface-muted:#eceff7;--field:#fff;--field-readonly:#eef0f6;--code-bg:#10162b;--code-ink:#e8ebff;--shadow:0 8px 28px rgba(0,0,53,.08);color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
5106
5542
  *{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
5107
- .topbar-readiness{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:0 1 220px;min-width:155px;padding:5px 8px;border:1px solid transparent;border-radius:8px;color:var(--ink);text-decoration:none}.topbar-readiness:hover{border-color:var(--accent-light);background:var(--accent-soft)}.topbar-readiness-copy{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}.topbar-readiness-copy>span{overflow:hidden;color:var(--muted);font-size:9.6px;font-weight:700;text-overflow:ellipsis;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.topbar-readiness-copy>strong{font-size:10.8px;line-height:1}.topbar-readiness>.progress{width:100%;height:5px}.topbar-status{display:flex;flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-end;gap:8px;margin-left:auto}.topbar-status .topbar-search{flex:0 1 240px;width:240px;min-width:140px;margin-left:0}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
5543
+ .topbar-readiness{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:0 1 220px;min-width:155px;padding:5px 8px;border:1px solid transparent;border-radius:8px;color:var(--ink);text-decoration:none}.topbar-readiness:hover{border-color:var(--accent-light);background:var(--accent-soft)}.topbar-readiness-copy{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}.topbar-readiness-copy>span{overflow:hidden;color:var(--muted);font-size:9.6px;font-weight:700;text-overflow:ellipsis;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.topbar-readiness-copy>strong{font-size:10.8px;line-height:1}.topbar-readiness>.progress{width:100%;height:5px}.topbar-status{display:flex;flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-end;gap:8px;margin-left:auto}.program-select select{max-width:180px;border:1px solid var(--line);border-radius:8px;padding:9px 28px 9px 10px;background:var(--surface);color:var(--ink);font:inherit}.topbar-status .topbar-search{flex:0 1 240px;width:240px;min-width:140px;margin-left:0}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
5108
5544
  .metadata>div{grid-template-columns:minmax(140px,1fr) minmax(0,2.5fr)}.metadata dt{min-width:0;overflow-wrap:anywhere}
5109
5545
  .audit-progress-empty{padding:11px 13px;border-radius:8px;background:var(--surface-soft)}.audit-progress-empty strong,.audit-progress-empty span{display:block}.audit-progress-empty strong{font-size:13.2px}.audit-progress-empty span{margin-top:4px;color:var(--muted);font-size:10.8px}
5110
5546
  .icon-button{position:relative;display:grid;place-items:center;padding:0;color:var(--ink);font-size:0}.icon-button:before,.icon-button:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.icon-button:after{transform:rotate(-45deg)}
@@ -5232,14 +5668,17 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
5232
5668
  .obligation-preview,.event-reminder-preview{display:grid;gap:8px}.obligation-preview a{display:flex;align-items:flex-start;gap:9px;text-decoration:none;padding:7px 0;border-top:1px solid var(--line)}.obligation-preview a:first-child{border-top:0;padding-top:0}.obligation-preview strong,.obligation-preview small,.event-reminder-preview strong,.event-reminder-preview small{display:block}.obligation-preview strong,.event-reminder-preview strong{font-size:12px}.obligation-preview small,.event-reminder-preview small{font-size:10.8px;color:var(--muted);margin-top:2px}.event-reminder-preview{grid-template-columns:repeat(2,minmax(0,1fr))}.event-reminder-preview a{padding:10px;border-radius:7px;background:var(--surface-soft);text-decoration:none}
5233
5669
  .obligation-board{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;align-items:start}.obligation-column{min-width:0}.obligation-column-head{display:flex;align-items:center;justify-content:space-between;margin:5px 1px 10px}.obligation-column-head>strong{font:500 26.4px Georgia,serif}.obligation-cards{display:grid;gap:9px}.obligation-card{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--accent-light);border-radius:9px;padding:14px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.obligation-card.status-overdue,.obligation-card.status-blocked{border-left-color:var(--red)}.obligation-card.status-due{border-left-color:#d89021}.obligation-card-head{display:flex;justify-content:space-between;gap:8px;text-transform:uppercase;letter-spacing:.06em;font-size:9.6px;color:var(--muted)}.obligation-card-head strong{color:var(--ink);text-align:right}.obligation-card h3{font-size:14.4px;margin:9px 0 7px}.obligation-card h3 a{text-decoration:none}.obligation-card p{font-size:10.8px;line-height:1.5;color:var(--muted);margin:0}.obligation-links{margin-top:10px}.workflow-section{margin-top:30px}.operation-gate{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:22px;border:1px solid var(--accent-light);border-radius:12px;background:var(--accent-wash)}.operation-gate h2{margin:5px 0 7px;font:500 25px Georgia,serif}.operation-gate p:not(.kicker){margin:0;max-width:680px;color:var(--muted)}.operation-setup-preview>summary{margin-top:18px;padding:14px 16px;border:1px solid var(--line);border-radius:9px;background:var(--panel);font-weight:700;cursor:pointer}.operation-setup-preview[open]>summary{margin-bottom:0}.section-head{display:flex;justify-content:space-between;margin-bottom:13px}.section-head h2{font:500 28.8px Georgia,serif;margin:6px 0}.section-head p:not(.kicker){font-size:13.2px;color:var(--muted);margin:0;max-width:720px}.policy-event-feedback{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:10px;align-items:center;margin-top:14px;padding:12px 14px;border:1px solid #9ccfb2;border-radius:8px;background:#e7f5ec}.policy-event-feedback .status-dot{align-self:start;margin-top:4px}.policy-event-feedback strong,.policy-event-feedback p{display:block}.policy-event-feedback strong{font-size:12px}.policy-event-feedback p{margin:3px 0 0;color:#315d44;font-size:10.8px}.policy-event-feedback .icon-button{width:30px;height:30px}.policy-event-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.policy-event-more{margin-top:10px}.policy-event-row{position:relative;display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0;padding:9px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.policy-event-row[hidden]{display:none}.policy-event-name{min-width:0}.policy-event-title{display:flex;align-items:center;gap:6px;min-width:0}.policy-event-title>strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.policy-event-guide{display:inline-flex;flex:0 0 auto}.policy-event-guide .guide-trigger{width:20px;height:20px}.policy-event-guide .guide-trigger svg{width:14px;height:14px}.policy-event-name strong,.policy-event-name>small{display:block}.policy-event-name strong{font-size:12px}.policy-event-name>small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.policy-event-row>.button{flex:none;padding:7px 9px;font-size:10.8px}.policy-event-tooltip{position:absolute;z-index:8;top:calc(100% + 7px);left:0;width:min(420px,calc(100vw - 48px));padding:12px 14px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow);opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s,transform .12s,visibility 0s .12s;pointer-events:none}.policy-event-row:nth-child(3n) .policy-event-tooltip{right:0;left:auto}.policy-event-row:hover,.policy-event-row:focus-within{z-index:9}.policy-event-guide:hover .policy-event-tooltip,.policy-event-guide:focus-within .policy-event-tooltip{opacity:1;visibility:visible;transform:none;transition-delay:0s}.policy-event-tooltip>strong{font-size:12px}.policy-event-tooltip ol{display:grid;gap:7px;margin:9px 0 0;padding-left:20px}.policy-event-tooltip li span,.policy-event-tooltip li small{display:block}.policy-event-tooltip li span{font-size:10.8px}.policy-event-tooltip li small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.4}
5234
5670
  .obligation-card-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:9px;margin-top:10px}.obligation-card-foot .obligation-links{margin-top:0;min-width:0}.obligation-action{flex:0 0 auto;border:0;border-radius:6px;background:var(--accent-soft);color:var(--accent);padding:7px 9px;font-family:inherit;font-size:10.8px;font-weight:700;line-height:1;text-decoration:none;cursor:pointer}.obligation-action:hover{filter:brightness(1.08)}.obligation-action.blocked{background:var(--surface-muted);color:var(--muted)}.obligation-more{width:100%;margin-top:9px}.workflow-section{scroll-margin-top:92px}
5671
+ .occurrence-review{margin:14px 0;border:1px solid var(--line);border-radius:8px;overflow:hidden}.occurrence-review-head{display:flex;justify-content:space-between;gap:12px;padding:10px 12px;background:var(--surface-soft);font-size:10.8px}.occurrence-review-head span,.occurrence-member small{color:var(--muted)}.occurrence-members{display:grid}.occurrence-member{display:grid;grid-template-columns:minmax(150px,1.4fr) minmax(110px,.7fr) minmax(170px,1.2fr) minmax(150px,1fr);gap:8px;align-items:end;padding:10px 12px;border-top:1px solid var(--line)}.occurrence-member:first-child{border-top:0}.occurrence-member-name{align-self:center;min-width:0}.occurrence-member-name strong,.occurrence-member-name small{display:block;overflow:hidden;text-overflow:ellipsis}.occurrence-member label span{display:block;margin-bottom:4px;color:var(--muted);font-size:9.6px}.occurrence-member input,.occurrence-member select{width:100%}.occurrence-empty,.occurrence-pending{margin:0;padding:12px;color:var(--muted);font-size:10.8px}.occurrence-pending{border-bottom:1px solid var(--line);background:#fff7df;color:#765410}
5672
+ .activation-review{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0}.activation-review>div{padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.activation-review .full{grid-column:1/-1}.activation-review span,.activation-review strong,.activation-review small{display:block}.activation-review span{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.activation-review strong{margin-top:4px;font-size:12px}.activation-review small{margin-top:3px;color:var(--muted);font-size:9.6px}.confirmation{display:flex!important;align-items:flex-start;gap:8px}.confirmation input{flex:none;width:auto!important;margin-top:2px}.cutover-note{margin:12px 0;padding:9px 11px;border-radius:7px;background:#fff7df;color:#765410;font-size:10.8px}
5235
5673
  .event-dialog label{display:block;margin-top:13px}.event-dialog label[hidden]{display:none}.event-dialog label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.event-dialog input,.event-dialog select,.event-dialog textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.event-dialog textarea{resize:vertical}.commit-dialog .form-grid label.full{grid-column:1/-1}.workflow-preview{margin-top:14px;padding:0 12px;border-radius:7px;background:var(--surface-soft)}.workflow-preview:not(:empty){padding-top:10px;padding-bottom:10px}.workflow-preview strong,.workflow-preview p{display:block;margin:0}.workflow-preview p{margin-top:5px;color:var(--muted);font-size:12px;line-height:1.5}.event-dialog-steps{display:grid;gap:6px;margin-top:15px;padding:10px;background:var(--surface-soft);border-radius:7px}.event-dialog-steps strong,.event-dialog-steps small{display:block}.event-dialog-steps strong{font-size:12px}.event-dialog-steps small{font-size:9.6px;color:var(--muted);margin-top:2px}
5236
5674
  .applicability-dialog{width:min(980px,calc(100vw - 30px));max-height:calc(100vh - 32px);border:0;border-radius:12px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 25px 80px rgba(0,0,24,.28)}.applicability-dialog form{padding:23px}.applicability-dialog form>p{color:var(--muted);font-size:13.2px}.applicability-dialog form>.applicability-baseline-note{padding:10px 12px;border-radius:7px;background:var(--accent-soft);color:var(--ink)}.review-context label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.review-context input,.review-context select,.applicability-row input,.applicability-row select{width:100%;min-height:38px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:8px 9px;font-size:13.2px}.review-context label.full{grid-column:1/-1}.applicability-rows{display:grid;gap:7px;max-height:46vh;overflow:auto;margin-top:16px;padding-right:4px}.applicability-row{display:grid;grid-template-columns:minmax(210px,1fr) 180px minmax(240px,1.3fr);gap:9px;align-items:center;padding:9px;border:1px solid var(--line);border-radius:8px}.applicability-row strong,.applicability-row small{display:block}.applicability-row small{margin-top:3px;color:var(--muted);font-size:10.8px}.applicability-row .applicability-constraint{color:var(--accent);line-height:1.35}
5237
5675
  .object-value-list{display:grid;gap:7px}.object-value{min-width:0;padding:7px 8px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.object-value-facts{display:flex;flex-wrap:wrap;gap:5px 10px}.object-value-facts>span{display:flex;align-items:baseline;gap:5px;min-width:0;font-size:11px}.object-value-facts b{color:var(--muted);font-size:8.8px;text-transform:uppercase;letter-spacing:.05em}.object-value>small{display:block;margin-top:6px;color:var(--muted);font-size:10px;line-height:1.45}.object-value-list.compact .object-value{padding:0;border:0;background:none}.object-value-list.compact .object-value-facts{display:grid;gap:5px}.object-value-list.compact .object-value-facts>span{display:grid;gap:2px;font-size:10px}
5238
5676
  .resource-review-criteria.compact{margin:12px 0;padding:10px 12px;background:var(--surface-soft)}.resource-review-criteria.compact summary{cursor:pointer;font-size:11px;font-weight:750}.resource-review-criteria.compact ul{margin-bottom:0}.evidence-map-more{justify-self:center;margin:2px 0 8px}
5239
- .packet-builder form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end}.packet-builder label>span{display:block;font-size:10.8px;font-weight:720;margin-bottom:6px}.packet-builder input,.packet-builder select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:13.2px}.packet-note,.packet-output>p{font-size:12px;color:var(--muted);margin:12px 0 0}.packet-output{margin:14px 0}.packet-output h3{overflow-wrap:anywhere}.packet-gaps{display:grid}.packet-gaps>div{display:grid;grid-template-columns:58px 1fr;gap:10px;border-top:1px solid var(--line);padding:10px 0}.packet-gaps>div:first-child{border-top:0}.packet-gaps p{font-size:12px;margin:0}.packet-list{display:grid}.packet-list a{display:block;text-decoration:none;border-top:1px solid var(--line);padding:9px 0}.packet-list a:first-child{border-top:0}.packet-list strong,.packet-list small{display:block}.packet-list strong{font-size:12px}.packet-list small{font-size:9.6px;color:var(--muted);margin-top:2px}
5677
+ .packet-builder form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end}.packet-builder label>span{display:block;font-size:10.8px;font-weight:720;margin-bottom:6px}.packet-builder input,.packet-builder select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:13.2px}.packet-note,.packet-output>p{font-size:12px;color:var(--muted);margin:12px 0 0}.packet-output{margin:14px 0}.packet-output h3{overflow-wrap:anywhere}.packet-gaps{display:grid}.packet-gaps>div{display:grid;grid-template-columns:max-content 1fr;align-items:start;gap:10px;border-top:1px solid var(--line);padding:10px 0}.packet-gaps>div:first-child{border-top:0}.packet-gaps .badge{display:inline-flex;align-items:center;justify-content:center;line-height:1.2}.packet-gaps p{font-size:12px;margin:0}.packet-list{display:grid}.packet-list a{display:block;text-decoration:none;border-top:1px solid var(--line);padding:9px 0}.packet-list a:first-child{border-top:0}.packet-list strong,.packet-list small{display:block}.packet-list strong{font-size:12px}.packet-list small{font-size:9.6px;color:var(--muted);margin-top:2px}
5240
5678
  .packet-preflight{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-bottom:12px}.packet-preflight a{display:flex;align-items:flex-start;gap:9px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);text-decoration:none}.packet-preflight .status-dot{margin-top:4px}.packet-preflight small,.packet-preflight strong{display:block}.packet-preflight small{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.07em}.packet-preflight strong{margin-top:3px;font-size:12px}.audit-evidence-paths{margin-bottom:12px}.audit-evidence-paths .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.audit-evidence-path-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.audit-evidence-path-grid>a{display:block;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.audit-evidence-path-grid h4{margin:7px 0 5px;font-size:13.2px}.audit-evidence-path-grid p{margin:0;color:var(--muted);font-size:10.8px;line-height:1.55}
5241
5679
  .audit-preparation{margin-bottom:12px}.audit-preparation .panel-head{align-items:flex-start}.audit-preparation .panel-head h3{margin:3px 0}.audit-preparation .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.preparation-progress{height:5px;margin:12px 0 0;border-radius:99px;background:var(--surface-muted);overflow:hidden}.preparation-progress span{display:block;height:100%;border-radius:inherit;background:var(--primary-gradient)}.audit-preparation-note{margin:9px 0 0;color:var(--muted);font-size:10.8px;line-height:1.5}.preparation-stages{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.preparation-stage{border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);overflow:hidden}.preparation-stage summary{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:11px 12px;cursor:pointer;list-style:none}.preparation-stage summary::-webkit-details-marker{display:none}.preparation-stage summary span,.preparation-stage summary strong,.preparation-stage summary small{display:block}.preparation-stage summary strong{font-size:12px}.preparation-stage summary small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.preparation-stage summary b{flex:none;color:var(--muted);font-size:9.6px;font-weight:650}.preparation-items{border-top:1px solid var(--line);background:var(--panel)}.preparation-items>a,.preparation-items>div{display:grid;grid-template-columns:22px minmax(0,1fr);gap:9px;padding:10px 12px;border-top:1px solid var(--line);text-decoration:none}.preparation-items>:first-child{border-top:0}.preparation-items strong,.preparation-items small{display:block}.preparation-items strong{font-size:10.8px}.preparation-items small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.45}.preparation-status{display:grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--surface-muted);color:var(--muted);font-size:10.8px;font-weight:800}.preparation-status.complete{background:#dcefe4;color:#125733}.preparation-status.action{background:#f7dfdc;color:#873027}.preparation-status.later{background:#f6e8c9;color:#79500f}.preparation-status.external,.preparation-status.info{background:var(--accent-soft);color:var(--accent)}.audit-preparation-error:empty{display:none}
5242
5680
  @media(max-width:900px){.obligation-board{grid-template-columns:1fr}.policy-event-list{grid-template-columns:repeat(2,minmax(0,1fr))}.policy-event-row:nth-child(3n) .policy-event-tooltip{right:auto;left:0}.policy-event-row:nth-child(2n) .policy-event-tooltip{right:0;left:auto}.packet-builder form,.packet-preflight{grid-template-columns:1fr 1fr}.packet-builder .button{align-self:end}}
5681
+ @media(max-width:900px){.occurrence-member{grid-template-columns:1fr 1fr}.occurrence-member-name,.occurrence-proof{grid-column:1/-1}}
5243
5682
  @media(max-width:1000px){.stage-overview-layout{grid-template-columns:1fr}.stage-page-grid,.group-destination-grid,.policy-activation-grid{grid-template-columns:1fr}}
5244
5683
  @media(max-width:760px){.preparation-stages{grid-template-columns:1fr}}
5245
5684
  @media(max-width:900px){.overview-grid{grid-template-columns:1fr}.overview-grid>.audit-panel{grid-column:auto}}
@@ -5280,6 +5719,9 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
5280
5719
  .missing-options{border-color:#77612f;background:#382f19;color:#ffdc92}
5281
5720
  .status-dot.neutral{background:#9aabff}
5282
5721
  }
5722
+ .record-table td:has(.relation){min-width:150px}
5723
+ .record-table .relation{border-radius:7px;line-height:1.35;overflow-wrap:anywhere;word-break:normal}
5724
+ @media(max-width:760px){.record-table td:has(.relation){min-width:0}.record-table td[data-label]>.relation{grid-column:2}}
5283
5725
  `;
5284
5726
 
5285
5727
  function safeJson(value) {