filegrc 0.13.2 → 0.13.4

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/model/v10.json CHANGED
@@ -1436,9 +1436,9 @@
1436
1436
  "policyBasis": "Policies rely on governed documents for detailed plans, schedules, procedures, charters, and reports. Approval accepts the intended values and exact content. Activation records that the linked requirements were implemented and the approved Document was put into use.",
1437
1437
  "cadence": "Complete required program Documents in Step 2, obtain approval from a reviewer who is separate from the owner, implement their linked requirements in Step 3, then activate the unchanged approved revision. Follow the linked review Obligation after activation. Prepare Audit Documents in Step 5, then approve and activate each one there as separate revision-bound updates.",
1438
1438
  "reviewPoints": [
1439
- "Write companion Markdown as a standalone company artifact. Keep FileGRC commands, record-entry instructions, readiness states, relationship IDs, and starter-library mechanics in guides and calculated work unless FileGRC itself is the document's subject.",
1440
- "Replace every bracketed prompt with a reviewed fact before approval, activation, signature, or delivery.",
1441
- "Describe the business fact in ordinary terms and keep resource relationships in the Document record and supporting records."
1439
+ "Keep companion Markdown to the policy, procedure, evidence, or narrative the Document records. Put FileGRC instructions and derived actions in the UI or guides unless FileGRC is the subject.",
1440
+ "Replace every bracketed prompt with a reviewed fact before approval, activation, signing, or delivery.",
1441
+ "Use ordinary business language; keep resource relationships in this record and its supporting records."
1442
1442
  ],
1443
1443
  "sourceResourceIds": [
1444
1444
  "policy-information-security",
@@ -1771,9 +1771,9 @@
1771
1771
  "cadence": "Create an Evidence Artifact only for a real retained artifact or approved reference. Identify its source Component or source records, Controls, collection and verification facts, coverage, revision, and classification.",
1772
1772
  "emptyState": "No Evidence Artifacts have been collected. Add one only when a real retained artifact or approved external reference exists.",
1773
1773
  "reviewPoints": [
1774
- "Use artifactKind signed-record and artifactSubtype signed-management-representation for the fixed signed management representation letter.",
1775
- "For a signed management representation, record the actual signing timestamp in businessEventAt; collectedOn records when FileGRC received the artifact, not when management signed it.",
1776
- "Use artifactKind third-party-report and artifactSubtype soc2-report for the final report issued by the CPA firm, and record the report's actual issuance timestamp in sourceGeneratedAt."
1774
+ "For a signed management representation, use artifactKind signed-record and artifactSubtype signed-management-representation.",
1775
+ "For that representation, record the signing time in businessEventAt; use collectedOn for when FileGRC received it.",
1776
+ "For a final CPA report, use artifactKind third-party-report and artifactSubtype soc2-report, then record its issuance time in sourceGeneratedAt."
1777
1777
  ],
1778
1778
  "sourceResourceIds": [
1779
1779
  "policy-information-security"
@@ -2865,9 +2865,9 @@
2865
2865
  "policyBasis": "Controls translate approved Policies and applicable Requirements into owned procedures that management can operate and prove. FileGRC does not infer technical implementation from policy prose. Configuration facts belong in Controls, Components, Systems, Obligations, and Evidence.",
2866
2866
  "cadence": "A Control may be implemented while its governing Policy, required program Document, or Training is approved but inactive. Before marking it implemented, record its owner, actual procedure in Record Markdown, bounded System scope, operation pattern, authoritative evidence-source Components, implementation date, and enabled calendar or event Obligations. Confirm each source Component is active, has the evidence role required by the Control family and current access owners, and includes repeatable retrieval instructions in Record Markdown. Enabled Obligations remain dormant until all of their governing content is active and effective.",
2867
2867
  "reviewPoints": [
2868
- "Confirm the Control is needed for the applicable Requirements, customer Commitments, and current service boundary.",
2869
- "Confirm the procedure describes what people actually do, and keep starter-selection and FileGRC record-entry instructions out of the Control statement and activity. A Control may name FileGRC when FileGRC is an actual operating Component or evidence source.",
2870
- "Use a not-applicable, external, or zero-population conclusion only when it is factually true for the current scope."
2868
+ "Confirm the Control is needed for the applicable Requirements, Commitments, and service boundary.",
2869
+ "Describe the actual procedure. Put starter-selection and FileGRC record-entry instructions in the UI or guides, unless FileGRC is an operating Component or evidence source.",
2870
+ "Use not applicable, external, or zero population only when it is true for the current scope."
2871
2871
  ],
2872
2872
  "sourceResourceIds": [
2873
2873
  "policy-information-security"
@@ -4389,9 +4389,9 @@
4389
4389
  "policyBasis": "The information security policy requires security training and added role-specific instruction when a person’s responsibilities or data access warrant it.",
4390
4390
  "cadence": "Approve the exact training content in Step 2. During Step 3, finish the linked Controls and Obligations, then activate the unchanged approved revision. Assign training through Obligations at onboarding, annually, and after relevant material changes or incidents.",
4391
4391
  "reviewPoints": [
4392
- "Write workforce-facing material as standalone company training. Keep FileGRC commands, record-entry instructions, and data-model terms in guides and assignment records.",
4393
- "State required behavior without implying that every user needs a paid product or that customer MFA applies without an approved requirement.",
4394
- "Tie each completion to the exact training revision the person reviewed."
4392
+ "Keep workforce material focused on required behavior. Put FileGRC commands, record-entry instructions, and data-model terms in guides and assignments.",
4393
+ "Do not imply every user needs a paid product or that customer MFA applies without an approved requirement.",
4394
+ "Bind each completion to the exact revision the person reviewed."
4395
4395
  ],
4396
4396
  "sourceResourceIds": [
4397
4397
  "policy-information-security"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1599,6 +1599,12 @@ function containsOpenPlaceholder(source) {
1599
1599
  function managementDocumentContentIssues(source, definition, audit) {
1600
1600
  if (!source) return ["Add the required Markdown content."];
1601
1601
  if (containsOpenPlaceholder(source)) return ["Replace every bracketed preparation placeholder before approval."];
1602
+ if (definition.kind === "soc2-management-assertion" && audit) {
1603
+ const incompatibleKind = audit.auditKind === "soc-2-type-1" ? "Type 2" : "Type 1";
1604
+ if (new RegExp(`^#{1,6}\\s+${incompatibleKind} Assertion(?: Draft)?\\s*$`, "imu").test(source)) {
1605
+ return [`Remove the ${incompatibleKind} assertion from this ${audit.auditKind === "soc-2-type-1" ? "Type 1" : "Type 2"} engagement document.`];
1606
+ }
1607
+ }
1602
1608
  const words = source.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || [];
1603
1609
  if (definition.minimumWords && words.length < definition.minimumWords) {
1604
1610
  return [`Add substantive content; this document has ${words.length} words and the preparation check expects at least ${definition.minimumWords}.`];
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Use
4
4
 
5
- This schedule records how long {{company_name}} keeps important record classes and what happens when each period ends. The policy owner and data owners must complete the organization-specific rows before approval.
5
+ This schedule records how long {{company_name}} keeps important record classes and what happens when each period ends.
6
6
 
7
7
  Retention periods may come from law, contract, tax, audit, security, or a documented business need. Use the longest applicable period, but do not keep data indefinitely without a reason.
8
8
 
@@ -10,7 +10,7 @@ Retention periods may come from law, contract, tax, audit, security, or a docume
10
10
 
11
11
  The structured Retention Schedule Items linked to this document are its schedule rows. Each approved item must name the covered Information Types and operational scope, owner, cutoff, period, disposition action, instructions, and authority. Planned items are review prompts and are not approved retention behavior.
12
12
 
13
- Management must cover important information used by Systems, Components, and Vendors, including security logs, backups or alternate recovery copies, governance records, audit evidence, customer and service records, and incident records when those classes exist. No starter period or disposition action is an approved organization value.
13
+ Management must cover important information used by Systems, Components, and Vendors, including security logs, backups or alternate recovery copies, governance records, audit evidence, customer and service records, and incident records when those classes exist. A proposed period or disposition action is not an approved organization value.
14
14
 
15
15
  ## Holds and exceptions
16
16
 
@@ -55,9 +55,10 @@ const DOCUMENT_CONTENT_UPDATES = [
55
55
  priorRevision: "45a408e8139bd57f42dda5ca5ae5c8cd4480b4e7bf08834f60058148a3a63475",
56
56
  additionalPriorRevisions: new Set([
57
57
  "d80b99ce53d1012cc169bbbc2afab8d0597bfbe9f30ac0812a8d5bbeb2ed9f90",
58
- "dd11857ae7d881f176bd93947ef3031c33c75ee41e3c0435198fd60c67a94cf7"
58
+ "dd11857ae7d881f176bd93947ef3031c33c75ee41e3c0435198fd60c67a94cf7",
59
+ "4a48c15a4e20e4f29028cf2ff8597315eb51878814120125b5268356b923c9db"
59
60
  ]),
60
- currentRevision: "4a48c15a4e20e4f29028cf2ff8597315eb51878814120125b5268356b923c9db",
61
+ currentRevision: "1738d29786cd6a027c9a2bb339ce36ce3e15c858a55ec503f4e690e65273598b",
61
62
  currentSourcePath: "./policy-library/data-retention-schedule-v2.md",
62
63
  replacements: [
63
64
  ["FileGRC detects the bracketed prompts as approval blockers. Remove each prompt only after replacing it with a reviewed fact.", "Remove each bracketed prompt only after replacing it with a reviewed fact."],
@@ -73,9 +74,10 @@ const DOCUMENT_CONTENT_UPDATES = [
73
74
  additionalPriorRevisions: new Set([
74
75
  "51a19e22f3e0b31196371676297bfa843f96295d2f6c72e81a969ce7bafc36ae",
75
76
  "5107f5c6015089a7c64e87692976aa48f727b8c03988f93f71e8f3e5084a05ac",
76
- "970b97c4f594ff54fab554460f3cf44f80ee8079e996f4d5dc1427290f8aa714"
77
+ "970b97c4f594ff54fab554460f3cf44f80ee8079e996f4d5dc1427290f8aa714",
78
+ "ff8b5764ba3073d7ab6678c7da607850de5a82de4383cb54417cd74d82c05305"
77
79
  ]),
78
- currentRevision: "ff8b5764ba3073d7ab6678c7da607850de5a82de4383cb54417cd74d82c05305",
80
+ currentRevision: "72a6049f2eed25525417fc3eb6263c231e44193d3c5cbbf6675bb98459374d0c",
79
81
  replacements: [
80
82
  ["## Reporting routes", "## Reporting channels"],
81
83
  ["The primary reporting route is {{security_contact_email}}.", "Use the current approved primary security Reporting Route. The Reporting Route record is the source of truth for its channel, destination, owner, priority, and effective period."],
@@ -90,7 +92,14 @@ const DOCUMENT_CONTENT_UPDATES = [
90
92
  ["- Restore-validation method and governed schedule", "- Restore-validation method and approved schedule"],
91
93
  ["- Dependencies, fallback paths, and validation steps", "- Dependencies, fallback paths, and validation steps\n- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision"],
92
94
  ["[Confirm or replace before activation: The starter proposal for important production data is a daily backup, 30-day retention period, and annual restore validation. Record the approved choice for every important System in its Control, Component, System, Retention Schedule, and Obligation records.]", "[Confirm or replace before activation: The proposed starting point for important production data is a daily backup, 30-day retention period, and annual restore validation. Document the approved choice for every important System in its recovery procedures and the Data Retention Schedule.]"],
93
- ["on the approved governed schedule", "on the approved schedule"]
95
+ ["on the approved governed schedule", "on the approved schedule"],
96
+ ["[Complete before activation: Describe how workers can find the fallback reporting channel when the normal email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the destination here or include plaintext credentials, private keys, tokens, or recovery codes.]", "[Describe how workers find the fallback reporting channel when the normal email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the destination here or include plaintext credentials, private keys, tokens, or recovery codes.]"],
97
+ ["The Policy Owner maintains this plan and ensures that the organization assigns these duties before activation:", "The Policy Owner maintains this plan and ensures that the organization assigns these duties:"],
98
+ ["[Complete before activation: Record the emergency contact arrangement, its owner, alternate communication channel, protected storage location, and review schedule.]", "[Describe the emergency contact arrangement, its owner, alternate communication channel, protected storage location, and review schedule.]"],
99
+ ["[Complete before activation: Identify every important System's recovery priority, dependencies, owner, backup or alternate recovery approach, and critical customer commitments. Record numeric recovery targets only when an approved commitment, included Availability criterion, or risk decision requires them.]", "[Identify every important System's recovery priority, dependencies, owner, backup or alternate recovery approach, and critical customer commitments. Record numeric recovery targets only when an approved commitment, included Availability criterion, or risk decision requires them.]"],
100
+ ["[Confirm or replace before activation: The proposed starting point for important production data is a daily backup, 30-day retention period, and annual restore validation. Document the approved choice for every important System in its recovery procedures and the Data Retention Schedule.]", "[Document the management-approved recovery approach for every important System in its recovery procedures and the Data Retention Schedule. A daily backup, 30-day retention period, and annual restore validation are examples, not approved organization values.]"],
101
+ ["[Complete before activation: Record the protected alternate location and access method responders will use when the primary identity, source-control, or collaboration Systems are unavailable. Confirm that authorized responders can retrieve the plan without exposing plaintext secrets or decryption keys.]", "[Describe the protected alternate location and access method responders use when the primary identity, source-control, or collaboration Systems are unavailable. Confirm that authorized responders can retrieve the plan without exposing plaintext secrets or decryption keys.]"],
102
+ ["- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision\n- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision", "- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision"],
94
103
  ],
95
104
  summary: "Express incident and recovery requirements as a standalone plan, keep FileGRC record-entry guidance outside it, and make numeric recovery targets conditional."
96
105
  },
@@ -98,9 +107,12 @@ const DOCUMENT_CONTENT_UPDATES = [
98
107
  id: "document-soc2-management-representation",
99
108
  path: "documents/document-soc2-management-representation.md",
100
109
  priorRevision: "59e7e5929eeb24fa6bc23e1ed1553206901c33ba249b372ad0f42282ab7a64c7",
101
- currentRevision: "83de736025d07d8085fba93a212a98e7b3e951aded9894b933eab1e77c3a0d29",
110
+ additionalPriorRevisions: new Set(["83de736025d07d8085fba93a212a98e7b3e951aded9894b933eab1e77c3a0d29"]),
111
+ currentRevision: "45fcfa1bfa280fe96c65220df48d55b2bd0f238c54a6dc18e8f2d9ccd38a7f96",
102
112
  replacements: [
103
- ["Signed letter evidence record: [Evidence ID]", "Signed letter reference: [Approved storage location or evidence reference]"]
113
+ ["Signed letter evidence record: [Evidence ID]", "Signed letter reference: [Approved storage location or evidence reference]"],
114
+ ["> The service auditor normally supplies the required representation-letter wording near the end of fieldwork. Replace this preparation note with the agreed final letter, obtain the required management signature, and retain the signed fixed-format copy as linked evidence.", "> [Insert the final management representation agreed with the service auditor.]"],
115
+ ["Before signing, reconcile the auditor's letter to:", "The signer confirms that the letter agrees with:"]
104
116
  ],
105
117
  summary: "Keep the representation-letter template independent of FileGRC relationship fields."
106
118
  },
@@ -121,11 +133,14 @@ const DOCUMENT_CONTENT_UPDATES = [
121
133
  id: "document-soc2-system-description",
122
134
  path: "documents/document-soc2-system-description.md",
123
135
  priorRevision: "dc15bc2141be0d63a832eb6f7734bc7381e77582030534bcaf636138782f8e84",
124
- currentRevision: "929a7cd87ca78a2d9caa349cad0ae23583ad79184f26ba50c1694c62549aab9e",
136
+ additionalPriorRevisions: new Set(["929a7cd87ca78a2d9caa349cad0ae23583ad79184f26ba50c1694c62549aab9e"]),
137
+ currentRevision: "0e2e0252e0e0a5be821851de1d168fbcf22bfcca41d80836c657583ab38ba154",
125
138
  replacements: [
126
139
  ["reconcile it to the filegrc records", "reconcile it to management's authoritative records"],
127
140
  ["Link the filegrc commitment records.", "Reconcile the summary to supporting commitment records."],
128
- ["Reference the selected criteria and control matrix generated by filegrc.", "Reference the selected criteria and management's control matrix."]
141
+ ["Reference the selected criteria and control matrix generated by filegrc.", "Reference the selected criteria and management's control matrix."],
142
+ ["> Draft preparation document. Complete every bracketed item, reconcile it to management's authoritative records, and have the service auditor review the final presentation.", "> [Describe the service as it existed for the stated reporting date or period.]"],
143
+ ["[Summarize customer commitments, contractual security promises, internal objectives, and the system requirements needed to meet them. Reconcile the summary to supporting commitment records.]", "[Summarize customer commitments, contractual security promises, internal objectives, and the system requirements needed to meet them.]"]
129
144
  ],
130
145
  summary: "Keep the SOC 2 System Description as a generic management deliverable."
131
146
  }
package/src/web.js CHANGED
@@ -43,7 +43,7 @@ export function renderIndex(state = null) {
43
43
  </head>
44
44
  <body>
45
45
  <a class="skip-link" href="#main">Skip to content</a>
46
- <div id="app"><div class="loading">Loading workspace…</div></div>
46
+ <div id="app"><div class="loading" role="status" aria-live="polite" aria-busy="true"><span class="spinner" aria-hidden="true"></span><span>Loading workspace…</span></div></div>
47
47
  ${snapshot}
48
48
  <script src="./filegrc-app.js" defer></script>
49
49
  </body>
@@ -98,6 +98,44 @@ let mutationStateRefreshRequested = false;
98
98
  let programSelectionGeneration = 0;
99
99
  let expiredStateRefresh = null;
100
100
 
101
+ function loadingIndicator(label) {
102
+ return '<span class="spinner" aria-hidden="true"></span><span>' + esc(label) + '</span>';
103
+ }
104
+
105
+ function setLoadingContent(element, label) {
106
+ if (!element) return;
107
+ element.classList.add("is-loading");
108
+ element.setAttribute("aria-busy", "true");
109
+ element.innerHTML = loadingIndicator(label);
110
+ }
111
+
112
+ function clearLoadingContent(element, label = "") {
113
+ if (!element) return;
114
+ element.classList.remove("is-loading");
115
+ element.removeAttribute("aria-busy");
116
+ element.textContent = label;
117
+ }
118
+
119
+ function setButtonBusy(button, busy, label = "") {
120
+ if (!button) return;
121
+ if (busy) {
122
+ if (!button.dataset.loadingIdle) {
123
+ button.dataset.loadingIdle = button.innerHTML;
124
+ button.dataset.loadingWasDisabled = String(button.disabled);
125
+ }
126
+ button.disabled = true;
127
+ setLoadingContent(button, label);
128
+ return;
129
+ }
130
+ const idle = button.dataset.loadingIdle;
131
+ const wasDisabled = button.dataset.loadingWasDisabled === "true";
132
+ clearLoadingContent(button);
133
+ if (idle !== undefined) button.innerHTML = idle;
134
+ button.disabled = wasDisabled;
135
+ delete button.dataset.loadingIdle;
136
+ delete button.dataset.loadingWasDisabled;
137
+ }
138
+
101
139
  start().catch((error) => {
102
140
  root.innerHTML = '<main class="fatal"><h1>Could Not Load the Workspace</h1><pre></pre></main>';
103
141
  root.querySelector("pre").textContent = error.stack || error.message;
@@ -188,7 +226,7 @@ function renderStateLoading(main, route, sections) {
188
226
  const message = failed
189
227
  ? state.sectionErrors?.[failed] || "Reload the workspace and try again."
190
228
  : "Records are ready. FileGRC is calculating the information needed for this page.";
191
- main.innerHTML = '<div class="page"><section class="panel state-loading" role="status" aria-live="polite"><p class="kicker">' + (failed ? "Loading error" : "One moment") + '</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p></section></div>';
229
+ main.innerHTML = '<div class="page"><section class="panel state-loading" role="' + (failed ? "alert" : "status") + '" aria-live="polite"' + (failed ? "" : ' aria-busy="true"') + '>' + (failed ? "" : '<span class="spinner spinner-large" aria-hidden="true"></span>') + '<div><p class="kicker">' + (failed ? "Loading error" : "One moment") + '</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p></div></section></div>';
192
230
  }
193
231
 
194
232
  function loadStateForRoute() {
@@ -373,13 +411,17 @@ function topbar(route) {
373
411
  const programSelect = programs.length > 1 && !state.readOnly
374
412
  ? '<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>'
375
413
  : "";
376
- 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>';
414
+ const validationMark = validationLoading && !repositoryError ? '<span class="spinner" aria-hidden="true"></span>' : '<span class="status-dot ' + validationTone + '"></span>';
415
+ const repositoryBusy = repositoryLoading && !repositoryError || state.repository?.status === "syncing";
416
+ const repositoryMark = repositoryBusy ? '<span class="spinner" aria-hidden="true"></span>' : '<span class="status-dot ' + repositoryTone + '"></span>';
417
+ 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' + (validationLoading && !repositoryError ? ' is-loading' : '') + '" href="#/repository"' + (validationLoading && !repositoryError ? ' aria-busy="true"' : '') + '>' + validationMark + validationLabel + '</a><a class="repo-chip' + (repositoryBusy ? ' is-loading' : '') + '" href="#/repository"' + (repositoryBusy ? ' aria-busy="true"' : '') + '>' + repositoryMark + esc(repositoryLabel) + '</a></div>';
377
418
  }
378
419
 
379
420
  function topbarProgramReadiness() {
380
421
  if (state.sections?.program !== "complete") {
381
- const label = state.sections?.program === "loading" ? "…" : "View";
382
- return '<a class="topbar-readiness" href="#/"><span class="topbar-readiness-copy"><span>Program readiness</span><strong>' + label + '</strong></span><span class="progress" aria-hidden="true"><span style="width:0%"></span></span></a>';
422
+ const loading = state.sections?.program !== "error";
423
+ const label = loading ? '<span class="spinner" aria-hidden="true"></span><span class="sr-only">Loading</span>' : "View";
424
+ return '<a class="topbar-readiness' + (loading ? ' is-loading' : '') + '" href="#/"' + (loading ? ' aria-busy="true"' : '') + '><span class="topbar-readiness-copy"><span>Program readiness</span><strong>' + label + '</strong></span><span class="progress" aria-hidden="true"><span style="width:0%"></span></span></a>';
383
425
  }
384
426
  const progress = dashboardProgramReadiness(state.programReadiness);
385
427
  const detail = progress.complete + " of " + progress.total + " readiness items complete";
@@ -394,7 +436,7 @@ function repositoryStatusTone(status) {
394
436
 
395
437
  function repositorySyncAlert() {
396
438
  if (state.repository?.status === "syncing") {
397
- return '<div class="repository-sync-alert syncing" role="status" aria-live="polite"><span class="status-dot neutral"></span><span><strong>Saved and committed locally.</strong> Git push is continuing in the background. Other changes unlock when synchronization finishes.</span><a href="#/repository">View status</a></div>';
439
+ return '<div class="repository-sync-alert syncing is-loading" role="status" aria-live="polite" aria-busy="true"><span class="spinner" aria-hidden="true"></span><span><strong>Saved and committed locally.</strong> Git push is continuing in the background. Other changes unlock when synchronization finishes.</span><a href="#/repository">View status</a></div>';
398
440
  }
399
441
  const message = state.repository?.backgroundSyncError;
400
442
  if (!message) return "";
@@ -805,13 +847,13 @@ function openPolicyActivationDialog() {
805
847
  }
806
848
 
807
849
  function workflowGuidance(options = {}) {
808
- const workflow = state.workflow;
850
+ const workflow = options.workflow || state.workflow;
809
851
  if (!workflow) return "";
810
852
  const matches = (item) => {
811
853
  if (options.type === "audit" && options.id && item.auditId === options.id) return true;
812
854
  if (options.stageId && item.stage !== options.stageId) return false;
813
- if (options.type && item.subject?.type !== options.type && item.source?.type !== options.type) return false;
814
- if (options.id && item.subject?.id !== options.id && item.source?.id !== options.id) return false;
855
+ if (options.type && item.subject?.type !== options.type) return false;
856
+ if (options.id && item.subject?.id !== options.id) return false;
815
857
  return true;
816
858
  };
817
859
  const activeStates = new Set(["blocked", "due", "open", "overdue", "ready", "scheduled", "upcoming", "waiting-external"]);
@@ -838,9 +880,16 @@ function workflowGuidance(options = {}) {
838
880
  const rows = visible.map(renderRow).join("");
839
881
  const remaining = items.slice(visible.length);
840
882
  const more = remaining.length
841
- ? '<details class="workflow-guidance-more"><summary>Show ' + remaining.length + ' more ' + pluralize("item", remaining.length) + '</summary><div class="workflow-findings workflow-findings-more">' + remaining.map(renderRow).join("") + '</div><p><code>filegrc workflow --json</code> returns the same checklist for headless review.</p></details>'
883
+ ? '<details class="workflow-guidance-more"><summary>Show ' + remaining.length + ' more ' + pluralize("item", remaining.length) + '</summary><div class="workflow-findings workflow-findings-more">' + remaining.map(renderRow).join("") + '</div><p><code>filegrc workflow --json</code> returns this same checklist for CLI and agent use.</p></details>'
842
884
  : "";
843
- return '<section class="workflow-guidance panel detail-support-panel detail-workflow-panel"><div class="panel-head"><div><p class="kicker">To-do</p><h3>' + esc(options.title || "Checklist") + '</h3><p>' + esc(status) + '</p></div><span class="badge ' + (blocking.length ? "warn" : "good") + '">' + (blocking.length ? "Needs work" : "Current") + '</span></div><div class="workflow-findings">' + rows + '</div>' + more + '</section>';
885
+ const completionRequirements = options.type && resourceReviewCriteria(options.type, { trigger: true });
886
+ return '<section class="workflow-guidance panel detail-support-panel detail-workflow-panel"><div class="panel-head"><div><p class="kicker">To-do</p><h3>' + esc(options.title || "Checklist") + '</h3></div><span class="badge ' + (blocking.length ? "warn" : "good") + '">' + (blocking.length ? "Needs work" : "Current") + '</span></div><p class="workflow-status">' + esc(status) + (completionRequirements ? '. ' + completionRequirements : "") + '</p><div class="workflow-findings">' + rows + '</div>' + more + '</section>';
887
+ }
888
+
889
+ function recordCompletionState(record) {
890
+ const completeStatuses = new Set(["approved", "active", "complete", "completed", "closed", "done", "reconciled"]);
891
+ if (!completeStatuses.has(record.status)) return "";
892
+ return '<section class="record-completion-state panel detail-support-panel"><p class="kicker">Record status</p><h3>' + esc(properCase(record.status)) + '</h3><p>This record has no direct next step.</p></section>';
844
893
  }
845
894
 
846
895
  function collectionReviewPanel(type) {
@@ -857,14 +906,21 @@ function collectionReviewPanel(type) {
857
906
  const reviewSummary = current
858
907
  ? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + "." + reviewNote + '</span></p>'
859
908
  : '<p class="collection-review-result"><strong>' + (needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(assessment.message) + '</span></p>';
909
+ const compactReviewSummary = current
910
+ ? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + ".</span></p>"
911
+ : reviewSummary;
860
912
  const action = state.readOnly
861
913
  ? ""
862
914
  : needsFirstRecord
863
915
  ? '<a class="button primary" href="#/resources/' + encodeURIComponent(type) + '?new=1">Add first ' + esc(state.model.resources[type].title.toLowerCase()) + '</a>'
864
916
  : '<button class="button ' + (current ? "" : "primary") + '" type="button" data-review-collection="' + esc(type) + '">' + (current ? "Review again" : "Review and confirm") + '</button>';
865
- return '<section class="collection-review-panel panel ' + (current ? "current" : "required") + '"><div class="collection-review-head"><div><p class="kicker">Scope confirmation</p><h3>' + esc(configuration.title) + '</h3><p>' + esc(configuration.description) + '</p></div><span class="badge ' + (current ? "good" : "warn") + '">' + (current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required") + '</span></div>' +
866
- '<details ' + (current ? "" : "open") + '><summary>What to review</summary><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></details>' +
867
- '<div class="collection-review-foot">' + reviewSummary + action + '</div></section>';
917
+ const status = current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required";
918
+ const details = '<details ' + (current ? 'class="collection-review-details"' : "open") + '><summary>' + (current ? "Show scope confirmation" : "What to review") + '</summary><div class="collection-review-detail-content"><div class="collection-review-head"><div><p class="kicker">Scope confirmation</p><h3>' + esc(configuration.title) + '</h3><p>' + esc(configuration.description) + '</p></div><span class="badge ' + (current ? "good" : "warn") + '">' + status + '</span></div><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>' + (current ? reviewSummary : "") + '</div></details>';
919
+ const summary = current
920
+ ? '<div class="collection-review-current-row"><div class="collection-review-complete-summary"><span class="kicker">Scope confirmation</span>' + compactReviewSummary + '</div>' + details + action + '</div>'
921
+ : details;
922
+ return '<section class="collection-review-panel panel ' + (current ? "current" : "required") + '">' + summary +
923
+ (current ? "" : '<div class="collection-review-foot">' + reviewSummary + action + '</div>') + '</section>';
868
924
  }
869
925
 
870
926
  function collectionNeedsFirstRecord(type) {
@@ -889,13 +945,31 @@ function collectionEmptyState(type, definition) {
889
945
  : "No records exist. Use the scope confirmation above to record an allowed empty-collection conclusion, or add the records management identified.";
890
946
  }
891
947
 
892
- function resourceReviewCriteria(type, collapsed = false) {
948
+ function resourceReviewCriteria(type, options = {}) {
949
+ const collapsed = options === true || options.collapsed;
950
+ const trigger = typeof options === "object" && options.trigger;
951
+ const title = typeof options === "object" && options.title ? options.title : "Review criteria";
893
952
  const reviewPoints = state.model.resources[type]?.guidance?.reviewPoints || [];
894
953
  if (!reviewPoints.length) return "";
954
+ if (trigger) return '<button type="button" class="workflow-completion-requirements" data-completion-requirements="' + esc(type) + '">Completion requirements</button>';
895
955
  const points = '<ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>';
896
956
  return collapsed
897
- ? '<details class="resource-review-criteria compact"><summary>Review criteria</summary>' + points + '</details>'
898
- : '<section class="resource-review-criteria panel detail-support-panel detail-review-panel"><div class="panel-head"><h3>Review criteria</h3></div>' + points + '</section>';
957
+ ? '<details class="resource-review-criteria compact"><summary>' + esc(title) + '</summary>' + points + '</details>'
958
+ : '<section class="resource-review-criteria panel detail-support-panel detail-review-panel"><div class="panel-head"><h3>' + esc(title) + '</h3></div>' + points + '</section>';
959
+ }
960
+
961
+ function openCompletionRequirementsDialog(type) {
962
+ const definition = state.model.resources[type];
963
+ const reviewPoints = definition?.guidance?.reviewPoints || [];
964
+ if (!reviewPoints.length) return;
965
+ const dialog = document.createElement("dialog");
966
+ dialog.className = "alert-dialog completion-requirements-dialog";
967
+ dialog.innerHTML = '<div class="dialog-head"><div><p class="kicker">Completion requirements</p><h2>' + esc(titleCase(definition.title)) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>Check these before considering this record complete.</p><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul><div class="dialog-actions"><button type="button" class="button primary">Close</button></div>';
968
+ document.body.append(dialog);
969
+ const close = () => dialog.close();
970
+ dialog.querySelectorAll("button").forEach((button) => button.addEventListener("click", close));
971
+ dialog.addEventListener("close", () => dialog.remove(), { once: true });
972
+ dialog.showModal();
899
973
  }
900
974
 
901
975
  function recordWorkflowItems(type, id) {
@@ -992,8 +1066,10 @@ function openCollectionReviewDialog(type) {
992
1066
  expectedRevision: assessment.reviewRevision || undefined
993
1067
  };
994
1068
  form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = true; });
1069
+ const submit = form.querySelector('button[type="submit"]');
1070
+ setButtonBusy(submit, true, "Saving…");
995
1071
  try {
996
- saveStatus.textContent = "Validating and saving…";
1072
+ setLoadingContent(saveStatus, "Validating and saving…");
997
1073
  const prefetch = await repositoryPrefetch;
998
1074
  if (prefetch?.error) throw prefetch.error;
999
1075
  const response = await localFetch("/api/collection-review", {
@@ -1002,14 +1078,15 @@ function openCollectionReviewDialog(type) {
1002
1078
  body: JSON.stringify({ ...payload, confirmed: true, prefetchToken: prefetch?.token })
1003
1079
  });
1004
1080
  if (!response.ok) throw new Error(await responseMessage(response));
1005
- saveStatus.textContent = "Saved locally. Refreshing page…";
1081
+ setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
1006
1082
  applyMutationState(await response.json());
1007
1083
  dialog.close();
1008
1084
  render();
1009
1085
  } catch (requestError) {
1010
- saveStatus.textContent = "Not saved";
1086
+ clearLoadingContent(saveStatus, "Not saved");
1011
1087
  error.textContent = requestError.message;
1012
1088
  } finally {
1089
+ setButtonBusy(submit, false);
1013
1090
  form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = false; });
1014
1091
  }
1015
1092
  });
@@ -1920,8 +1997,7 @@ async function openObligationRuleActivation(ruleId) {
1920
1997
  const approvedByIds = [...form.querySelectorAll('input[name="approver"]:checked')].map(({ value }) => value);
1921
1998
  if (!approvedByIds.length) return void (error.textContent = "Select at least one approver.");
1922
1999
  const button = form.querySelector('button[type="submit"]');
1923
- button.disabled = true;
1924
- button.textContent = "Activating…";
2000
+ setButtonBusy(button, true, "Activating…");
1925
2001
  error.textContent = "";
1926
2002
  try {
1927
2003
  const saved = await localFetch("/api/obligation-rule-activations", {
@@ -1942,8 +2018,7 @@ async function openObligationRuleActivation(ruleId) {
1942
2018
  render();
1943
2019
  } catch (caught) {
1944
2020
  error.textContent = caught.message;
1945
- button.disabled = false;
1946
- button.textContent = "Activate schedule";
2021
+ setButtonBusy(button, false);
1947
2022
  }
1948
2023
  });
1949
2024
  } catch (error) {
@@ -2079,6 +2154,8 @@ function openObligationEventDialog(trigger) {
2079
2154
  const form = event.currentTarget;
2080
2155
  if (!form.reportValidity()) return;
2081
2156
  form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
2157
+ const submit = form.querySelector('button[type="submit"]');
2158
+ setButtonBusy(submit, true, "Adding tasks…");
2082
2159
  try {
2083
2160
  const response = await localFetch("/api/obligation-events", {
2084
2161
  method: "POST",
@@ -2103,6 +2180,7 @@ function openObligationEventDialog(trigger) {
2103
2180
  history.replaceState(null, "", "#/stage/run");
2104
2181
  render();
2105
2182
  } catch (error) {
2183
+ setButtonBusy(submit, false);
2106
2184
  form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
2107
2185
  form.querySelector(".dialog-error").textContent = error.message;
2108
2186
  }
@@ -2135,6 +2213,8 @@ function openExternalReviewerGovernanceDialog() {
2135
2213
  const error = dialog.querySelector(".dialog-error");
2136
2214
  error.textContent = "";
2137
2215
  form.querySelectorAll("button,input,textarea").forEach((control) => { control.disabled = true; });
2216
+ const submit = form.querySelector('[data-preview-governance]');
2217
+ setButtonBusy(submit, true, previewedPayload ? "Applying…" : "Preparing preview…");
2138
2218
  try {
2139
2219
  if (!previewedPayload) {
2140
2220
  const response = await localFetch("/api/external-reviewer-governance/preview", {
@@ -2161,6 +2241,8 @@ function openExternalReviewerGovernanceDialog() {
2161
2241
  } catch (requestError) {
2162
2242
  error.textContent = requestError.message;
2163
2243
  } finally {
2244
+ setButtonBusy(submit, false);
2245
+ submit.textContent = previewedPayload ? "Confirm and apply" : "Preview bundle";
2164
2246
  form.querySelectorAll("button,input,textarea").forEach((control) => { control.disabled = false; });
2165
2247
  }
2166
2248
  });
@@ -2197,6 +2279,8 @@ function openNextAuditCycleDialog(prior) {
2197
2279
  const error = dialog.querySelector(".dialog-error");
2198
2280
  error.textContent = "";
2199
2281
  form.querySelectorAll("button,input").forEach((control) => { control.disabled = true; });
2282
+ const submit = form.querySelector("[data-preview-cycle]");
2283
+ setButtonBusy(submit, true, previewedPayload ? "Creating…" : "Preparing preview…");
2200
2284
  try {
2201
2285
  if (!previewedPayload) {
2202
2286
  const response = await localFetch("/api/audit-cycle/preview", {
@@ -2225,6 +2309,8 @@ function openNextAuditCycleDialog(prior) {
2225
2309
  } catch (requestError) {
2226
2310
  error.textContent = requestError.message;
2227
2311
  } finally {
2312
+ setButtonBusy(submit, false);
2313
+ submit.textContent = previewedPayload ? "Confirm and create" : "Preview carry-forward";
2228
2314
  form.querySelectorAll("button,input").forEach((control) => { control.disabled = false; });
2229
2315
  }
2230
2316
  });
@@ -2314,6 +2400,8 @@ function openApplicabilityReviewDialog(type, entries) {
2314
2400
  ])
2315
2401
  };
2316
2402
  form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
2403
+ const submit = form.querySelector("[data-preview-review]");
2404
+ setButtonBusy(submit, true, previewedPayload ? "Saving…" : "Preparing preview…");
2317
2405
  try {
2318
2406
  if (!previewedPayload) {
2319
2407
  const response = await localFetch("/api/applicability-review/preview", {
@@ -2327,7 +2415,7 @@ function openApplicabilityReviewDialog(type, entries) {
2327
2415
  dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>' + preview.reviewedIds.length + ' ' + pluralize("decision", preview.reviewedIds.length) + ' will be saved with the reviewer, review date, and current scope recorded automatically.</p>';
2328
2416
  dialog.querySelector("[data-preview-review]").textContent = "Confirm and save";
2329
2417
  } else {
2330
- saveStatus.textContent = "Validating and saving…";
2418
+ setLoadingContent(saveStatus, "Validating and saving…");
2331
2419
  const prefetch = await repositoryPrefetch;
2332
2420
  if (prefetch?.error) throw prefetch.error;
2333
2421
  const response = await localFetch("/api/applicability-review", {
@@ -2336,14 +2424,17 @@ function openApplicabilityReviewDialog(type, entries) {
2336
2424
  body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
2337
2425
  });
2338
2426
  if (!response.ok) throw new Error(await responseMessage(response));
2339
- saveStatus.textContent = "Saved locally. Refreshing page…";
2427
+ setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
2340
2428
  applyMutationState(await response.json());
2341
2429
  dialog.close();
2342
2430
  render();
2343
2431
  }
2344
2432
  } catch (requestError) {
2433
+ clearLoadingContent(saveStatus, "Not saved");
2345
2434
  error.textContent = requestError.message;
2346
2435
  } finally {
2436
+ setButtonBusy(submit, false);
2437
+ submit.textContent = previewedPayload ? "Confirm and save" : "Preview decisions";
2347
2438
  form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
2348
2439
  }
2349
2440
  });
@@ -2388,8 +2479,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
2388
2479
  main.querySelector("#initialize-audit-work")?.addEventListener("click", async (event) => {
2389
2480
  const button = event.currentTarget;
2390
2481
  const error = main.querySelector(".audit-preparation-error");
2391
- button.disabled = true;
2392
- button.textContent = "Initializing…";
2482
+ setButtonBusy(button, true, "Initializing…");
2393
2483
  error.textContent = "";
2394
2484
  try {
2395
2485
  const response = await localFetch("/api/audit-preparation", {
@@ -2402,8 +2492,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
2402
2492
  render();
2403
2493
  } catch (caught) {
2404
2494
  error.textContent = caught.message;
2405
- button.disabled = false;
2406
- button.textContent = "Initialize audit work";
2495
+ setButtonBusy(button, false);
2407
2496
  }
2408
2497
  });
2409
2498
  main.querySelector("#packet-form").addEventListener("submit", async (event) => {
@@ -2412,8 +2501,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
2412
2501
  if (!form.reportValidity()) return;
2413
2502
  const button = form.querySelector('button[type="submit"]');
2414
2503
  const error = main.querySelector(".dialog-error");
2415
- button.disabled = true;
2416
- button.textContent = "Generating…";
2504
+ setButtonBusy(button, true, "Generating…");
2417
2505
  error.textContent = "";
2418
2506
  try {
2419
2507
  const response = await localFetch("/api/evidence-packet", {
@@ -2434,8 +2522,8 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
2434
2522
  } catch (caught) {
2435
2523
  error.textContent = caught.message;
2436
2524
  } finally {
2525
+ setButtonBusy(button, false);
2437
2526
  button.disabled = state.readOnly;
2438
- button.textContent = draft ? "Generate draft" : "Generate packet";
2439
2527
  }
2440
2528
  });
2441
2529
  if (latestPacketResult && latestPacketState === state && latestPacketResult.packet.audit?.id === selected?.id) {
@@ -2707,7 +2795,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2707
2795
  const definition = state.model.resources[type];
2708
2796
  if (!entry || !definition) return renderNotFound(main);
2709
2797
  if (entry.detailsLoaded === false) {
2710
- 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><section class="panel detail-loading" role="status">Loading record…</section></div>';
2798
+ 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><section class="panel detail-loading is-loading" role="status" aria-live="polite" aria-busy="true">' + loadingIndicator("Loading record…") + '</section></div>';
2711
2799
  loadResourceDetail(type, id);
2712
2800
  return;
2713
2801
  }
@@ -2779,12 +2867,13 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2779
2867
  ? '<section class="panel detail-history-panel"><div class="panel-head"><h3>File History</h3></div><div class="history">' + entry.history.map((commit) => '<div><code>' + esc(commit.shortCommit) + '</code><span><strong>' + esc(commit.subject) + '</strong><small>' + esc(commit.author) + ' · ' + esc(formatLocalDateTime(commit.timestamp)) + '</small></span></div>').join("") + '</div></section>'
2780
2868
  : "";
2781
2869
  const participationPanel = entry.historyLoaded === false
2782
- ? '<section class="panel detail-support-panel" role="status"><div class="panel-head"><h3>Participation</h3></div><p class="muted">Loading participation and file history…</p></section>'
2783
- : personParticipation(entry);
2870
+ ? '<section class="panel detail-support-panel" role="status" aria-live="polite" aria-busy="true"><div class="panel-head"><h3>Participation</h3></div><p class="muted is-loading">' + loadingIndicator("Loading participation and file history…") + '</p></section>'
2871
+ : entry.historyError
2872
+ ? '<section class="panel detail-support-panel" role="status" aria-live="polite"><div class="panel-head"><h3>Participation</h3></div><p class="muted">Participation and file history are unavailable. Reload the record to try again.</p></section>'
2873
+ : personParticipation(entry);
2784
2874
  const supportPanels = renderDetailSupport({
2785
2875
  hasRecordBody,
2786
- workflowPanel: workflowGuidance({ type, id, title: "Next steps" }),
2787
- reviewPanel: resourceReviewCriteria(type),
2876
+ workflowPanel: workflowGuidance({ type, id, title: "Next steps", workflow: entry.workflow }) || recordCompletionState(entry.record),
2788
2877
  metadataPanel: '<section class="panel detail-support-panel detail-metadata-panel"><div class="panel-head"><h3>Record details</h3></div><dl class="metadata">' + sourceMetadata + visible.map(([name, value]) => '<div><dt>' + esc(fields[name]?.label || humanize(name)) + '</dt><dd>' + formatValue(name === "status" ? displayStatus(entry.record) : value, name, type) + '</dd></div>').join("") + '</dl></section>',
2789
2878
  attachmentPanel,
2790
2879
  participationPanel,
@@ -2792,6 +2881,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2792
2881
  historyPanel
2793
2882
  });
2794
2883
  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>';
2884
+ main.querySelectorAll("[data-completion-requirements]").forEach((button) => button.addEventListener("click", () => openCompletionRequirementsDialog(button.dataset.completionRequirements)));
2795
2885
  main.querySelector("#edit-resource")?.addEventListener("click", () => openEditor(type, entry));
2796
2886
  main.querySelector("[data-external-reviewer-governance]")?.addEventListener("click", openExternalReviewerGovernanceDialog);
2797
2887
  main.querySelector("[data-next-audit-cycle]")?.addEventListener("click", () => openNextAuditCycleDialog(entry.record));
@@ -2929,11 +3019,11 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
2929
3019
  }
2930
3020
  }
2931
3021
 
2932
- function renderDetailSupport({ hasRecordBody, workflowPanel, reviewPanel, metadataPanel, attachmentPanel, participationPanel, connectionsPanel, historyPanel }) {
2933
- const panels = workflowPanel + reviewPanel + metadataPanel + attachmentPanel + participationPanel + connectionsPanel + historyPanel;
3022
+ function renderDetailSupport({ hasRecordBody, workflowPanel, metadataPanel, attachmentPanel, participationPanel, connectionsPanel, historyPanel }) {
3023
+ const panels = workflowPanel + metadataPanel + attachmentPanel + participationPanel + connectionsPanel + historyPanel;
2934
3024
  if (hasRecordBody) return '<aside>' + panels + '</aside>';
2935
3025
  const stacks = [
2936
- { name: "guidance", content: workflowPanel + reviewPanel },
3026
+ { name: "guidance", content: workflowPanel },
2937
3027
  { name: "record", content: metadataPanel + attachmentPanel + historyPanel },
2938
3028
  { name: "relationships", content: participationPanel + connectionsPanel }
2939
3029
  ].filter(({ content }) => content);
@@ -2965,7 +3055,7 @@ async function loadResourceDetail(type, id) {
2965
3055
  let token = state.stateToken;
2966
3056
  let detail = null;
2967
3057
  for (let attempt = 0; attempt < 3; attempt += 1) {
2968
- const tokenQuery = token ? "?token=" + encodeURIComponent(token) + "&history=false" : "?history=false";
3058
+ const tokenQuery = token ? "?token=" + encodeURIComponent(token) + "&history=false&workflow=true" : "?history=false&workflow=true";
2969
3059
  const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
2970
3060
  if (response.status === 409 && token) {
2971
3061
  await refreshExpiredAppState(token);
@@ -3007,8 +3097,8 @@ async function loadResourceHistory(type, id) {
3007
3097
  const key = (state.stateToken || "live") + "\0history\0" + type + "\0" + id;
3008
3098
  if (resourceDetailRequests.has(key)) return resourceDetailRequests.get(key);
3009
3099
  const request = (async () => {
3100
+ const token = state.stateToken;
3010
3101
  try {
3011
- const token = state.stateToken;
3012
3102
  const tokenQuery = (token ? "?token=" + encodeURIComponent(token) + "&" : "?") + "history=only";
3013
3103
  const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
3014
3104
  if (response.status === 409 && token) {
@@ -3029,8 +3119,15 @@ async function loadResourceHistory(type, id) {
3029
3119
  entry.historyLoaded = true;
3030
3120
  const route = parseRoute();
3031
3121
  if (route.name === "detail" && route.type === type && route.id === id) render();
3032
- } catch {
3033
- // History and participation are supplemental. The record stays usable.
3122
+ } catch (error) {
3123
+ if (token && state.stateToken !== token) return;
3124
+ const entry = state.resources.find(({ record }) => record.type === type && record.id === id);
3125
+ if (entry) {
3126
+ entry.historyLoaded = true;
3127
+ entry.historyError = error.message;
3128
+ }
3129
+ const route = parseRoute();
3130
+ if (route.name === "detail" && route.type === type && route.id === id) render();
3034
3131
  } finally {
3035
3132
  for (const [requestKey, pending] of resourceDetailRequests) {
3036
3133
  if (pending === request) resourceDetailRequests.delete(requestKey);
@@ -3423,8 +3520,8 @@ async function runRepositoryGitAction(action) {
3423
3520
  status.textContent = "";
3424
3521
  status.classList.remove("error");
3425
3522
  }
3523
+ if (active) setButtonBusy(active, true, label);
3426
3524
  buttons.forEach((button) => { button.disabled = true; });
3427
- if (active) active.textContent = label;
3428
3525
  try {
3429
3526
  const response = await localFetch("/api/git/" + action, { method: "POST" });
3430
3527
  if (!response.ok) throw new Error(await responseMessage(response));
@@ -3440,8 +3537,8 @@ async function runRepositoryGitAction(action) {
3440
3537
  ? "Synchronized " + result.shortCommit + " with " + result.upstream + "."
3441
3538
  : "Pushed " + result.shortCommit + " to " + result.upstream + ".";
3442
3539
  } catch (cause) {
3540
+ if (active) setButtonBusy(active, false);
3443
3541
  buttons.forEach((button, index) => { button.disabled = disabled[index]; });
3444
- if (active) active.textContent = action === "pull" ? "Check incoming commits" : action === "retry-sync" ? "Retry sync" : "Push";
3445
3542
  if (status) {
3446
3543
  status.textContent = cause.message;
3447
3544
  status.classList.add("error");
@@ -3467,8 +3564,10 @@ function openCommitDialog() {
3467
3564
  const form = event.currentTarget;
3468
3565
  if (!form.reportValidity()) return;
3469
3566
  const button = form.querySelector('button[type="submit"]');
3470
- form.querySelectorAll("button,input").forEach((control) => { control.disabled = true; });
3471
- button.textContent = busyLabel;
3567
+ const controls = [...form.querySelectorAll("button,input")];
3568
+ const disabled = controls.map((control) => control.disabled);
3569
+ setButtonBusy(button, true, busyLabel);
3570
+ controls.forEach((control) => { control.disabled = true; });
3472
3571
  try {
3473
3572
  const response = await localFetch("/api/commit", {
3474
3573
  method: "POST",
@@ -3490,8 +3589,8 @@ function openCommitDialog() {
3490
3589
  status.classList.toggle("error", !result.pushed && !result.pushSkipped);
3491
3590
  }
3492
3591
  } catch (error) {
3493
- form.querySelectorAll("button,input").forEach((control) => { control.disabled = false; });
3494
- button.textContent = actionLabel;
3592
+ setButtonBusy(button, false);
3593
+ controls.forEach((control, index) => { control.disabled = disabled[index]; });
3495
3594
  form.querySelector(".dialog-error").textContent = error.message;
3496
3595
  }
3497
3596
  });
@@ -4072,9 +4171,11 @@ function setOnboardingBusy(busy, label = "") {
4072
4171
  onboardingBusy = busy;
4073
4172
  onboardingDialog.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = busy; });
4074
4173
  const next = onboardingDialog.querySelector('[data-onboarding="next"]');
4075
- if (next && label) next.textContent = label;
4174
+ const draft = onboardingDialog.querySelector('[data-onboarding="draft"]');
4076
4175
  const skip = onboardingDialog.querySelector('[data-onboarding="skip"]');
4077
- if (skip && label && onboardingStep !== onboardingSteps().length - 1) skip.textContent = label;
4176
+ const retry = onboardingDialog.querySelector(".onboarding-retry-sync");
4177
+ const active = label === "Saving draft…" ? draft : label === "Skipping…" ? skip : label === "Retrying sync…" ? retry : next;
4178
+ if (busy && active && label) setLoadingContent(active, label);
4078
4179
  if (busy && onboardingStep === onboardingSteps().length - 1) {
4079
4180
  onboardingStillWorkingTimer = setTimeout(() => {
4080
4181
  const status = onboardingDialog?.querySelector(".onboarding-save-status");
@@ -4754,12 +4855,8 @@ function wireStructuredObjectEditors(dialog) {
4754
4855
  dialog.querySelector("form").addEventListener("click", (event) => {
4755
4856
  const bind = event.target.closest("[data-bind-review-revisions]");
4756
4857
  if (bind) {
4757
- const previousLabel = bind.textContent;
4758
4858
  bindCurrentReviewRevisions(dialog, bind).catch((error) => {
4759
4859
  dialog.querySelector(".dialog-error").textContent = error.message;
4760
- }).finally(() => {
4761
- bind.disabled = false;
4762
- if (bind.textContent === "Binding…") bind.textContent = previousLabel;
4763
4860
  });
4764
4861
  return;
4765
4862
  }
@@ -4835,23 +4932,28 @@ async function bindCurrentReviewRevisions(dialog, button) {
4835
4932
  throw new Error("Select the retention schedule, Information Types, and operational scope before binding their current revisions.");
4836
4933
  }
4837
4934
  const ids = [...new Set(Object.values(selected).flat().concat(scheduleDocumentId, retentionUseReviewIds(dialog)).filter(Boolean))];
4838
- button.disabled = true;
4839
- button.textContent = "Binding…";
4840
- const query = ids.map((id) => "id=" + encodeURIComponent(id)).join("&");
4841
- const response = await localFetch("/api/review-revisions?" + query);
4842
- if (!response.ok) throw new Error(await responseMessage(response));
4843
- const { revisions } = await response.json();
4844
- const editor = button.closest(".string-map-editor");
4845
- const items = editor.querySelector(".string-map-items");
4846
- items.replaceChildren();
4847
- for (const [key, value] of Object.entries(revisions)) {
4848
- items.append(editor.querySelector("template").content.cloneNode(true));
4849
- const row = items.lastElementChild;
4850
- row.querySelector("[data-map-key]").value = key;
4851
- row.querySelector("[data-map-value]").value = value;
4935
+ setButtonBusy(button, true, "Binding…");
4936
+ let bound = false;
4937
+ try {
4938
+ const query = ids.map((id) => "id=" + encodeURIComponent(id)).join("&");
4939
+ const response = await localFetch("/api/review-revisions?" + query);
4940
+ if (!response.ok) throw new Error(await responseMessage(response));
4941
+ const { revisions } = await response.json();
4942
+ const editor = button.closest(".string-map-editor");
4943
+ const items = editor.querySelector(".string-map-items");
4944
+ items.replaceChildren();
4945
+ for (const [key, value] of Object.entries(revisions)) {
4946
+ items.append(editor.querySelector("template").content.cloneNode(true));
4947
+ const row = items.lastElementChild;
4948
+ row.querySelector("[data-map-key]").value = key;
4949
+ row.querySelector("[data-map-value]").value = value;
4950
+ }
4951
+ bound = true;
4952
+ dialog.querySelector(".dialog-error").textContent = "";
4953
+ } finally {
4954
+ setButtonBusy(button, false);
4955
+ if (bound) button.textContent = "Refresh current revisions";
4852
4956
  }
4853
- button.textContent = "Refresh current revisions";
4854
- dialog.querySelector(".dialog-error").textContent = "";
4855
4957
  }
4856
4958
 
4857
4959
  function retentionUseReviewIds(dialog) {
@@ -5698,16 +5800,16 @@ function prefetchRepositoryForReview(status = null) {
5698
5800
  if (state.repository?.mode !== "trunk" || state.repository?.developmentOverride) {
5699
5801
  return Promise.resolve(null);
5700
5802
  }
5701
- if (status) status.textContent = "Checking repository…";
5803
+ if (status) setLoadingContent(status, "Checking repository…");
5702
5804
  return fetch("/api/git/prefetch", { method: "POST" })
5703
5805
  .then(async (response) => {
5704
5806
  if (!response.ok) throw new Error(await responseMessage(response));
5705
5807
  const result = await response.json();
5706
- if (status) status.textContent = result.status === "checked" ? "Repository checked" : "Ready to save";
5808
+ if (status) clearLoadingContent(status, result.status === "checked" ? "Repository checked" : "Ready to save");
5707
5809
  return result;
5708
5810
  })
5709
5811
  .catch((error) => {
5710
- if (status) status.textContent = "Repository check failed";
5812
+ if (status) clearLoadingContent(status, "Repository check failed");
5711
5813
  return null;
5712
5814
  });
5713
5815
  }
@@ -5756,7 +5858,10 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
5756
5858
  clearTimeout(dialog._stillWorkingTimer);
5757
5859
  dialog._stillWorkingTimer = null;
5758
5860
  dialog.dataset.mutationBusy = busy ? "true" : "false";
5861
+ const button = dialog.querySelector("#save-record,#save-content,#activate-policy") || dialog.querySelector('button[type="submit"]');
5862
+ if (busy && button) setButtonBusy(button, true, label);
5759
5863
  dialog.querySelectorAll("button,input,select,textarea").forEach((control) => {
5864
+ if (control === button) return;
5760
5865
  if (busy) {
5761
5866
  control.dataset.mutationWasDisabled = control.disabled ? "true" : "false";
5762
5867
  control.disabled = true;
@@ -5765,13 +5870,15 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
5765
5870
  delete control.dataset.mutationWasDisabled;
5766
5871
  }
5767
5872
  });
5768
- const button = dialog.querySelector("#save-record,#save-content,#activate-policy");
5769
- if (button) button.textContent = busy ? label : idleLabel;
5873
+ if (!busy && button) {
5874
+ setButtonBusy(button, false);
5875
+ if (idleLabel) button.textContent = idleLabel;
5876
+ }
5770
5877
  const status = dialog.querySelector(".save-status");
5771
- if (status) status.textContent = "";
5878
+ if (status) clearLoadingContent(status);
5772
5879
  if (busy) {
5773
5880
  dialog._stillWorkingTimer = setTimeout(() => {
5774
- if (status) status.textContent = "Still working. Recalculating readiness and repository state.";
5881
+ if (status) setLoadingContent(status, "Still working. Recalculating readiness and repository state.");
5775
5882
  }, 1_500);
5776
5883
  }
5777
5884
  }
@@ -5815,6 +5922,8 @@ async function responseMessage(response) {
5815
5922
  try { return JSON.parse(source).error || source; } catch { return source; }
5816
5923
  }
5817
5924
  async function localFetch(url, options) {
5925
+ const requestProgramId = state?.selectedProgramId || null;
5926
+ const requestProgramGeneration = programSelectionGeneration;
5818
5927
  const requestUrl = new URL(url, location.origin);
5819
5928
  if (state?.selectedProgramId && !requestUrl.searchParams.has("programId")) {
5820
5929
  requestUrl.searchParams.set("programId", state.selectedProgramId);
@@ -5837,17 +5946,24 @@ async function localFetch(url, options) {
5837
5946
  const chip = synchronizing ? document.querySelector(".repo-chip") : null;
5838
5947
  const previousChip = chip?.innerHTML;
5839
5948
  let repositoryRefreshed = false;
5840
- if (chip) chip.innerHTML = '<span class="status-dot neutral"></span>Syncing';
5949
+ if (chip) {
5950
+ chip.classList.add("is-loading");
5951
+ chip.setAttribute("aria-busy", "true");
5952
+ chip.innerHTML = loadingIndicator("Syncing");
5953
+ }
5841
5954
  try {
5842
5955
  const response = await fetch(scopedUrl, requestOptions);
5843
5956
  if (synchronizing && !response.ok) {
5844
5957
  try {
5845
- const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(state.selectedProgramId || ""));
5958
+ const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(requestProgramId || ""));
5846
5959
  if (stateResponse.ok) {
5847
- state = await stateResponse.json();
5960
+ const refreshedState = normalizeAppState(await stateResponse.json());
5961
+ if (programSelectionGeneration !== requestProgramGeneration || (state?.selectedProgramId || null) !== requestProgramId) return response;
5962
+ state = refreshedState;
5963
+ repositoryRefreshed = true;
5964
+ scheduleRepositorySyncPoll();
5848
5965
  if (chip?.isConnected) {
5849
5966
  chip.innerHTML = '<span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span>' + esc(state.repository.label);
5850
- repositoryRefreshed = true;
5851
5967
  }
5852
5968
  }
5853
5969
  } catch {
@@ -5858,7 +5974,11 @@ async function localFetch(url, options) {
5858
5974
  } catch {
5859
5975
  throw new Error("The filegrc server is unavailable. Restart npm run serve, or pnpm dev in the monorepo, and try again.");
5860
5976
  } finally {
5861
- if (!repositoryRefreshed && chip?.isConnected && previousChip) chip.innerHTML = previousChip;
5977
+ if (chip?.isConnected) {
5978
+ chip.classList.remove("is-loading");
5979
+ chip.removeAttribute("aria-busy");
5980
+ if (!repositoryRefreshed && previousChip) chip.innerHTML = previousChip;
5981
+ }
5862
5982
  }
5863
5983
  }
5864
5984
  async function fetchJson(url, options) { const response = await localFetch(url, options); if (!response.ok) throw new Error(await responseMessage(response)); return response.json(); }
@@ -5867,9 +5987,11 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
5867
5987
 
5868
5988
  export const APP_STYLES = String.raw`
5869
5989
  :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}
5870
- *{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}
5990
+ *{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}@keyframes spinner-rotate{to{transform:rotate(360deg)}}.spinner{display:inline-block;flex:0 0 auto;width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-rotate .7s linear infinite}.spinner-large{width:30px;height:30px;border-width:3px}.loading,.detail-loading,.muted.is-loading,.save-status.is-loading,.onboarding-save-status.is-loading,.button.is-loading{display:flex;align-items:center;gap:9px}.loading{min-height:100vh;justify-content:center;color:var(--muted)}.state-loading{display:flex;align-items:flex-start;gap:16px}.state-loading>.spinner{margin-top:5px;color:var(--accent)}.state-loading h2{margin:5px 0 8px}.state-loading p:last-child{margin:0;color:var(--muted)}.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:2px}.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}
5871
5991
  .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)}
5872
- .metadata>div{grid-template-columns:minmax(140px,1fr) minmax(0,2.5fr)}.metadata dt{min-width:0;overflow-wrap:anywhere}
5992
+ .metadata>div{grid-template-columns:minmax(140px,1fr) minmax(0,2.5fr)}.metadata dt{min-width:0;overflow-wrap:anywhere}.editor textarea{tab-size:2}
5993
+ .is-loading>.spinner,.state-loading>.spinner{align-self:center;margin-top:0}.state-loading,.repository-sync-alert.is-loading{align-items:center}.topbar-readiness.is-loading .topbar-readiness-copy>strong{display:flex;align-items:center;justify-content:center}
5994
+ @media(prefers-reduced-motion:reduce){.spinner{animation:none}}
5873
5995
  .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}
5874
5996
  .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)}
5875
5997
  html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(0,1fr);height:100vh;min-height:0}.sidebar{display:flex;flex-direction:column;height:100vh;overflow:hidden;overscroll-behavior:contain}.workspace{height:100vh;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
@@ -5881,6 +6003,9 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
5881
6003
  .stage-overview-hero{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;align-items:center;padding:26px 28px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stage-overview-hero h2,.group-overview-head h2{font:500 37.2px Georgia,serif;margin:7px 0}.stage-overview-hero>div>p:not(.kicker),.group-overview-head>div>p:not(.kicker){max-width:760px;color:var(--muted);font-size:14.4px;line-height:1.55;margin:0}.stage-progress-card{padding:15px 17px;background:var(--paper);border:1px solid var(--line);border-radius:9px}.stage-progress-card>div:first-child{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}.stage-progress-card>div:first-child>strong{font:500 33.6px Georgia,serif}.stage-progress-card p{color:var(--muted);font-size:10.8px;line-height:1.45;margin:9px 0 0}.badge.neutral{background:#e5e8f2;color:#555e73}.stage-overview-layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:15px;margin-top:15px;align-items:start}.stage-plan ol,.group-plan ol{display:grid;gap:12px;padding-left:20px;margin:0}.stage-plan li,.group-plan li{padding-left:4px;color:var(--ink);font-size:13.2px;line-height:1.5}.stage-groups{min-width:0}.stage-groups>.section-head{margin:4px 0 13px}.stage-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-group-card{position:relative;display:block;min-height:138px;padding:18px 38px 17px 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.stage-group-card:hover{border-color:var(--accent-light)}.stage-group-card h3{font-size:15.6px;margin:0 0 7px}.stage-group-card p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.stage-group-card small{display:block;color:var(--accent);font-size:9.6px;font-weight:700;margin-top:12px}.stage-group-arrow{position:absolute;right:16px;top:16px;color:var(--accent);font-size:24px}.group-overview-head{display:flex;justify-content:space-between;align-items:end;gap:25px;margin-bottom:15px}.stage-status-link{display:grid;grid-template-columns:auto auto;align-items:center;gap:4px 12px;min-width:155px;padding:12px 14px;background:var(--panel);border:1px solid var(--line);border-radius:9px;text-decoration:none}.stage-status-link>strong{font:500 28.8px Georgia,serif;text-align:right}.stage-status-link>small{grid-column:1/-1;color:var(--muted);font-size:9.6px;text-align:right}.relationship-note{display:grid;grid-template-columns:250px minmax(0,1fr);gap:20px;align-items:center;margin-bottom:15px;padding:17px 20px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.relationship-note h3{font-size:15.6px;margin:5px 0 0}.relationship-note>p{color:var(--muted);font-size:12px;line-height:1.55;margin:0}.relationship-note code{font-size:10.8px}.group-plan{margin-bottom:24px}.group-related-links{display:flex;align-items:center;gap:8px;margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}.group-related-links>span{color:var(--muted);font-size:10.8px;margin-right:auto}.group-destinations>.section-head{margin-bottom:12px}.group-destination-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.group-destination-card{display:grid;grid-template-columns:minmax(0,1fr) 100px;gap:16px;min-height:150px;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.group-destination-card:hover{border-color:var(--accent-light)}.group-destination-card h3{font-size:15.6px;margin:5px 0 7px}.group-destination-card p:not(.kicker){color:var(--muted);font-size:10.8px;line-height:1.5;margin:0}.destination-rollup{align-self:center;text-align:right}.destination-rollup strong,.destination-rollup small{display:block}.destination-rollup strong{font:500 32.4px Georgia,serif}.destination-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:3px}
5882
6004
  .collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.review-save-status{margin-right:auto;color:var(--muted);font-size:11px}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.workflow-guidance-more>summary{cursor:pointer;font-weight:750}.workflow-findings-more{margin-top:9px}.workflow-guidance-more>p{margin:8px 0 0}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.policy-lifecycle-note{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px;margin-top:18px}.policy-lifecycle-note h3{margin:5px 0 8px}.policy-lifecycle-note p:not(.kicker),.policy-library-proposals p{color:var(--muted);font-size:11px;line-height:1.55}.policy-library-proposals{display:grid;gap:8px}.policy-library-proposals article{padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.policy-library-proposals p{margin:5px 0 8px}.policy-activation{display:grid;gap:12px;margin-top:22px}.policy-activation-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.policy-activation-card{padding:17px 18px;border:1px solid #d8bd78;border-radius:10px;background:var(--panel)}.policy-activation-card.active-and-operating{border-color:#b9dac6}.policy-activation-card h3{margin:7px 0 0}.policy-activation-gaps{display:grid;gap:9px;margin-top:13px}.policy-activation-gaps>div{padding-top:8px;border-top:1px solid var(--line)}.policy-activation-gaps small{display:block;margin-bottom:6px;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.policy-activation-warning{margin:11px 0 0;padding:9px 10px;border-radius:7px;background:#f7e9cf;color:#6d4917;font-size:10.4px;line-height:1.45}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-link{position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-card-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
5883
6005
  .workflow-findings>a:hover{border-color:var(--accent-light);box-shadow:0 3px 9px rgba(21,40,33,.05)}.workflow-findings>a:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{align-items:flex-start}.stage-page-completion-state{flex:0 0 auto;max-width:150px;padding:5px 8px;border-radius:99px;background:#f7e9cf;color:#855717;font-size:9.6px;font-weight:750;line-height:1.25;text-align:right}.stage-page-completion-state.complete{background:#ddefe5;color:#176143}.obligation-card.workflow-target{outline:2px solid var(--focus);outline-offset:3px}.obligation-card-foot{flex-wrap:wrap}.obligation-card-foot .obligation-links{flex:1 1 120px}
6006
+ .collection-review-panel.current details{margin-top:8px}.collection-review-detail-content{margin-top:10px}.collection-review-complete-summary{display:flex;align-items:baseline;gap:10px}.collection-review-complete-summary>.kicker{margin:0;white-space:nowrap}.collection-review-panel.current .collection-review-foot{justify-content:flex-end;margin-top:8px}
6007
+ .collection-review-current-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.collection-review-panel.current .collection-review-details{flex:0 0 auto;margin:0;padding:0;border:0;background:transparent}.collection-review-panel.current .collection-review-details>summary{color:var(--accent);white-space:nowrap}.collection-review-panel.current .collection-review-details[open]{flex-basis:100%;order:3;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel.current .collection-review-details[open]>summary{margin-bottom:10px}.collection-review-panel.current .collection-review-details[open] .collection-review-detail-content{margin-top:0}.collection-review-panel.current .button{margin-left:auto}
6008
+ @media(max-width:520px){.collection-review-complete-summary{flex:1 1 180px;min-width:0;flex-wrap:wrap}.collection-review-complete-summary .collection-review-result{min-width:0;flex-wrap:wrap}.collection-review-complete-summary .collection-review-result span{overflow-wrap:anywhere}.collection-review-panel.current .button{margin-left:0}}
5884
6009
  .evidence-attachments .panel-head{align-items:flex-start}.evidence-attachments .panel-head h3{margin:0}.evidence-attachments .panel-head p{margin:4px 0 0;color:var(--muted);font-size:10px}.evidence-attachments ul{list-style:none;margin:0;padding:0}.evidence-attachments li{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 0;border-top:1px solid var(--line)}.evidence-attachments li:first-child{border-top:0}.evidence-attachments strong,.evidence-attachments small{display:block}.evidence-attachments strong{font-size:11px}.evidence-attachments small{margin-top:2px;color:var(--muted);font-size:9px;overflow-wrap:anywhere}.evidence-attachments .attachment-empty{display:block;color:var(--muted);font-size:10px;line-height:1.45}.danger-text{color:var(--red)}
5885
6010
  .policy-lifecycle-note.single{grid-template-columns:1fr}
5886
6011
  .policy-content-table{margin-top:22px}.policy-content-table>.section-head{align-items:end}.policy-content-table>.section-head h2{font:500 27.6px Georgia,serif;margin:5px 0 6px}.policy-content-table>.section-head p:not(.kicker){max-width:720px;margin:0;color:var(--muted);font-size:12px;line-height:1.5}.policy-content-tools{display:flex;align-items:center;justify-content:flex-end;gap:9px;flex-wrap:wrap}.policy-content-tools input[type="search"]{min-width:210px;border:1px solid var(--line);border-radius:7px;background:var(--field);padding:9px 11px}.history-toggle{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:11px}.policy-content-add{position:relative}.policy-content-add>summary{list-style:none}.policy-content-add>summary::-webkit-details-marker{display:none}.policy-content-add>div{position:absolute;right:0;z-index:5;display:grid;min-width:150px;margin-top:6px;padding:5px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow)}.policy-content-add button{border:0;border-radius:5px;background:transparent;padding:8px 10px;color:var(--ink);text-align:left;cursor:pointer}.policy-content-add button:hover{background:var(--accent-soft)}.policy-content-type span,.policy-content-approval span{display:block}.policy-content-type small,.policy-content-approval small{display:block;margin-top:3px;color:var(--muted);font-size:9.6px}.policy-content-count{margin:9px 2px 0;color:var(--muted);font-size:10.8px;text-align:right}
@@ -6052,6 +6177,9 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
6052
6177
  .record-table td:has(.relation){min-width:150px}
6053
6178
  .record-table .relation{border-radius:7px;line-height:1.35;overflow-wrap:anywhere;word-break:normal}
6054
6179
  @media(max-width:760px){.record-table td:has(.relation){min-width:0}.record-table td[data-label]>.relation{grid-column:2}}
6180
+ @media(max-width:760px){.diagnostics>div{grid-template-columns:max-content minmax(0,1fr)}}
6181
+ .workflow-guidance .workflow-status{margin:4px 0 12px;color:var(--muted);font-size:10px}.workflow-completion-requirements{display:inline;appearance:none;-webkit-appearance:none;white-space:nowrap;padding:0;border:0;background:none;color:var(--accent);font:inherit;font-weight:750;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.workflow-completion-requirements:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.completion-requirements-dialog ul{margin:14px 0 0;padding-left:20px;color:var(--muted);font-size:12px;line-height:1.55}
6182
+ .record-completion-state{background:var(--surface-soft)}.record-completion-state h3{margin:4px 0 5px;font-size:16.8px}.record-completion-state p:last-child{margin:0;color:var(--muted);font-size:11px;line-height:1.45}
6055
6183
  `;
6056
6184
 
6057
6185
  function safeJson(value) {
package/src/workflow.js CHANGED
@@ -217,18 +217,34 @@ export function buildWorkflowDelta(before, after) {
217
217
  }
218
218
 
219
219
  export function workflowForResource(workflow, type, id) {
220
- if (!workflow) return { findings: [], workItems: [], recommended: null };
220
+ if (!workflow) return {
221
+ findings: [],
222
+ workItems: [],
223
+ related: { findings: [], workItems: [], recommended: null },
224
+ recommended: null
225
+ };
221
226
  const matches = (item) => item.subject?.type === type && item.subject?.id === id;
222
- const findings = workflow.findings.filter(matches);
223
- const workItems = workflow.workItems.filter((item) => (
224
- matches(item)
225
- || item.source?.type === type && item.source?.id === id
226
- ));
227
+ const related = (item) => (
228
+ item.source?.type === type
229
+ && item.source?.id === id
230
+ && !matches(item)
231
+ );
232
+ const directFindings = workflow.findings.filter(matches);
233
+ const directWorkItems = workflow.workItems.filter(matches);
234
+ const relatedFindings = workflow.findings.filter(related);
235
+ const relatedWorkItems = workflow.workItems.filter(related);
236
+ const findings = [...directFindings, ...relatedFindings];
237
+ const workItems = [...directWorkItems, ...relatedWorkItems];
227
238
  return {
228
239
  contractVersion: workflow.contractVersion,
229
240
  assessments: workflow.assessments,
230
241
  findings,
231
242
  workItems,
243
+ related: {
244
+ findings: relatedFindings,
245
+ workItems: relatedWorkItems,
246
+ recommended: [...relatedFindings, ...relatedWorkItems].sort(compareRecommended)[0] || null
247
+ },
232
248
  recommended: [...findings, ...workItems].sort(compareRecommended)[0] || null
233
249
  };
234
250
  }
@@ -430,7 +446,7 @@ function recordIncompleteReason(record, loaded, program) {
430
446
  return {
431
447
  state: "ready",
432
448
  requiredness: "required",
433
- message: "Review this criterion against the current service scope and record the applicability decision."
449
+ message: "Review this criterion against the current service scope, then record the applicability decision."
434
450
  };
435
451
  }
436
452
  if (record.type === "appointment" && record.status === "planned") {
@@ -440,16 +456,16 @@ function recordIncompleteReason(record, loaded, program) {
440
456
  return {
441
457
  state: "ready",
442
458
  requiredness: appointmentRequiredness(record, loaded.model),
443
- message: "Assign a holder, confirm the authority scope and independence needs, then activate this Appointment on its real start date."
459
+ message: "Assign a holder, confirm the authority and independence needed, then activate this Appointment on its actual start date."
444
460
  };
445
461
  }
446
462
  const messages = {
447
- draft: "Complete the record, its relationships, and required Markdown before moving it to review.",
448
- planned: "Review this planned record against the actual program and complete its finalization checks.",
449
- proposed: "Review the proposed work, owner, schedule, and completion profile before activating it.",
463
+ draft: "Complete the record, its relationships, and required Markdown before review.",
464
+ planned: "Check this planned record against the actual program, then complete its finalization checks.",
465
+ proposed: "Review the proposed work, owner, schedule, and completion requirements before activation.",
450
466
  "in-review": "Complete independent review and bind the approval to the exact content revision.",
451
467
  open: "Complete or formally dispose of this open work with the required proof.",
452
- "in-progress": "Finish the work, record its result, and link its completion proof.",
468
+ "in-progress": "Finish the work, record the result, and link the completion proof.",
453
469
  blocked: "Resolve the recorded blockers before completing this work.",
454
470
  "partially-implemented": "Finish the remaining control design, operation, source, and scheduling work."
455
471
  };
@@ -517,7 +533,7 @@ function finalizationFields(record, model, resources, program) {
517
533
  fields.push({
518
534
  field: "applicabilityReview",
519
535
  requiredness: "required",
520
- message: "Record or refresh the reviewed applicability decision, rationale, reviewer, and date. FileGRC records the current material scope automatically."
536
+ message: "Record or refresh the applicability decision, rationale, reviewer, and date. FileGRC records the current material scope automatically."
521
537
  });
522
538
  }
523
539
  if (record.type === "policy" && model.resources.policy?.fields?.programRole && !record.programRole) {
@@ -570,7 +586,7 @@ function finalizationFields(record, model, resources, program) {
570
586
  fields.push({
571
587
  field,
572
588
  requiredness: "conditional",
573
- message: `Complete ${fieldLabel(field)} before treating this ${fieldLabel(record.type).toLowerCase()} as finalized.`
589
+ message: `Add ${fieldLabel(field)} before treating this ${fieldLabel(record.type).toLowerCase()} as complete.`
574
590
  });
575
591
  }
576
592
  return fields;
@@ -676,9 +692,9 @@ async function sourceCoverageFindings(loaded, program) {
676
692
  ? "The source family has a reviewed authoritative path, valid coverage dates, retrieval ownership, and reconciliation method."
677
693
  : record
678
694
  ? record.status === "active"
679
- ? "Complete any missing source details and link a passed retrieval test after a candidate period is set."
680
- : "Finish the planned source-family decision, retrieval method, retention, validity dates, and pre-period dry run when a candidate period is set."
681
- : "Create a source-family coverage record and choose FileGRC, an external authoritative System, reviewed zero population, or reviewed not applicable.",
695
+ ? "Add missing source details and link a passed retrieval test after setting a candidate period."
696
+ : "Finish the source decision, retrieval method, retention, validity dates, and pre-period dry run after setting a candidate period."
697
+ : "Create a source-coverage record and choose FileGRC, an external authoritative System, zero population, or not applicable.",
682
698
  subject: { type: "source-coverage", ...(record ? { id: record.id } : {}) },
683
699
  dependencies: [],
684
700
  actions: record