filegrc 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/web.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  utcCalendarDate,
9
9
  validCalendarRecurrence
10
10
  } from "./recurrence.js";
11
+ import { POLICY_EVENT_NAMES, PROGRAM_PATH, RESOURCE_INSTRUCTIONS } from "./program-path.js";
11
12
  import { formatCalendarDate, formatLocalDateTime } from "./time.js";
12
13
 
13
14
  export function renderIndex(state = null) {
@@ -39,74 +40,24 @@ let state;
39
40
  const LIST_PAGE_SIZE = 25;
40
41
  const SEARCH_PAGE_SIZE = 25;
41
42
  const NAV_GROUP_STORAGE_KEY = "filegrc.sidebar.groups.v3";
43
+ const NAV_SCROLL_STORAGE_KEY = "filegrc.sidebar.scroll.v1";
44
+ let navigationScrollTop = readNavigationScrollTop();
42
45
  let latestPacketResult = null;
43
46
  let latestPacketState = null;
44
- const READINESS_STAGES = [
45
- {
46
- id: "scope",
47
- number: "1",
48
- title: "Scope",
49
- description: "Systems and boundary",
50
- sections: [
51
- { id: "boundary", title: "Boundary", types: ["system", "asset"], nested: true, defaultOpen: true },
52
- { id: "dependencies", title: "Dependencies", types: ["vendor", "vendor-review"], nested: true, defaultOpen: false }
53
- ]
54
- },
55
- {
56
- id: "criteria",
57
- number: "2",
58
- title: "Criteria",
59
- description: "What the auditor evaluates",
60
- sections: [
61
- { id: "framework", title: "Framework", types: ["framework", "requirement"], nested: true, defaultOpen: true },
62
- { id: "service-description", title: "Supplements", types: ["commitment", "complementary-control"], nested: true, defaultOpen: false }
63
- ]
64
- },
65
- {
66
- id: "policies",
67
- number: "3",
68
- title: "Policies",
69
- description: "Rules the company adopts",
70
- sections: [
71
- { id: "library", title: "Policy Library", types: ["policy", "document"], defaultOpen: true }
72
- ]
73
- },
74
- {
75
- id: "controls",
76
- number: "4",
77
- title: "Controls",
78
- description: "How the rules operate",
79
- sections: [
80
- { id: "catalog", title: "Control Catalog", types: ["control"], defaultOpen: true },
81
- { id: "testing", title: "Testing", types: ["control-test"], defaultOpen: false }
82
- ]
83
- },
84
- {
85
- id: "run",
86
- number: "5",
87
- title: "Operate Controls",
88
- description: "Recurring and event work",
89
- sections: [
90
- { id: "queue", title: "Work Queue", types: ["obligation", "obligation-event", "action-item"], utility: "obligation-board", nested: true, defaultOpen: true },
91
- { id: "governance-risk", title: "Governance and Risk", types: ["policy-review", "meeting", "risk-assessment", "risk", "exception"], nested: true, defaultOpen: false },
92
- { id: "access-training", title: "Access and Training", types: ["access-grant", "access-review", "service-account", "training", "attestation"], nested: true, defaultOpen: false },
93
- { id: "security", title: "Security Operations", types: ["vulnerability-scan", "vulnerability", "penetration-test", "incident"], nested: true, defaultOpen: false },
94
- { id: "resilience", title: "Resilience", types: ["backup-test", "exercise"], nested: true, defaultOpen: false },
95
- { id: "evidence-issues", title: "Evidence and Issues", types: ["evidence", "finding", "data-request"], nested: true, defaultOpen: false }
96
- ]
97
- },
98
- {
99
- id: "audit",
100
- number: "6",
101
- title: "Audit",
102
- description: "Firm, requests, and report",
103
- sections: [
104
- { id: "engagement", title: "Engagement", types: ["audit", "audit-request"], defaultOpen: true },
105
- { id: "packet", title: "Evidence Delivery", types: ["audit-population"], utility: "audit-packet", nested: true, defaultOpen: true }
106
- ]
107
- }
108
- ];
109
- const ORGANIZATION_RESOURCE_TYPES = ["person", "team"];
47
+ let policyEventFeedback = null;
48
+ const SHARED_PROGRAM_STAGES = ${JSON.stringify(PROGRAM_PATH)};
49
+ const READINESS_STAGES = SHARED_PROGRAM_STAGES.map((stage) => ({
50
+ ...stage,
51
+ number: String(stage.number)
52
+ }));
53
+ const STAGE_PAGE_SUMMARIES = ${JSON.stringify({
54
+ ...RESOURCE_INSTRUCTIONS,
55
+ "utility:audit-packet": "Review FileGRC Evidence and External Evidence for the formal period, complete engagement preparation, and build the indexed audit packet."
56
+ })};
57
+ const POLICY_EVENT_NAMES = ${JSON.stringify(POLICY_EVENT_NAMES)};
58
+ const STAGE_PAGE_ID_ALIASES = {
59
+ "controls:complementary-control": ["scope:complementary-control"]
60
+ };
110
61
  const OBLIGATION_COMPLETION_TYPES = {
111
62
  "access-review": "access-review",
112
63
  "backup-test": "backup-test",
@@ -126,6 +77,8 @@ const OBLIGATION_COMPLETION_TYPES = {
126
77
  "vulnerability-scan": "vulnerability-scan"
127
78
  };
128
79
  const RECORD_TEXT_FIELDS = new Set(["description", "statement", "activity", "purpose", "scope", "objective", "applicabilityRationale", "summary", "rationale", "acceptanceRationale", "businessPurpose", "changeSummary", "decisionSummary", "decisionRationale", "recommendation", "remediationPlan", "auditorNotes", "notPerformedReason"]);
80
+ const FINDING_SOURCE_TYPES = new Set(["control-test", "policy-review", "meeting", "risk", "risk-assessment", "vendor-review", "access-review", "incident", "exercise", "backup-test", "penetration-test", "audit"]);
81
+ const ACTION_ITEM_SOURCE_TYPES = new Set(["finding", "exception", "policy-review", "meeting", "risk", "risk-assessment", "vendor-review", "access-review", "vulnerability", "incident", "exercise", "backup-test", "data-request", "audit-request"]);
129
82
  const TITLE_CASE_MINOR_WORDS = new Set(["a", "an", "and", "as", "at", "but", "by", "for", "in", "nor", "of", "on", "or", "per", "the", "to", "via", "vs"]);
130
83
  const navigationGroupState = readNavigationGroupState();
131
84
  let onboardingDialog = null;
@@ -155,11 +108,16 @@ async function start() {
155
108
  function render() {
156
109
  resourceGuideCleanup?.();
157
110
  resourceGuideCleanup = null;
111
+ const previousNavigation = root.querySelector(".sidebar-nav");
112
+ if (previousNavigation) navigationScrollTop = previousNavigation.scrollTop;
158
113
  const route = parseRoute();
159
114
  const nav = buildNavigation(route);
160
115
  root.innerHTML = '<div class="shell">' + nav + '<div class="workspace"><header class="topbar">' + topbar(route) + '</header><main id="main"></main></div></div>';
116
+ const nextNavigation = root.querySelector(".sidebar-nav");
117
+ if (nextNavigation) nextNavigation.scrollTop = navigationScrollTop;
161
118
  const main = root.querySelector("main");
162
119
  if (route.name === "home") renderHome(main);
120
+ else if (route.name === "stage") renderStageOverview(main, route.stageId, route.params);
163
121
  else if (route.name === "obligations") renderObligations(main, route.params);
164
122
  else if (route.name === "audit-packet") renderAuditPacket(main, route.params);
165
123
  else if (route.name === "list") renderList(main, route.type, route.params);
@@ -179,6 +137,7 @@ function parseRoute() {
179
137
  return { name: "missing" };
180
138
  }
181
139
  if (!parts.length) return { name: "home" };
140
+ if (parts.length === 2 && parts[0] === "stage" && parts[1]) return { name: "stage", stageId: parts[1], params: new URLSearchParams(query) };
182
141
  if (parts.length === 1 && parts[0] === "obligations") return { name: "obligations", params: new URLSearchParams(query) };
183
142
  if (parts.length === 1 && parts[0] === "audit-packet") return { name: "audit-packet", params: new URLSearchParams(query) };
184
143
  if (parts.length === 2 && parts[0] === "resources" && parts[1]) return { name: "list", type: parts[1], params: new URLSearchParams(query) };
@@ -191,6 +150,8 @@ function parseRoute() {
191
150
  function buildNavigation(route) {
192
151
  const currentStage = readinessStageForRoute(route);
193
152
  const stages = READINESS_STAGES.map((stage) => {
153
+ const stagePageCurrent = (route.name === "stage" && route.stageId === stage.id)
154
+ || (stage.id === "run" && route.name === "obligations");
194
155
  const stageOpen = navigationGroupState[stage.id] ?? currentStage?.id === stage.id;
195
156
  const sections = stage.sections.map((section) => {
196
157
  const sectionKey = stage.id + ":" + section.id;
@@ -201,37 +162,43 @@ function buildNavigation(route) {
201
162
  const resources = section.types
202
163
  .map((type) => [type, state.model.resources[type]])
203
164
  .filter(([, definition]) => definition);
204
- const links = renderSidebarUtility(section.utility, route, !section.nested) + resources.map(([type, definition]) => {
205
- return '<a class="' + (!section.nested ? "nav-direct " : "") + (route.type === type ? "current" : "") + '" href="#/resources/' + encodeURIComponent(type) + '"><span>' + esc(titleCase(definition.pluralTitle)) + '</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
206
- }).join("");
207
- if (!section.nested) return links;
208
- return '<section class="nav-group nav-subgroup ' + (sectionOpen ? "open" : "") + '" data-group="' + esc(sectionKey) + '"><button class="nav-subheading" type="button" aria-expanded="' + sectionOpen + '" aria-controls="nav-group-' + esc(sectionKey) + '"><span>' + esc(section.title) + '</span><span class="chevron nav-control">›</span></button><div class="nav-items" id="nav-group-' + esc(sectionKey) + '">' + links + '</div></section>';
165
+ const direct = stage.sections.length === 1 || sectionDestinations(section).length === 1;
166
+ const links = resources.map(([type, definition]) => {
167
+ return '<a class="' + (direct ? "nav-direct " : "") + (route.type === type ? "current" : "") + '" href="#/resources/' + encodeURIComponent(type) + '"><span>' + esc(titleCase(definition.pluralTitle)) + '</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
168
+ }).join("") + renderSidebarUtility(section.utility, route, direct);
169
+ if (direct) return links;
170
+ return '<section class="nav-group nav-subgroup ' + (sectionOpen ? "open" : "") + '" data-group="' + esc(sectionKey) + '"><button class="nav-subheading-row nav-subgroup-toggle" type="button" aria-label="' + (sectionOpen ? "Collapse " : "Expand ") + esc(section.title) + '" aria-expanded="' + sectionOpen + '" aria-controls="nav-group-' + esc(sectionKey) + '"><span class="nav-subheading">' + esc(section.title) + '</span><svg class="nav-chevron" viewBox="0 0 12 12" aria-hidden="true"><path d="M4 2.5 7.5 6 4 9.5"></path></svg></button><div class="nav-items" id="nav-group-' + esc(sectionKey) + '">' + links + '</div></section>';
209
171
  }).join("");
210
- return '<section class="nav-group nav-stage ' + (stageOpen ? "open" : "") + '" data-group="' + esc(stage.id) + '"><button class="nav-heading" type="button" aria-expanded="' + stageOpen + '" aria-controls="nav-group-' + esc(stage.id) + '"><span class="nav-stage-number">' + esc(stage.number) + '</span><span class="nav-stage-copy"><strong>' + esc(stage.title) + '</strong><small>' + esc(stage.description) + '</small></span><span class="chevron nav-control">›</span></button><div class="nav-items" id="nav-group-' + esc(stage.id) + '">' + sections + '</div></section>';
172
+ return '<section class="nav-group nav-stage ' + (stageOpen ? "open" : "") + '" data-group="' + esc(stage.id) + '"><div class="nav-heading-row"><a class="nav-heading ' + (stagePageCurrent ? "current" : "") + '" href="#/stage/' + encodeURIComponent(stage.id) + '"><span class="nav-stage-number">' + esc(stage.number) + '</span><span class="nav-stage-copy"><strong>' + esc(stage.title) + '</strong><small>' + esc(stage.description) + '</small></span></a><button class="nav-toggle nav-stage-toggle" type="button" aria-label="' + (stageOpen ? "Collapse " : "Expand ") + esc(stage.title) + '" aria-expanded="' + stageOpen + '" aria-controls="nav-group-' + esc(stage.id) + '"><svg class="nav-chevron" viewBox="0 0 12 12" aria-hidden="true"><path d="M4 2.5 7.5 6 4 9.5"></path></svg></button></div><div class="nav-items" id="nav-group-' + esc(stage.id) + '">' + sections + '</div></section>';
211
173
  }).join("");
212
- const organizationCurrent = route.name === "organization" || route.name === "repository" || ["workspace", "renderer-settings", ...ORGANIZATION_RESOURCE_TYPES].includes(route.type);
174
+ const organizationCurrent = route.name === "organization" || route.name === "repository" || ["workspace", "renderer-settings"].includes(route.type);
213
175
  const organizationName = state.workspace.organizationName || "Organization";
214
176
  const initial = organizationName.trim().charAt(0).toUpperCase() || "O";
215
177
  return '<aside class="sidebar" id="sidebar-navigation"><button class="nav-close" type="button" aria-label="Close navigation">×</button><a href="#/" class="brand"' + (route.name === "home" ? ' aria-current="page"' : "") + '><img class="mark" src="./favicon.png" alt="" width="39" height="39"><span><strong>FileGRC</strong><small>SOC 2 workspace</small></span></a><nav class="sidebar-nav">' + stages + '</nav><div class="sidebar-footer"><a class="organization-nav ' + (organizationCurrent ? "current" : "") + '" href="#/organization"><span class="organization-mark">' + esc(initial) + '</span><span><strong>' + esc(organizationName) + '</strong><small>Organization</small></span><span class="organization-arrow">›</span></a></div></aside><button class="nav-scrim" type="button" aria-label="Close navigation"></button>';
216
178
  }
217
179
 
218
180
  function readinessStageForRoute(route) {
219
- return READINESS_STAGES.find((stage) => stage.sections.some((section) => section.types.includes(route.type)
220
- || (section.utility === "obligation-board" && route.name === "obligations")
221
- || (section.utility === "audit-packet" && route.name === "audit-packet")));
181
+ if (route.name === "stage") return READINESS_STAGES.find((stage) => stage.id === route.stageId);
182
+ return READINESS_STAGES.find((stage) => (
183
+ (stage.supportingResourceTypes || []).includes(route.type)
184
+ || stage.sections.some((section) => section.types.includes(route.type)
185
+ || (section.utility === "obligation-board" && route.name === "obligations")
186
+ || (section.utility === "audit-packet" && route.name === "audit-packet"))
187
+ ));
222
188
  }
223
189
 
224
190
  function readinessStageForType(type) {
225
- return READINESS_STAGES.find((stage) => stage.sections.some((section) => section.types.includes(type)));
191
+ return READINESS_STAGES.find((stage) => (
192
+ (stage.supportingResourceTypes || []).includes(type)
193
+ || stage.sections.some((section) => section.types.includes(type))
194
+ ));
226
195
  }
227
196
 
228
197
  function renderSidebarUtility(utility, route, direct = false) {
229
198
  const directClass = direct ? "nav-direct " : "";
230
- if (utility === "obligation-board") {
231
- return '<a class="' + directClass + (route.name === "obligations" ? "current" : "") + '" href="#/obligations"><span>Obligation Board</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
232
- }
199
+ if (utility === "obligation-board") return "";
233
200
  if (utility === "audit-packet") {
234
- return '<a class="' + directClass + 'audit-packet-link ' + (route.name === "audit-packet" ? "current" : "") + '" href="#/audit-packet"><span>Audit Readiness</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
201
+ return '<a class="' + directClass + 'audit-packet-link ' + (route.name === "audit-packet" ? "current" : "") + '" href="#/audit-packet"><span>Audit Evidence &amp; Packet</span><span class="nav-control-slot" aria-hidden="true"></span></a>';
235
202
  }
236
203
  return "";
237
204
  }
@@ -239,12 +206,14 @@ function renderSidebarUtility(utility, route, direct = false) {
239
206
  function topbar(route) {
240
207
  const title = route.name === "home"
241
208
  ? "Program Overview"
209
+ : route.name === "stage"
210
+ ? READINESS_STAGES.find((stage) => stage.id === route.stageId)?.title || "Program"
242
211
  : route.name === "organization"
243
212
  ? "Organization"
244
213
  : route.name === "repository"
245
214
  ? "Repository"
246
215
  : route.name === "obligations"
247
- ? "Obligation Board"
216
+ ? "Work Queue"
248
217
  : route.name === "audit-packet"
249
218
  ? "Audit Readiness"
250
219
  : state.model.resources[route.type]?.pluralTitle || "FileGRC";
@@ -253,64 +222,265 @@ function topbar(route) {
253
222
 
254
223
  function renderHome(main) {
255
224
  const activeAudit = resourcesOfType("audit").find((item) => !["complete", "closed", "cancelled"].includes(item.record.status));
256
- main.innerHTML = '<div class="page home-page"><section class="hero overview-hero"><div><p class="kicker">Current program state</p><h2>' + esc(titleCase(state.workspace.title)) + '</h2><p>' + esc(state.workspace.description || "Governance, risk, controls, evidence, and audit work maintained as plain files in Git.") + '</p></div></section>' + readinessOverview() +
257
- '<div class="overview-grid"><section class="panel obligation-panel"><div class="panel-head"><div><p class="kicker">Policy obligations</p><h3>Due Windows</h3></div><a href="#/obligations">Open board</a></div>' + obligationPreview(state.obligations.items.filter((item) => item.status !== "complete").slice(0, 3)) + '</section>' +
258
- '<section class="panel event-reminder-panel"><div class="panel-head"><div><p class="kicker">Event reminders</p><h3>Did Something Change?</h3></div><a href="#/obligations?section=events">Start workflow</a></div>' + eventReminderPreview(state.obligations.triggers.slice(0, 4)) + '</section>' +
259
- '<section class="panel audit-panel"><div class="panel-head"><div><p class="kicker">Audit activity</p><h3>' + esc(titleCase(activeAudit?.record.title || "Plan the Engagement")) + '</h3></div>' + (activeAudit ? '<a href="#/resource/audit/' + encodeURIComponent(activeAudit.record.id) + '">Open audit</a>' : '<a href="#/resources/audit">Plan audit</a>') + '</div>' +
260
- (activeAudit ? auditProgress(activeAudit.record) + auditEngagementPrompt(activeAudit.record) : auditEngagementPrompt()) + '</section></div></div>';
225
+ const program = state.programReadiness;
226
+ const activeFirm = activeAudit && (activeAudit.record.auditorVendorId || (activeAudit.record.auditor && Object.keys(activeAudit.record.auditor).length));
227
+ const setupPending = rendererSettingsEntry()?.record.showOnboarding === true;
228
+ const acceptedEventTriggers = state.obligations.triggers.filter(({ programStatus }) => programStatus !== "proposed");
229
+ const openObligations = state.obligations.items.filter((item) => item.status !== "complete");
230
+ const obligationHeading = openObligations.some((item) => item.status !== "proposed") ? "Due Windows" : "Starter Proposals";
231
+ const setupBanner = setupPending
232
+ ? '<section class="setup-banner"><div><p class="kicker">Setup incomplete</p><h3>Define the initial service boundary</h3><p>Record the management program goal and the systems that should enter policy and control review.</p></div><ol><li>Describe the service boundary.</li><li>Choose the program goal.</li><li><button class="text-button" type="button" id="resume-setup">Resume setup</button></li></ol></section>'
233
+ : "";
234
+ const auditPanel = program.evidenceReady
235
+ ? '<section class="panel audit-panel"><div class="panel-head"><div><p class="kicker">Optional next phase</p><h3>' + esc(activeFirm ? titleCase(activeAudit.record.title) : "Target: " + program.target.label) + '</h3></div>' + (activeAudit ? '<a href="#/resource/audit/' + encodeURIComponent(activeAudit.record.id) + '">Open audit</a>' : '<a href="#/resources/audit">Engagements</a>') + '</div>' +
236
+ (activeAudit ? auditProgress(activeAudit.record) + auditEngagementPrompt(activeAudit.record) : auditEngagementPrompt()) + '</section>'
237
+ : "";
238
+ main.innerHTML = '<div class="page home-page"><section class="hero overview-hero"><div><p class="kicker">Current program state</p><h2>' + esc(titleCase(state.workspace.title)) + '</h2><p>' + esc(state.workspace.description || "Governance, risk, controls, evidence, and audit work maintained as plain files in Git.") + '</p></div></section>' + setupBanner + readinessOverview() +
239
+ '<div class="overview-grid"><section class="panel obligation-panel"><div class="panel-head"><div><p class="kicker">Policy obligations</p><h3>' + obligationHeading + '</h3></div><a href="#/stage/run">Open board</a></div>' + obligationPreview(openObligations.slice(0, 3)) + '</section>' +
240
+ '<section class="panel event-reminder-panel"><div class="panel-head"><div><p class="kicker">Event reminders</p><h3>Did Something Change?</h3></div><a href="#/stage/run?section=events">' + (acceptedEventTriggers.length ? "Trigger work" : "Review proposals") + '</a></div>' + eventReminderPreview(state.obligations.triggers.slice(0, 4)) + '</section>' +
241
+ auditPanel + '</div></div>';
242
+ main.querySelector("#resume-setup")?.addEventListener("click", requestOnboarding);
261
243
  }
262
244
 
263
245
  function readinessOverview() {
264
- const inScopeSystems = resourcesOfType("system").filter(({ record }) => record.inScope && record.status !== "retired");
265
- const requirements = resourcesOfType("requirement").filter(({ record }) => record.applicability === "applicable");
266
- const policies = resourcesOfType("policy").filter(({ record }) => !["superseded", "retired"].includes(record.status));
267
- const approvedPolicies = policies.filter(({ record }) => ["approved", "active"].includes(record.status));
268
- const controls = resourcesOfType("control");
269
- const confirmedControls = controls.filter(({ record }) => record.status !== "planned");
270
- const evidence = resourcesOfType("evidence");
271
- const activeAudit = resourcesOfType("audit").find(({ record }) => !["complete", "closed", "cancelled"].includes(record.status));
246
+ const progress = programPathProgress();
247
+ const nextHref = nextProgramStageHref();
248
+ const programStage = (id, body, href) => {
249
+ const stage = READINESS_STAGES.find((candidate) => candidate.id === id);
250
+ const current = stageProgress(stage);
251
+ const remaining = current.total - current.complete;
252
+ const status = !remaining ? "Complete" : current.complete ? remaining + " remaining" : current.total + " to confirm";
253
+ return [stage.title, body, href, status, !remaining ? "good" : current.complete ? "warn" : "neutral"];
254
+ };
272
255
  const stages = [
273
- ["Scope", "Define the service, system boundary, people, data, and vendors.", inScopeSystems.length ? "#/resources/system" : "#/resources/system?new=1", inScopeSystems.length + " in-scope " + pluralize("system", inScopeSystems.length), inScopeSystems.length ? "good" : "warn"],
274
- ["Criteria", "Record what the auditor will evaluate and whether it applies.", "#/resources/requirement", requirements.length + " applicable", requirements.length ? "neutral" : "warn"],
275
- ["Policies", "Set the rules and responsibilities the company adopts.", "#/resources/policy", approvedPolicies.length + " of " + policies.length + " approved", approvedPolicies.length === policies.length && policies.length ? "good" : "warn"],
276
- ["Controls", "Confirm the repeatable work that satisfies those rules and criteria.", "#/resources/control", confirmedControls.length + " of " + controls.length + " reviewed", confirmedControls.length === controls.length && controls.length ? "good" : "warn"],
277
- ["Operate Controls", "Complete recurring and event work, then attach dated evidence.", "#/obligations", state.obligations.counts.overdue ? state.obligations.counts.overdue + " overdue" : state.obligations.counts.due + " due · " + evidence.length + " evidence", state.obligations.counts.overdue ? "bad" : state.obligations.counts.due ? "warn" : "good"],
278
- ["Audit", "Track the firm, date or period, requests, evidence packet, findings, and report.", activeAudit ? "#/resources/audit" : "#/resources/audit?new=1", activeAudit ? "Engagement active" : "Not planned", activeAudit ? "good" : "neutral"]
256
+ programStage("scope", "Confirm program ownership, criteria, and commitments, then describe the service, supporting systems, and dependencies.", "#/stage/scope"),
257
+ programStage("policies", "Tailor the policy set, obtain independent management approval, and establish effective dates.", "#/stage/policies"),
258
+ programStage("controls", "Finish the internal control set with actual procedures, owners, scope, and evidence sources, then record any complementary controls.", "#/stage/controls"),
259
+ programStage("evidence", "For each control family, finish the source-system instructions and verify a real test export or capture.", "#/stage/evidence"),
260
+ programStage("run", "Begin the candidate period, maintain risk assessments, work the FileGRC queue, run the remaining controls, and retain dated evidence.", "#/stage/run"),
261
+ programStage("audit", "Engage the CPA firm, confirm the formal period, complete fieldwork, and generate the final evidence packet.", "#/stage/audit")
279
262
  ];
280
- return '<section class="readiness-map"><div class="readiness-map-head"><div><p class="kicker">SOC 2 program path</p><h3>Follow the Audit Chain</h3></div><p>An auditor traces the system in scope to criteria, company rules, operating controls, and proof that the controls were implemented at the Type 1 date or operated during the Type 2 period.</p></div><div class="readiness-flow">' + stages.map(([title, body, href, status, tone], index) => '<a href="' + href + '"><span>' + (index + 1) + '</span><strong>' + esc(title) + '</strong><small>' + esc(body) + '</small><b class="readiness-state ' + esc(tone) + '">' + esc(status) + '</b></a>').join("") + '</div></section>';
263
+ return '<section class="readiness-map"><div class="readiness-map-head"><div><p class="kicker">SOC 2 program path</p><h3>Prepare, Operate, Then Audit</h3></div><div class="readiness-progress-summary"><div><span>Program Progress</span><strong>' + progress.percent + '%</strong><div class="progress"><span style="width:' + progress.percent + '%"></span></div><small>' + esc(progress.complete + " of " + progress.total + " program milestones complete") + '</small></div><a class="button primary" href="' + nextHref + '">Continue</a></div></div><div class="readiness-flow">' + stages.map(([title, body, href, status, tone], index) => '<a href="' + href + '"><span>' + (index + 1) + '</span><strong>' + esc(title) + '</strong><small>' + esc(body) + '</small><b class="readiness-state ' + esc(tone) + '">' + esc(status) + '</b></a>').join("") + '</div></section>';
264
+ }
265
+
266
+ function nextProgramStageHref() {
267
+ const nextStage = READINESS_STAGES.find((stage) => {
268
+ const progress = stageProgress(stage);
269
+ return progress.complete < progress.total;
270
+ }) || READINESS_STAGES.at(-1);
271
+ if (!nextStage) return "#/stage/scope";
272
+ return "#/stage/" + encodeURIComponent(nextStage.id);
273
+ }
274
+
275
+ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
276
+ const stage = READINESS_STAGES.find((candidate) => candidate.id === stageId);
277
+ if (!stage) return renderNotFound(main);
278
+ if (stage.id === "run") return renderObligations(main, params);
279
+ const progress = stageProgress(stage);
280
+ main.innerHTML = '<div class="page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
281
+ '<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 6</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(progress) + '</section>' +
282
+ renderStagePageIndex(stage) + '</div>';
283
+ }
284
+
285
+ function renderStagePageIndex(stage) {
286
+ const destinations = stagePageDestinations(stage);
287
+ const cards = destinations.map((destination, index) => stagePageCard(stage, destination, index)).join("");
288
+ return '<section class="stage-pages" aria-label="Step ' + esc(stage.number) + ' pages"><div class="stage-page-grid">' + cards + '</div></section>';
289
+ }
290
+
291
+ function stagePageDestinations(stage) {
292
+ return stage.sections.flatMap((section) => sectionDestinations(section)
293
+ .filter((destination) => destination.utility !== "obligation-board")
294
+ .map((destination) => ({ ...destination, section })));
295
+ }
296
+
297
+ function stagePageId(stage, destination) {
298
+ return stage.id + ":" + (destination.type || "utility:" + destination.utility);
299
+ }
300
+
301
+ function stagePageComplete(pageId) {
302
+ const completedPageIds = rendererSettingsEntry()?.record.completedStagePageIds || [];
303
+ return [pageId, ...(STAGE_PAGE_ID_ALIASES[pageId] || [])].some((id) => completedPageIds.includes(id));
304
+ }
305
+
306
+ function stagePageSummary(destination) {
307
+ const summaryKey = destination.type || "utility:" + destination.utility;
308
+ const section = destination.section || (destination.type
309
+ ? readinessStageForType(destination.type)?.sections.find((candidate) => candidate.types.includes(destination.type))
310
+ : null);
311
+ return STAGE_PAGE_SUMMARIES[summaryKey] || section?.description || "";
312
+ }
313
+
314
+ function stagePageCard(stage, destination, index) {
315
+ const details = destination.type ? resourceRollup(destination.type) : utilityRollup(destination.utility);
316
+ const summary = stagePageSummary(destination);
317
+ const stepLabel = "Step " + stage.number + "." + String.fromCharCode(97 + index);
318
+ const pageId = stagePageId(stage, destination);
319
+ const complete = stagePageComplete(pageId);
320
+ const completionControl = state.readOnly
321
+ ? '<span class="stage-page-completion-state ' + (complete ? "complete" : "") + '">' + (complete ? "Complete" : "Not marked complete") + '</span>'
322
+ : '<button class="button stage-page-completion ' + (complete ? "complete" : "") + '" type="button" data-stage-page-completion="' + esc(pageId) + '" data-complete="' + complete + '">' + (complete ? "Mark incomplete" : "Mark complete") + '</button>';
323
+ return '<article class="stage-page-card ' + (complete ? "complete" : "") + '"><a class="stage-page-card-link" href="' + destination.href + '" aria-label="Open ' + esc(destination.label) + '"></a><div class="stage-page-card-head"><div><small>' + esc(stepLabel) + '</small><h3>' + esc(destination.label) + '</h3></div><span class="stage-page-rollup"><strong>' + esc(details.value) + '</strong><small>' + esc(details.label) + '</small></span></div><p>' + esc(summary) + '</p><div class="stage-page-card-foot"><span class="stage-page-open" aria-hidden="true">Open ›</span>' + completionControl + '</div></article>';
324
+ }
325
+
326
+ function stageProgress(stage) {
327
+ if (stage.id === "run") return operationProgress();
328
+ const pages = stagePageDestinations(stage);
329
+ const complete = pages.filter((destination) => stagePageComplete(stagePageId(stage, destination))).length;
330
+ return progressFromCounts(complete, pages.length, "page");
331
+ }
332
+
333
+ function operationProgress() {
334
+ const program = state.programReadiness;
335
+ const goal = program?.target?.goal || state.workspace.assuranceGoal || "none";
336
+ const asOf = program?.asOf || currentDate();
337
+ const candidateStarted = goal === "soc-2-type-2"
338
+ ? Boolean(program?.target?.candidatePeriodStart && program.target.candidatePeriodStart <= asOf)
339
+ : goal === "soc-2-type-1"
340
+ ? Boolean(program?.target?.candidateTypeOneAsOf)
341
+ : Boolean(program?.evidenceReady);
342
+ const overdue = state.obligations.counts.overdue || 0;
343
+ const complete = Boolean(program?.evidenceReady && candidateStarted && overdue === 0);
344
+ if (complete) {
345
+ return {
346
+ percent: 100,
347
+ complete: 1,
348
+ total: 1,
349
+ status: "Operating",
350
+ tone: "good",
351
+ detail: "Evidence collection is running and the Work Queue has no overdue work."
352
+ };
353
+ }
354
+ if (overdue) {
355
+ return {
356
+ percent: 0,
357
+ complete: 0,
358
+ total: 1,
359
+ status: "Needs attention",
360
+ tone: "bad",
361
+ detail: overdue + " overdue Work Queue " + pluralize("item", overdue) + " must be resolved."
362
+ };
363
+ }
364
+ if (program?.evidenceReady && goal === "soc-2-type-2" && !candidateStarted) {
365
+ return {
366
+ percent: 0,
367
+ complete: 0,
368
+ total: 1,
369
+ status: "Ready to start",
370
+ tone: "warn",
371
+ detail: "Record the management candidate period start when evidence collection begins."
372
+ };
373
+ }
374
+ return {
375
+ percent: 0,
376
+ complete: 0,
377
+ total: 1,
378
+ status: "Not started",
379
+ tone: "warn",
380
+ detail: "Finish the Evidence Ready work before starting program operation."
381
+ };
382
+ }
383
+
384
+ function programPathProgress() {
385
+ const progress = READINESS_STAGES.map((stage) => stageProgress(stage));
386
+ return progressFromCounts(
387
+ progress.reduce((sum, current) => sum + current.complete, 0),
388
+ progress.reduce((sum, current) => sum + current.total, 0),
389
+ "program milestone"
390
+ );
391
+ }
392
+
393
+ function progressFromCounts(complete, total, noun) {
394
+ if (!total) return { percent: 0, complete: 0, total: 0, status: "Not started", tone: "neutral", detail: "No " + pluralize(noun, 2) + " are configured yet." };
395
+ const percent = Math.round((complete / total) * 100);
396
+ const detail = complete + " of " + total + " " + pluralize(noun, total) + " marked complete.";
397
+ if (complete === total) return { percent: 100, complete, total, status: "Complete", tone: "good", detail };
398
+ if (!complete) return { percent: 0, complete, total, status: "Not started", tone: "warn", detail };
399
+ return { percent, complete, total, status: "In progress", tone: "warn", detail };
400
+ }
401
+
402
+ function stageProgressCard(progress) {
403
+ return '<div class="stage-progress-card"><div><span class="badge ' + esc(progress.tone) + '">' + esc(progress.status) + '</span><strong>' + progress.percent + '%</strong></div><div class="progress"><span style="width:' + progress.percent + '%"></span></div><p>' + esc(progress.detail) + '</p></div>';
404
+ }
405
+
406
+ function sectionDestinations(section) {
407
+ const destinations = [];
408
+ for (const type of section.types) {
409
+ const definition = state.model.resources[type];
410
+ if (!definition) continue;
411
+ destinations.push({ type, kind: "Record page", label: titleCase(definition.pluralTitle), href: "#/resources/" + encodeURIComponent(type), description: definition.description });
412
+ }
413
+ if (section.utility === "obligation-board") destinations.push({ utility: section.utility, kind: "Working page", label: "Work Queue", href: "#/stage/run", description: "Complete recurring work, Policy Event tasks, and assigned follow-up with its due windows and linked proof." });
414
+ if (section.utility === "audit-packet") destinations.push({ utility: section.utility, kind: "Working page", label: "Audit Evidence & Packet", href: "#/audit-packet", description: "Review FileGRC and External Evidence, prepare fieldwork, and build the indexed packet." });
415
+ return destinations;
416
+ }
417
+
418
+ function resourceRollup(type) {
419
+ const records = resourcesOfType(type).map(({ record }) => record);
420
+ if (!records.length) return { value: "0", label: "No records yet" };
421
+ const statuses = new Map();
422
+ records.forEach((record) => {
423
+ if (record.status) statuses.set(record.status, (statuses.get(record.status) || 0) + 1);
424
+ });
425
+ const statusText = [...statuses.entries()].sort((a, b) => b[1] - a[1]).slice(0, 2).map(([status, count]) => count + " " + humanize(status).toLowerCase()).join(" · ");
426
+ return { value: String(records.length), label: statusText || pluralize("record", records.length) };
427
+ }
428
+
429
+ function utilityRollup(utility) {
430
+ if (utility === "obligation-board") {
431
+ const open = state.obligations.items.filter((item) => item.status !== "complete");
432
+ return { value: String(open.length), label: open.length ? "Open work items" : "No work due" };
433
+ }
434
+ if (utility === "audit-packet") {
435
+ const audits = resourcesOfType("audit");
436
+ return { value: String(audits.length), label: audits.length ? pluralize("engagement", audits.length) : "No engagement yet" };
437
+ }
438
+ return { value: "0", label: "Not started" };
281
439
  }
282
440
 
283
441
  function auditEngagementPrompt(audit = null) {
284
442
  const hasAuditor = audit?.auditorVendorId || (audit?.auditor && Object.keys(audit.auditor).length);
285
443
  if (hasAuditor) return "";
286
- const heading = audit ? "Auditor Not Recorded" : "Engage an Auditor Before the Target Date";
287
- return '<div class="audit-engagement"><div><strong>' + heading + '</strong><p>Shortlist independent CPA firms that perform SOC 2 examinations. Share the system boundary, Security scope, Type 1 or Type 2 goal, and target timing.</p></div><ul><li>Compare relevant service experience, schedule, fee, and evidence-request process.</li><li>Agree on scope and dates before the evidence period or as early as practical.</li><li>Store the selected firm and contact in the audit record.</li></ul>' + (!audit ? '<a class="button primary" href="#/resources/audit?new=1">Create audit record</a>' : "") + '</div>';
444
+ const heading = audit ? "CPA Firm Not Recorded" : "Optional: Engage a CPA Firm Early";
445
+ return '<div class="audit-engagement"><div><strong>' + heading + '</strong><p>The program can keep operating while management selects a firm. Engage early when a customer deadline, unusual scope, or other timing risk needs CPA input.</p></div><ul><li>Share the program boundary, goal, and evidence-source plan.</li><li>Keep management candidate dates separate from the firm-agreed report period.</li><li>Create or update the audit record only for a real engagement.</li></ul>' + (!audit ? '<a class="button" href="#/resources/audit?new=1">Create engagement</a>' : "") + '</div>';
288
446
  }
289
447
 
290
448
  function renderObligations(main, params = new URLSearchParams()) {
449
+ const stage = READINESS_STAGES.find((candidate) => candidate.id === "run");
291
450
  const plan = state.obligations;
451
+ const controls = resourcesOfType("control");
452
+ const linkedControlIds = new Set(resourcesOfType("obligation").flatMap(({ record }) => record.controlIds || []));
453
+ const scheduledControls = controls.filter(({ record }) => linkedControlIds.has(record.id)).length;
454
+ const assignedFollowUp = plan.standaloneItems.length;
292
455
  const visibleCardLimit = 6;
293
- const sections = ["upcoming", "due", "overdue"].map((status) => {
456
+ const sections = ["proposed", "upcoming", "due", "overdue"].map((status) => {
294
457
  const items = plan.items.filter((item) => item.status === status);
295
458
  const cards = items.map((item, index) => obligationCard(item, index >= visibleCardLimit)).join("");
296
459
  const more = items.length > visibleCardLimit
297
460
  ? '<button class="button obligation-more" type="button" data-expand-obligations="' + status + '" data-total="' + items.length + '" aria-expanded="false">Show ' + (items.length - visibleCardLimit) + ' more</button>'
298
461
  : "";
299
- return '<section class="obligation-column" data-obligation-column="' + status + '"><div class="obligation-column-head"><span class="badge status-' + status + '">' + esc(status) + '</span><strong>' + items.length + '</strong></div><div class="obligation-cards">' + (items.length ? cards : empty("Nothing " + status + ".")) + '</div>' + more + '</section>';
462
+ return '<section class="obligation-column" data-obligation-column="' + status + '"><div class="obligation-column-head"><span class="badge status-' + status + '">' + esc(properCase(status)) + '</span><strong>' + items.length + '</strong></div><div class="obligation-cards">' + (items.length ? cards : empty("Nothing " + status + ".")) + '</div>' + more + '</section>';
300
463
  }).join("");
301
- const triggers = plan.triggers.map((trigger) => '<article class="event-trigger-card"><div><p class="kicker">' + esc(trigger.eventType) + '</p><h3>' + esc(titleCase(trigger.prompt)) + '</h3><p>' + trigger.steps.length + ' policy actions will be created with their own owners and due windows.</p></div><ol>' + trigger.steps.map((step) => '<li><span>' + esc(step.title) + '</span><small>' + esc(eventStepSummary(step)) + '</small></li>').join("") + '</ol>' + (!state.readOnly ? '<button class="button primary" type="button" data-start-event="' + esc(trigger.eventType) + '">Start workflow</button>' : "") + '</article>').join("");
302
- const runs = plan.eventRuns
303
- .filter((run) => run.status !== "canceled")
304
- .sort((a, b) => String(b.occurredAt || b.occurredOn).localeCompare(String(a.occurredAt || a.occurredOn)));
305
- main.innerHTML = '<div class="page obligation-board-page"><div class="page-intro"><div><p class="kicker">Policy work queue</p><h2>Obligation Board</h2><p>Recurring work shows when a task may be completed and the date it becomes overdue. Policy events create a tracked checklist when something changes.</p></div><div class="page-actions"><button class="button" type="button" data-scroll-events>Start policy event</button><a class="button" href="#/resources/obligation">Edit templates</a></div></div>' +
464
+ const triggers = plan.triggers.map((trigger, index) => policyEventTrigger(trigger, index)).join("");
465
+ const feedback = policyEventFeedback
466
+ ? '<section class="policy-event-feedback" role="status" aria-live="polite"><span class="status-dot good"></span><div><strong>Work added to the Work Queue</strong><p>' + esc(policyEventFeedback.name + " created " + policyEventFeedback.taskCount + " " + pluralize("task", policyEventFeedback.taskCount) + ".") + '</p></div><button class="button" type="button" data-view-added-work>View Work Queue</button><button class="icon-button" type="button" data-dismiss-policy-event-feedback aria-label="Dismiss confirmation">×</button></section>'
467
+ : "";
468
+ main.innerHTML = '<div class="page obligation-board-page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
469
+ '<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 6</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(stageProgress(stage)) + '</section>' +
470
+ feedback +
471
+ '<section class="workflow-section event-reminders"><div class="section-head"><div><p class="kicker">Changes that create work</p><h2>Policy Events</h2><p>Trigger the matching workflow when an event occurs. FileGRC adds every required action to the Work Queue with its owner and deadline.</p></div></div><div class="policy-event-list">' + (triggers || empty("No event-driven obligations are configured.")) + '</div></section>' +
472
+ '<section class="workflow-section work-queue-section"><div class="section-head"><div><p class="kicker">Recurring, event, and assigned work</p><h2>Work Queue</h2><p>This board schedules work linked to ' + scheduledControls + ' of ' + controls.length + ' controls and includes ' + assignedFollowUp + ' open ' + pluralize("Action Item", assignedFollowUp) + '. Triggered Policy Event actions appear here as individual tasks in Upcoming, Due, or Overdue. Other controls operate continuously or per transaction in their source systems and are documented through evidence records. Starter work remains a proposal until its governing policies are effective and at least one linked control is implemented.</p></div><div class="page-actions">' + (!state.readOnly ? '<button class="button" type="button" data-new-action-item>New task</button>' : "") + '<a class="button" href="#/resources/obligation">Edit schedules</a></div></div>' +
306
473
  '<div class="obligation-board">' + sections + '</div>' +
307
- '<section class="workflow-section event-reminders"><div class="section-head"><div><p class="kicker">Ongoing reminders</p><h2>Start a Policy Event</h2><p>Use these when the underlying event happens. The generated checklist remains a normal set of Git-tracked records.</p></div></div><div class="event-trigger-grid">' + (triggers || empty("No event-driven obligations are configured.")) + '</div></section>' +
308
- '<section class="workflow-section"><div class="section-head"><div><p class="kicker">Event execution</p><h2>Active and Recent Workflows</h2><p>Link the requested completion records and evidence on each action item before marking it done.</p></div></div><div class="event-run-list">' + (runs.length ? runs.map(eventRunCard).join("") : empty("No policy events have been started.")) + '</div></section></div>';
474
+ '</section></div>';
309
475
  main.querySelectorAll("[data-start-event]").forEach((button) => button.addEventListener("click", () => {
310
476
  const trigger = plan.triggers.find((item) => item.eventType === button.dataset.startEvent);
311
477
  if (trigger) openObligationEventDialog(trigger);
312
478
  }));
313
- main.querySelector("[data-scroll-events]")?.addEventListener("click", () => main.querySelector(".event-reminders")?.scrollIntoView({ behavior: "smooth", block: "start" }));
479
+ main.querySelector("[data-view-added-work]")?.addEventListener("click", () => main.querySelector(".work-queue-section")?.scrollIntoView({ behavior: "smooth", block: "start" }));
480
+ main.querySelector("[data-dismiss-policy-event-feedback]")?.addEventListener("click", (event) => {
481
+ policyEventFeedback = null;
482
+ event.currentTarget.closest(".policy-event-feedback")?.remove();
483
+ });
314
484
  main.querySelectorAll("[data-expand-obligations]").forEach((button) => button.addEventListener("click", () => {
315
485
  const column = button.closest("[data-obligation-column]");
316
486
  const expanded = button.getAttribute("aria-expanded") === "true";
@@ -323,6 +493,9 @@ function renderObligations(main, params = new URLSearchParams()) {
323
493
  const item = plan.items.find((candidate) => candidate.key === button.dataset.recordObligation);
324
494
  if (item) openObligationCompletion(item);
325
495
  }));
496
+ main.querySelector("[data-new-action-item]")?.addEventListener("click", () => openEditor("action-item", null, {
497
+ description: "Create a task only when follow-up from another record needs its own assignee, deadline, and completion proof. Point it to that source record; it will remain in Work Queue until done or canceled."
498
+ }));
326
499
  const requestedEvent = params.get("event");
327
500
  if (requestedEvent || params.get("section") === "events") {
328
501
  queueMicrotask(() => {
@@ -332,16 +505,33 @@ function renderObligations(main, params = new URLSearchParams()) {
332
505
  }
333
506
  }
334
507
 
508
+ function policyEventTrigger(trigger, index) {
509
+ const tooltipId = "policy-event-tooltip-" + index;
510
+ const proposed = trigger.programStatus === "proposed";
511
+ const unavailable = state.readOnly || proposed;
512
+ const availability = proposed
513
+ ? "Available after its governing policies are effective and at least one linked control is implemented."
514
+ : state.readOnly
515
+ ? "Open this workspace in writable mode to trigger the workflow."
516
+ : trigger.steps.length + " " + pluralize("task", trigger.steps.length) + " will be added to the Work Queue.";
517
+ return '<article class="policy-event-row"><div class="policy-event-name"><div class="policy-event-title"><strong>' + esc(policyEventName(trigger.eventType)) + '</strong><span class="policy-event-guide"><button class="guide-trigger policy-event-guide-trigger" type="button" aria-label="Show ' + esc(policyEventName(trigger.eventType)) + ' workflow steps" aria-describedby="' + tooltipId + '"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button><div class="policy-event-tooltip" id="' + tooltipId + '" role="tooltip"><strong>' + esc(proposed ? "Proposed workflow" : "Adds " + trigger.steps.length + " " + pluralize("task", trigger.steps.length) + " to the Work Queue") + '</strong><ol>' + trigger.steps.map((step) => '<li><span>' + esc(step.title) + '</span><small>' + esc(eventStepSummary(step)) + '</small></li>').join("") + '</ol></div></span></div><small>' + esc(availability) + '</small></div><button class="button primary" type="button" data-start-event="' + esc(trigger.eventType) + '"' + (unavailable ? " disabled" : "") + '>Trigger Work</button></article>';
518
+ }
519
+
520
+ function policyEventName(eventType) {
521
+ return POLICY_EVENT_NAMES[eventType] || titleCase(humanize(eventType));
522
+ }
523
+
335
524
  function obligationCard(item, collapsed = false) {
336
525
  const type = item.actionItemId ? "action-item" : "obligation";
337
526
  const id = item.actionItemId || item.obligationId;
338
527
  const completion = !item.actionItemId ? obligationCompletionPlan(item) : null;
339
- const action = !state.readOnly && item.status !== "upcoming" && completion
528
+ const action = !state.readOnly && !["upcoming", "proposed"].includes(item.status) && completion
340
529
  ? completion.blocked
341
530
  ? '<a class="obligation-action blocked" href="' + completion.href + '">' + esc(completion.blocked) + '</a>'
342
531
  : '<button class="obligation-action" type="button" data-record-obligation="' + esc(item.key) + '">Record work</button>'
343
532
  : "";
344
- return '<article class="obligation-card status-' + esc(item.status) + '"' + (collapsed ? ' data-collapsed hidden' : "") + '><div class="obligation-card-head"><span>' + esc(item.kind === "event" ? "Event action" : item.activityType || "Recurring") + '</span><strong>' + esc(timingText(item)) + '</strong></div><h3><a href="#/resource/' + type + '/' + encodeURIComponent(id) + '">' + esc(titleCase(item.title)) + '</a></h3><p>' + esc(windowText(item)) + '</p><div class="obligation-card-foot"><div class="obligation-links">' + (item.policyIds || []).map(formatReference).join("") + '</div>' + action + '</div></article>';
533
+ const kind = item.kind === "event" ? "Policy Event Task" : item.kind === "action" ? "Assigned Follow-up" : properCase(item.activityType || "Recurring");
534
+ return '<article class="obligation-card status-' + esc(item.status) + '"' + (collapsed ? ' data-collapsed hidden' : "") + '><div class="obligation-card-head"><span>' + esc(kind) + '</span><strong>' + esc(timingText(item)) + '</strong></div><h3><a href="#/resource/' + type + '/' + encodeURIComponent(id) + '">' + esc(titleCase(item.title)) + '</a></h3><p>' + esc(windowText(item)) + '</p><div class="obligation-card-foot"><div class="obligation-links">' + (item.policyIds || []).map(formatReference).join("") + '</div>' + action + '</div></article>';
345
535
  }
346
536
 
347
537
  function obligationCompletionPlan(item) {
@@ -428,12 +618,6 @@ function obligationCompletionSeed(type, item, obligation) {
428
618
  };
429
619
  }
430
620
 
431
- function eventRunCard(run) {
432
- const percentage = run.actions.length ? Math.round((run.completeCount / run.actions.length) * 100) : 0;
433
- const occurred = run.occurredAt ? formatLocalDateTime(run.occurredAt) : formatCalendarDate(run.occurredOn);
434
- return '<article class="event-run"><div class="event-run-head"><div><span class="badge status-' + esc(run.status) + '">' + esc(run.status) + '</span><h3><a href="#/resource/obligation-event/' + encodeURIComponent(run.id) + '">' + esc(run.title) + '</a></h3><small>' + esc(occurred) + ' · ' + run.completeCount + ' of ' + run.actions.length + ' complete</small></div><strong>' + percentage + '%</strong></div><div class="progress"><span style="width:' + percentage + '%"></span></div><div class="event-actions">' + run.actions.map((action) => '<a href="#/resource/action-item/' + encodeURIComponent(action.actionItemId) + '"><span class="status-dot ' + (action.status === "complete" ? "good" : action.status === "overdue" ? "bad" : "warn") + '"></span><span><strong>' + esc(action.title) + '</strong><small>' + esc(action.status === "complete" ? "Complete" : windowText(action) + " · " + timingText(action)) + '</small></span></a>').join("") + '</div></article>';
435
- }
436
-
437
621
  function openObligationEventDialog(trigger) {
438
622
  const subjectType = trigger.eventType.startsWith("person") || trigger.eventType.includes("person-") ? "person"
439
623
  : trigger.eventType.startsWith("vendor") ? "vendor"
@@ -448,9 +632,9 @@ function openObligationEventDialog(trigger) {
448
632
  const dialog = document.createElement("dialog");
449
633
  dialog.className = "commit-dialog event-dialog";
450
634
  dialog.setAttribute("aria-labelledby", "event-dialog-title");
451
- dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Policy event</p><h2 id="event-dialog-title">' + esc(titleCase(trigger.prompt)) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>This creates one event record and ' + trigger.steps.length + ' linked action items. Review and commit them like any other compliance change.</p>' + eventField +
635
+ dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Policy event</p><h2 id="event-dialog-title">' + esc(policyEventName(trigger.eventType)) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>This creates one event record and adds ' + trigger.steps.length + ' linked tasks to the Work Queue.</p>' + eventField +
452
636
  (subjects.length ? '<label><span>Subject</span><select name="subject"><option value="">Select</option>' + subjects.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label>' : "") +
453
- '<label><span>Workflow name <small>optional</small></span><input name="title" maxlength="200" placeholder="' + esc(trigger.prompt.replace(/\?$/, "")) + '"></label><div class="event-dialog-steps">' + trigger.steps.map((step) => '<div><strong>' + esc(step.title) + '</strong><small>' + esc(eventStepSummary(step)) + '</small></div>').join("") + '</div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary">Create checklist</button></div></form>';
637
+ '<label><span>Workflow name <small>optional</small></span><input name="title" maxlength="200" placeholder="' + esc(policyEventName(trigger.eventType)) + '"></label><div class="event-dialog-steps">' + trigger.steps.map((step) => '<div><strong>' + esc(step.title) + '</strong><small>' + esc(eventStepSummary(step)) + '</small></div>').join("") + '</div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary">Add Tasks to Work Queue</button></div></form>';
454
638
  document.body.append(dialog);
455
639
  dialog.showModal();
456
640
  dialog.querySelector(".icon-button").addEventListener("click", () => dialog.close());
@@ -474,8 +658,14 @@ function openObligationEventDialog(trigger) {
474
658
  })
475
659
  });
476
660
  if (!response.ok) throw new Error(await responseMessage(response));
661
+ const created = await response.json();
662
+ policyEventFeedback = {
663
+ name: policyEventName(trigger.eventType),
664
+ taskCount: created.actions?.length || trigger.steps.length
665
+ };
477
666
  state = await fetchJson("/api/state");
478
667
  dialog.close();
668
+ history.replaceState(null, "", "#/stage/run");
479
669
  render();
480
670
  } catch (error) {
481
671
  form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
@@ -488,6 +678,10 @@ function openObligationEventDialog(trigger) {
488
678
  function renderAuditPacket(main, params = new URLSearchParams()) {
489
679
  const audits = resourcesOfType("audit");
490
680
  const evidence = resourcesOfType("evidence");
681
+ const fileGRCRecordTypes = new Set((state.model.evidenceSourceFamilies || [])
682
+ .filter((family) => family.collectionTestRequired === false)
683
+ .flatMap((family) => family.operationRecordTypes || []));
684
+ const fileGRCRecords = state.resources.filter(({ record }) => fileGRCRecordTypes.has(record.type));
491
685
  const requestedAudit = params.get("auditId");
492
686
  const selected = audits.find(({ record }) => record.id === requestedAudit)?.record || audits.find(({ record }) => record.status !== "complete")?.record || null;
493
687
  const today = currentDate();
@@ -499,13 +693,15 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
499
693
  const preflight = [
500
694
  ["Repository", state.git.available ? state.git.clean ? "Clean revision" : state.git.changes.length + " uncommitted" : "Git unavailable", "#/repository", state.git.clean ? "good" : "warn"],
501
695
  ["Engagement", selected ? selected.title : "No audit record", "#/resources/audit", selected ? "good" : "warn"],
502
- ["Evidence", evidence.length + " " + pluralize("record", evidence.length), "#/resources/evidence", evidence.length ? "good" : "warn"],
503
- ["Policy work", state.obligations.counts.overdue ? state.obligations.counts.overdue + " overdue" : state.obligations.counts.due + " due", "#/obligations", state.obligations.counts.overdue ? "bad" : state.obligations.counts.due ? "warn" : "good"]
696
+ ["FileGRC Evidence", fileGRCRecords.length + " operating " + pluralize("record", fileGRCRecords.length), "#/stage/run", "neutral"],
697
+ ["External Evidence", evidence.length + " " + pluralize("record", evidence.length), "#/resources/evidence", evidence.length ? "good" : "neutral"],
698
+ ["Policy work", state.obligations.counts.overdue ? state.obligations.counts.overdue + " overdue" : state.obligations.counts.due ? state.obligations.counts.due + " due" : state.obligations.counts.proposed ? state.obligations.counts.proposed + " proposals" : "No work due", "#/stage/run", state.obligations.counts.overdue ? "bad" : state.obligations.counts.due ? "warn" : state.obligations.counts.proposed ? "neutral" : "good"]
504
699
  ];
700
+ const evidencePaths = '<section class="panel audit-evidence-paths"><div class="panel-head"><div><p class="kicker">Evidence workflow</p><h3>Review both evidence paths</h3><p>Use the formal audit date or period. Each selected control may need one or both paths.</p></div></div><div class="audit-evidence-path-grid"><a href="#/stage/run"><span class="step-label">FileGRC Evidence</span><h4>Review operating records</h4><p>Complete the applicable Step 5 records, link them to their Controls, and record results in their fields or Markdown. Link any external artifact needed to support the result. The packet includes the records, Markdown, Git history, and linked artifacts.</p></a><a href="#/resources/evidence"><span class="step-label">External Evidence</span><h4>Review imported or referenced proof</h4><p>Verify the source System, audit date or period, Control links, collector, verifier, and fixed attachment or approved external reference. The packet includes the records, retained files, delivery index, and checksums.</p></a></div></section>';
505
701
  const dateFields = typeOne
506
702
  ? '<label><span>As-of date</span><input type="date" name="start" required value="' + esc(start) + '"></label>'
507
703
  : '<label><span>Period start</span><input type="date" name="start" required value="' + esc(start) + '"></label><label><span>Period end</span><input type="date" name="end" required value="' + esc(end) + '"></label>';
508
- main.innerHTML = '<div class="page audit-packet-page"><div class="page-intro"><div><p class="kicker">Audit readiness and evidence</p><h2>Prepare the Audit</h2><p>Complete management-owned scope, adoption, documents, source-system evidence, and Type 2 population reconciliation here. FileGRC builds a delivery index, control matrix, source-system and external-evidence indexes, source records, attachments, history, and checksums for the engagement date or period.</p></div></div><section class="packet-preflight" aria-label="Packet readiness">' + preflight.map(([label, value, href, tone]) => '<a href="' + href + '"><span class="status-dot ' + tone + '"></span><span><small>' + esc(label) + '</small><strong>' + esc(value) + '</strong></span></a>').join("") + '</section>' + renderAuditPreparation(preparation) + '<section class="panel packet-builder"><div class="panel-head"><div><p class="kicker">Evidence delivery</p><h3>' + (typeOne ? "Build the As-of Packet" : "Build the Period Packet") + '</h3></div></div><form id="packet-form">' + dateFields + '<label><span>Audit <small>required for delivery</small></span><select name="auditId"><option value="">Draft Without Audit Scope</option>' + audits.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === selected?.id ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><button class="button primary" type="submit" ' + (state.readOnly ? "disabled" : "") + '>' + (draft ? "Generate draft" : "Generate packet") + '</button></form><p class="packet-note">' + (state.readOnly ? "Packet generation requires the local writable renderer or the CLI." : draft ? "Drafts expose coverage gaps now. Commit a clean revision and select an audit record before delivery." : "The packet is derived under .filegrc/ and bound to the selected audit and current Git revision. FileGRC checks preparation and integrity; the engagement team determines evidence sufficiency.") + '</p><div class="dialog-error" role="alert"></div></section><div id="packet-results"></div></div>';
704
+ main.innerHTML = '<div class="page audit-packet-page"><div class="page-intro"><div><p class="kicker">Audit evidence and packet</p><h2>Prepare Fieldwork</h2><p>Confirm the firm-agreed date or period, review FileGRC Evidence and External Evidence, complete management documents and Type 2 populations, then build the indexed packet. FileGRC compiles both evidence paths with their links, control matrix, history, attachments, indexes, and checksums.</p></div></div><section class="packet-preflight" aria-label="Packet readiness">' + preflight.map(([label, value, href, tone]) => '<a href="' + href + '"><span class="status-dot ' + tone + '"></span><span><small>' + esc(label) + '</small><strong>' + esc(value) + '</strong></span></a>').join("") + '</section>' + evidencePaths + renderAuditPreparation(preparation) + '<section class="panel packet-builder"><div class="panel-head"><div><p class="kicker">Evidence delivery</p><h3>' + (typeOne ? "Build the As-of Packet" : "Build the Period Packet") + '</h3></div></div><form id="packet-form">' + dateFields + '<label><span>Audit <small>required for delivery</small></span><select name="auditId"><option value="">Draft Without Audit Scope</option>' + audits.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === selected?.id ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><button class="button primary" type="submit" ' + (state.readOnly ? "disabled" : "") + '>' + (draft ? "Generate draft" : "Generate packet") + '</button></form><p class="packet-note">' + (state.readOnly ? "Packet generation requires the local writable renderer or the CLI." : draft ? "Drafts expose coverage gaps now. Commit a clean revision and select an audit record before delivery." : "The packet is derived under .filegrc/ and bound to the selected audit and current Git revision. FileGRC checks preparation and integrity; the engagement team determines evidence sufficiency.") + '</p><div class="dialog-error" role="alert"></div></section><div id="packet-results"></div></div>';
509
705
  main.querySelector('select[name="auditId"]').addEventListener("change", (event) => {
510
706
  const next = event.currentTarget.value;
511
707
  location.hash = "#/audit-packet" + (next ? "?auditId=" + encodeURIComponent(next) : "");
@@ -600,20 +796,24 @@ function renderPacketResults(container, result) {
600
796
  const packet = result.packet;
601
797
  const ready = packet.readiness.status === "delivery-ready";
602
798
  container.innerHTML = '<section class="metrics packet-metrics">' +
603
- metric("Dated records", packet.summary.datedRecords, packet.summary.records + " total source records", "neutral") +
799
+ metric("FileGRC Evidence", packet.summary.fileGRCRecords, packet.summary.records + " total packet records", "neutral") +
604
800
  metric("Obligations", packet.summary.obligationOccurrences, packet.summary.eventRuns + " event workflows", "neutral") +
605
- metric("Evidence", packet.summary.evidence, packet.summary.policies + " policies · " + packet.summary.controls + " controls", "neutral") +
801
+ metric("External Evidence", packet.summary.evidence, packet.summary.policies + " policies · " + packet.summary.controls + " controls", "neutral") +
606
802
  metric("Review items", packet.summary.gaps, packet.summary.errors + " errors · " + packet.summary.warnings + " warnings", packet.summary.errors ? "bad" : packet.summary.warnings ? "warn" : "good") +
607
803
  '</section><section class="panel packet-output"><div class="panel-head"><div><p class="kicker">' + (ready ? "FileGRC management checks passed" : "Draft packet") + '</p><h3>' + esc(result.output) + '</h3></div>' + (result.packetUrl ? '<a class="button primary" href="' + esc(result.packetUrl) + '" target="_blank" rel="noreferrer">Open index</a>' : "") + '</div><p>The directory contains ' + result.files.length + ' files. ' + (ready ? "Verify the checksums, reconcile external deliveries, and let the engagement team confirm evidence sufficiency." : "Do not deliver it until every error is resolved and each warning has been reviewed.") + '</p></section>' +
608
- '<div class="dashboard-grid"><section class="panel span-2"><div class="panel-head"><h3>Coverage Gaps and Warnings</h3></div>' + (packet.gaps.length ? '<div class="packet-gaps">' + packet.gaps.map((gap) => '<div><span class="badge ' + (gap.severity === "error" ? "bad" : "warn") + '">' + esc(gap.severity) + '</span><p>' + esc(gap.message) + '</p></div>').join("") + '</div>' : empty("No packet gaps were detected.")) + '</section><section class="panel"><div class="panel-head"><h3>Included Evidence</h3></div>' + (packet.evidence.length ? '<div class="packet-list">' + packet.evidence.slice(0, 12).map((item) => '<a href="#/resource/evidence/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(item.status) + ' · ' + esc(item.evidenceKind) + '</small></a>').join("") + '</div>' : empty("No evidence records matched.")) + '</section></div>';
804
+ '<div class="dashboard-grid"><section class="panel span-2"><div class="panel-head"><h3>Coverage Gaps and Warnings</h3></div>' + (packet.gaps.length ? '<div class="packet-gaps">' + packet.gaps.map((gap) => '<div><span class="badge ' + (gap.severity === "error" ? "bad" : "warn") + '">' + esc(properCase(gap.severity)) + '</span><p>' + esc(gap.message) + '</p></div>').join("") + '</div>' : empty("No packet gaps were detected.")) + '</section><section class="panel"><div class="panel-head"><h3>Included FileGRC Evidence</h3></div>' + (packet.fileGRCRecords.length ? '<div class="packet-list">' + packet.fileGRCRecords.slice(0, 12).map((item) => '<a href="#/resource/' + encodeURIComponent(item.type) + '/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.type)) + ' · ' + esc(item.primaryDate) + '</small></a>').join("") + '</div>' : empty("No FileGRC Evidence matched.")) + '</section><section class="panel"><div class="panel-head"><h3>Included External Evidence</h3></div>' + (packet.evidence.length ? '<div class="packet-list">' + packet.evidence.slice(0, 12).map((item) => '<a href="#/resource/evidence/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.status)) + ' · ' + esc(properCase(item.evidenceKind)) + '</small></a>').join("") + '</div>' : empty("No External Evidence matched.")) + '</section></div>';
609
805
  }
610
806
 
611
807
  function obligationPreview(items) {
612
- return items.length ? '<div class="obligation-preview">' + items.map((item) => '<a href="#/obligations"><span class="status-dot ' + (item.status === "overdue" ? "bad" : item.status === "due" ? "warn" : "neutral") + '"></span><span><strong>' + esc(item.title) + '</strong><small>' + esc(timingText(item)) + '</small></span></a>').join("") + '</div>' : empty("No open obligations.");
808
+ return items.length ? '<div class="obligation-preview">' + items.map((item) => '<a href="#/stage/run"><span class="status-dot ' + (item.status === "overdue" ? "bad" : item.status === "due" ? "warn" : "neutral") + '"></span><span><strong>' + esc(item.title) + '</strong><small>' + esc(timingText(item)) + '</small></span></a>').join("") + '</div>' : empty("No open obligations.");
613
809
  }
614
810
 
615
811
  function eventReminderPreview(triggers) {
616
- return triggers.length ? '<div class="event-reminder-preview">' + triggers.map((trigger) => '<a href="#/obligations?event=' + encodeURIComponent(trigger.eventType) + '"><strong>' + esc(trigger.prompt) + '</strong><small>' + trigger.steps.length + ' required actions</small></a>').join("") + '</div>' : empty("No event reminders configured.");
812
+ return triggers.length ? '<div class="event-reminder-preview">' + triggers.map((trigger) => {
813
+ const proposed = trigger.programStatus === "proposed";
814
+ const href = proposed ? "#/stage/run?section=events" : "#/stage/run?event=" + encodeURIComponent(trigger.eventType);
815
+ return '<a href="' + href + '"><strong>' + esc(policyEventName(trigger.eventType)) + '</strong><small>' + trigger.steps.length + (proposed ? ' proposed actions' : ' required actions') + '</small></a>';
816
+ }).join("") + '</div>' : empty("No event reminders configured.");
617
817
  }
618
818
 
619
819
  function windowText(item) {
@@ -628,6 +828,7 @@ function windowText(item) {
628
828
  function timingText(item) {
629
829
  if (item.canceledAction) return "Action canceled; resolve or cancel the event";
630
830
  if (item.missingCompletion) return "Link required completion proof";
831
+ if (item.status === "proposed") return "Starter proposal";
631
832
  if (item.status === "overdue" && Number.isInteger(item.hoursOverdue)) {
632
833
  return item.hoursOverdue === 0 ? "Overdue less than 1 hour" : item.hoursOverdue + " hour" + (item.hoursOverdue === 1 ? "" : "s") + " overdue";
633
834
  }
@@ -669,7 +870,12 @@ function renderList(main, type, params = new URLSearchParams()) {
669
870
  const entries = resourcesOfType(type);
670
871
  const requestedPage = Number(params.get("page"));
671
872
  let pageNumber = Number.isInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
672
- const fields = [...new Set(["title", ...(definition.listFields || [])])].filter((name) => name !== "title");
873
+ const fields = [...new Set([
874
+ "title",
875
+ ...(definition.listFields || []),
876
+ ...(type === "control" ? ["$operationTracking"] : []),
877
+ ...(type === "obligation" ? ["$workQueueStatus"] : [])
878
+ ])].filter((name) => name !== "title");
673
879
  const modelFields = { ...state.model.commonFields, ...definition.fields };
674
880
  const filters = Object.entries(modelFields).filter(([, field]) => field.filter).map(([name, field]) => {
675
881
  const observed = entries.flatMap(({ record }) => Array.isArray(record[name]) ? record[name] : [record[name]]).filter((value) => ["string", "number", "boolean"].includes(typeof value)).map(String);
@@ -680,7 +886,7 @@ function renderList(main, type, params = new URLSearchParams()) {
680
886
  const guideTrigger = '<button class="guide-trigger" id="resource-guide-trigger" type="button" aria-label="About ' + esc(definition.pluralTitle) + '" aria-haspopup="dialog" aria-controls="resource-guide" aria-expanded="false"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button>';
681
887
  const listTools = '<div class="list-tools list-header-tools"><label><span class="sr-only">Filter list</span><input id="list-search" type="search" placeholder="Filter ' + esc(definition.pluralTitle.toLowerCase()) + '"></label>' +
682
888
  filters.map(({ name, label, values }) => '<select class="field-filter" data-field="' + esc(name) + '" aria-label="Filter by ' + esc(label.toLowerCase()) + '"><option value="">Any ' + esc(properCase(label)) + '</option>' + values.map((value) => '<option value="' + esc(value) + '">' + esc(filterOptionLabel(value)) + '</option>').join("") + '</select>').join("") + '<span id="result-count" aria-live="polite">' + entries.length + ' records</span>' + createButton + '</div>';
683
- main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">' + esc(readinessStageForType(type)?.title || (ORGANIZATION_RESOURCE_TYPES.includes(type) ? "Organization" : groupTitle(definition.group))) + '</p><div class="page-title-line"><h2>' + esc(titleCase(definition.pluralTitle)) + '</h2>' + guideTrigger + '</div></div>' + listTools + '</div>' + resourceGuide(type) +
889
+ main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">' + esc(readinessStageForType(type)?.title || groupTitle(definition.group)) + '</p><div class="page-title-line"><h2>' + esc(titleCase(definition.pluralTitle)) + '</h2>' + guideTrigger + '</div></div>' + listTools + '</div>' + resourceGuide(type) +
684
890
  '<section class="record-table-wrap"><table class="record-table"><thead><tr><th>' + esc(fieldLabel(type, "title")) + '</th>' + fields.map((name) => '<th>' + esc(fieldLabel(type, name)) + '</th>').join("") + '<th>Git file</th></tr></thead><tbody id="record-rows"></tbody></table></section>' +
685
891
  '<nav class="pagination list-pagination" aria-label="' + esc(definition.pluralTitle) + ' pages" hidden><button class="button" type="button" data-page="previous">Previous</button><span class="page-status" aria-live="polite"></span><button class="button" type="button" data-page="next">Next</button></nav></div>';
686
892
  resourceGuideCleanup = setupResourceGuide(main);
@@ -697,7 +903,7 @@ function renderList(main, type, params = new URLSearchParams()) {
697
903
  const start = (pageNumber - 1) * LIST_PAGE_SIZE;
698
904
  const visible = filtered.slice(start, start + LIST_PAGE_SIZE);
699
905
  main.querySelector("#result-count").textContent = filtered.length + (filtered.length === 1 ? " record" : " records");
700
- main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + formatValue(entry.record[name], name, type) + '</td>').join("") + '<td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 2) + '">' + empty("No records match this filter.") + '</td></tr>';
906
+ main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$operationTracking" ? controlOperationTracking(entry.record) : name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(entry.record[name], name, type)) + '</td>').join("") + '<td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 2) + '">' + empty("No records match this filter.") + '</td></tr>';
701
907
  pagination.hidden = totalPages === 1;
702
908
  previous.disabled = pageNumber === 1;
703
909
  next.disabled = pageNumber === totalPages;
@@ -762,10 +968,28 @@ function renderDetail(main, type, id) {
762
968
  const addRecordContent = recordContent && !entry.content[recordContent.slot] && !state.readOnly
763
969
  ? '<div class="record-content-action"><button class="button" type="button" id="add-record-content">Add Record Markdown</button></div>'
764
970
  : "";
765
- 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">' + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Evidence packet</a>' : "") + (!state.readOnly ? '<button class="button" id="edit-resource">Edit</button>' + (!definition.singleton ? '<button class="button danger" id="delete-resource">Delete</button>' : "") : "") + '</div></div><div class="detail-grid"><section class="panel detail-main">' +
766
- (narrativeContent || markdownContent ? narrativeContent + markdownContent + addRecordContent : '<div class="panel-head"><h3>Record</h3></div>' + empty("Add Record Markdown when this record needs context beyond its structured fields.") + addRecordContent) +
767
- '</section><aside><section class="panel"><div class="panel-head"><h3>Metadata</h3></div><dl class="metadata">' + sourceMetadata + visible.map(([name, value]) => '<div><dt>' + esc(fields[name]?.label || humanize(name)) + '</dt><dd>' + formatValue(value, name, type) + '</dd></div>').join("") + '</dl></section>' + resourceConnections(entry) + '<section class="panel"><div class="panel-head"><h3>File History</h3></div>' + (entry.history?.length ? '<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>' : empty("No committed history for this file.")) + '</section></aside></div></div>';
971
+ const hasRecordBody = Boolean(narrativeContent || markdownContent);
972
+ const addRecordContentAction = !hasRecordBody && addRecordContent
973
+ ? '<button class="button" type="button" id="add-record-content">Add Record Markdown</button>'
974
+ : "";
975
+ const issueActions = state.readOnly
976
+ ? ""
977
+ : (FINDING_SOURCE_TYPES.has(type) ? '<button class="button" type="button" data-record-finding>Record finding</button>' : "")
978
+ + (ACTION_ITEM_SOURCE_TYPES.has(type) ? '<button class="button" type="button" data-add-action-item>Add task</button>' : "");
979
+ const detailMain = hasRecordBody
980
+ ? '<section class="panel detail-main">' + narrativeContent + markdownContent + addRecordContent + '</section>'
981
+ : "";
982
+ 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">' + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence &amp; Packet</a>' : "") + issueActions + addRecordContentAction + (!state.readOnly ? '<button class="button" id="edit-resource">Edit</button>' + (!definition.singleton ? '<button class="button danger" id="delete-resource">Delete</button>' : "") : "") + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + '">' + detailMain +
983
+ '<aside><section class="panel"><div class="panel-head"><h3>Metadata</h3></div><dl class="metadata">' + sourceMetadata + visible.map(([name, value]) => '<div><dt>' + esc(fields[name]?.label || humanize(name)) + '</dt><dd>' + formatValue(value, name, type) + '</dd></div>').join("") + '</dl></section>' + resourceConnections(entry) + '<section class="panel"><div class="panel-head"><h3>File History</h3></div>' + (entry.history?.length ? '<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>' : empty("No committed history for this file.")) + '</section></aside></div></div>';
768
984
  main.querySelector("#edit-resource")?.addEventListener("click", () => openEditor(type, entry));
985
+ main.querySelector("[data-record-finding]")?.addEventListener("click", () => openEditor("finding", null, {
986
+ seed: issueSeed("finding", entry.record),
987
+ description: "Record only a confirmed gap that needs separate remediation tracking. Keep the report details in this source record’s Markdown."
988
+ }));
989
+ main.querySelector("[data-add-action-item]")?.addEventListener("click", () => openEditor("action-item", null, {
990
+ seed: issueSeed("action-item", entry.record),
991
+ description: "Create a separate task only when this follow-up needs its own assignee, deadline, and completion proof. It will appear in Work Queue."
992
+ }));
769
993
  main.querySelector("#add-record-content")?.addEventListener("click", () => openEditor(type, entry, { addRecordContent: true }));
770
994
  main.querySelectorAll("[data-edit-content]").forEach((button) => button.addEventListener("click", () => openContentEditor(entry, button.dataset.editContent)));
771
995
  main.querySelector("#delete-resource")?.addEventListener("click", async () => {
@@ -781,6 +1005,29 @@ function renderDetail(main, type, id) {
781
1005
  });
782
1006
  }
783
1007
 
1008
+ function issueSeed(type, source) {
1009
+ const owners = source.ownerIds || source.reviewerIds || source.assessorIds || source.testerIds || [];
1010
+ if (type === "finding") {
1011
+ return {
1012
+ title: "Finding from " + source.title,
1013
+ status: "open",
1014
+ severity: "medium",
1015
+ sourceResourceId: source.id,
1016
+ ownerIds: owners,
1017
+ identifiedOn: currentDate(),
1018
+ controlIds: source.controlIds || (source.controlId ? [source.controlId] : []),
1019
+ riskIds: source.riskIds || [],
1020
+ systemIds: source.systemIds || []
1021
+ };
1022
+ }
1023
+ return {
1024
+ title: "Follow up: " + source.title,
1025
+ status: "open",
1026
+ sourceResourceId: source.id,
1027
+ assigneeIds: owners
1028
+ };
1029
+ }
1030
+
784
1031
  function recordNarrative(record, fields) {
785
1032
  return Object.entries(record).filter(([name, value]) => RECORD_TEXT_FIELDS.has(name) && fields[name]?.type === "string" && String(value || "").trim());
786
1033
  }
@@ -836,7 +1083,6 @@ function resourceConnections(entry) {
836
1083
  function navigationResourceTypes() {
837
1084
  return [
838
1085
  ...READINESS_STAGES.flatMap((stage) => stage.sections.flatMap((section) => section.types)),
839
- ...ORGANIZATION_RESOURCE_TYPES,
840
1086
  "workspace",
841
1087
  "renderer-settings"
842
1088
  ];
@@ -845,10 +1091,8 @@ function navigationResourceTypes() {
845
1091
  function renderOrganization(main) {
846
1092
  const workspace = resourcesOfType("workspace")[0];
847
1093
  const renderer = rendererSettingsEntry();
848
- const people = resourcesOfType("person");
849
- const teams = resourcesOfType("team");
850
1094
  const organizationName = state.workspace.organizationName || "Organization";
851
- main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Administration</p><h2>' + esc(titleCase(organizationName)) + '</h2><p>Manage the organization records that supply ownership, accountability, shared teams, and renderer behavior across the compliance program.</p></div>' + (workspace ? '<a class="button primary" href="#/resource/workspace/' + encodeURIComponent(workspace.record.id) + '">Organization profile</a>' : "") + '</div><div class="organization-grid"><section class="panel organization-profile"><div class="panel-head"><div><p class="kicker">Organization</p><h3>Program Settings</h3></div></div><dl class="metadata"><div><dt>Name</dt><dd>' + esc(organizationName) + '</dd></div><div><dt>Timezone</dt><dd>' + esc(state.workspace.timezone) + '</dd></div><div><dt>Data model</dt><dd>Version ' + esc(state.workspace.dataModelVersion) + '</dd></div><div><dt>Repository</dt><dd>' + (state.git.available ? esc((state.git.branch || "detached") + " · " + state.git.shortCommit) : "Git unavailable") + '</dd></div></dl></section><section class="panel organization-directory"><div class="panel-head"><div><p class="kicker">Directory</p><h3>People and Teams</h3></div></div><div class="organization-links"><a href="#/resources/person"><span><strong>People</strong><small>Owners, approvers, trainees, reviewers, and contacts</small></span><b>' + people.length + '</b></a><a href="#/resources/team"><span><strong>Teams</strong><small>Committees, response groups, and shared ownership</small></span><b>' + teams.length + '</b></a></div></section><section class="panel organization-tools"><div class="panel-head"><div><p class="kicker">Workspace</p><h3>Renderer and Repository</h3></div></div><div class="organization-links">' + (renderer ? '<a href="#/resource/renderer-settings/' + encodeURIComponent(renderer.record.id) + '"><span><strong>Renderer Settings</strong><small>Committed behavior, including onboarding</small></span><b>›</b></a>' : "") + '<a href="#/repository"><span><strong>Repository</strong><small>Validation, file changes, history, and commits</small></span><b>›</b></a></div></section></div></div>';
1095
+ main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Administration</p><h2>' + esc(titleCase(organizationName)) + '</h2><p>Manage organization settings, renderer behavior, and repository state.</p></div>' + (workspace ? '<a class="button primary" href="#/resource/workspace/' + encodeURIComponent(workspace.record.id) + '">Organization profile</a>' : "") + '</div><div class="organization-grid"><section class="panel organization-profile"><div class="panel-head"><div><p class="kicker">Organization</p><h3>Program Settings</h3></div></div><dl class="metadata"><div><dt>Name</dt><dd>' + esc(organizationName) + '</dd></div><div><dt>Timezone</dt><dd>' + esc(state.workspace.timezone) + '</dd></div><div><dt>Data model</dt><dd>Version ' + esc(state.workspace.dataModelVersion) + '</dd></div><div><dt>Repository</dt><dd>' + (state.git.available ? esc((state.git.branch || "detached") + " · " + state.git.shortCommit) : "Git unavailable") + '</dd></div></dl></section><section class="panel organization-tools"><div class="panel-head"><div><p class="kicker">Workspace</p><h3>Renderer and Repository</h3></div></div><div class="organization-links">' + (renderer ? '<a href="#/resource/renderer-settings/' + encodeURIComponent(renderer.record.id) + '"><span><strong>Renderer Settings</strong><small>Committed behavior, including onboarding</small></span><b>›</b></a>' : "") + '<a href="#/repository"><span><strong>Repository</strong><small>Validation, file changes, history, and commits</small></span><b>›</b></a></div></section></div></div>';
852
1096
  }
853
1097
 
854
1098
  function renderRepository(main) {
@@ -876,18 +1120,22 @@ function renderRepository(main) {
876
1120
  ? '<button class="button" type="button" data-git-action="pull" ' + (pullDisabled ? 'disabled title="' + esc(pullDisabled) + '"' : "") + '>Pull with rebase</button>'
877
1121
  : "";
878
1122
  const commitButton = !state.readOnly && state.git.available && !state.git.clean
879
- ? '<button class="button primary" type="button" id="commit-workspace" ' + (state.validation.ok ? "" : 'disabled title="Fix validation errors before committing"') + '>' + (hasRemote ? "Commit and push" : "Commit locally") + '</button>'
1123
+ ? '<button class="button primary" type="button" id="commit-workspace" ' + (!state.git.branch
1124
+ ? 'disabled title="Check out a branch before committing"'
1125
+ : state.validation.ok ? "" : 'disabled title="Fix validation errors before committing"') + '>' + (hasRemote ? "Commit and push" : "Commit locally") + '</button>'
880
1126
  : "";
881
1127
  const pushButton = !state.readOnly && state.git.available && hasRemote
882
1128
  ? '<button class="button primary" type="button" data-git-action="push" ' + (pushDisabled ? 'disabled title="' + esc(pushDisabled) + '"' : "") + '>Push</button>'
883
1129
  : "";
884
- const repositoryInstructions = hasRemote
1130
+ const repositoryInstructions = !state.git.branch
1131
+ ? "This workspace is on a detached HEAD. Check out a branch before using browser commit, pull, or push."
1132
+ : hasRemote
885
1133
  ? "Pull remote changes with rebase, review the workspace diff, then commit and push together."
886
1134
  : "Review the workspace diff, then commit it locally. Add a Git remote when you want browser pull and push.";
887
1135
  const validationBody = state.validation.diagnostics.length
888
- ? '<div class="diagnostics">' + state.validation.diagnostics.map((item) => '<div><span class="badge ' + item.severity + '">' + esc(item.severity) + '</span><code>' + esc(item.path) + '</code><p>' + esc(item.message) + '</p></div>').join("") + '</div>'
1136
+ ? '<div class="diagnostics">' + state.validation.diagnostics.map((item) => '<div><span class="badge ' + item.severity + '">' + esc(properCase(item.severity)) + '</span><code>' + esc(item.path) + '</code><p>' + esc(item.message) + '</p></div>').join("") + '</div>'
889
1137
  : empty("No validation problems.");
890
- main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>' + repositoryInstructions + ' Git supplies authors, timestamps, messages, revisions, and file history.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + pullButton + commitButton + pushButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div><div class="dashboard-grid"><section class="panel"><div class="panel-head"><h3>Current Revision</h3></div><dl class="metadata"><div><dt>Branch</dt><dd>' + esc(state.git.branch || "Unavailable") + '</dd></div><div><dt>Upstream</dt><dd>' + esc(state.git.upstream || "Not configured") + '</dd></div><div><dt>Remotes</dt><dd>' + esc(state.git.remotes?.join(", ") || "None configured") + '</dd></div><div><dt>Commit</dt><dd><code>' + esc(state.git.commit || "Unavailable") + '</code></dd></div><div><dt>Working tree</dt><dd>' + (state.git.clean === null ? "Unavailable" : state.git.clean ? "Clean" : "Has changes") + '</dd></div><div><dt>Generated</dt><dd>' + esc(formatLocalDateTime(state.generatedAt)) + '</dd></div></dl></section><section class="panel span-2"><div class="panel-head"><h3>Uncommitted Changes</h3></div>' + (state.git.changes?.length ? '<ul class="changes">' + state.git.changes.map((change) => '<li><code>' + esc(change) + '</code></li>').join("") + '</ul>' : empty(state.git.available ? "No uncommitted changes." : state.git.message)) + '</section><section class="panel span-2"><div class="panel-head"><h3>Validation</h3><span class="badge ' + (state.validation.ok ? "good" : "bad") + '">' + (state.validation.ok ? "Passing" : "Needs attention") + '</span></div>' + validationBody + '</section></div></div>';
1138
+ main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>' + repositoryInstructions + ' Git supplies authors, timestamps, messages, revisions, and file history.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + pullButton + commitButton + pushButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div><div class="dashboard-grid"><section class="panel"><div class="panel-head"><h3>Current Revision</h3></div><dl class="metadata"><div><dt>Branch</dt><dd>' + esc(state.git.branch || (state.git.available ? "Detached HEAD" : "Unavailable")) + '</dd></div><div><dt>Upstream</dt><dd>' + esc(state.git.upstream || "Not configured") + '</dd></div><div><dt>Remotes</dt><dd>' + esc(state.git.remotes?.join(", ") || "None configured") + '</dd></div><div><dt>Commit</dt><dd><code>' + esc(state.git.commit || "Unavailable") + '</code></dd></div><div><dt>Working tree</dt><dd>' + (state.git.clean === null ? "Unavailable" : state.git.clean ? "Clean" : "Has changes") + '</dd></div><div><dt>Generated</dt><dd>' + esc(formatLocalDateTime(state.generatedAt)) + '</dd></div></dl></section><section class="panel span-2"><div class="panel-head"><h3>Uncommitted Changes</h3></div>' + (state.git.changes?.length ? '<ul class="changes">' + state.git.changes.map((change) => '<li><code>' + esc(change) + '</code></li>').join("") + '</ul>' : empty(state.git.available ? "No uncommitted changes." : state.git.message)) + '</section><section class="panel span-2"><div class="panel-head"><h3>Validation</h3><span class="badge ' + (state.validation.ok ? "good" : "bad") + '">' + (state.validation.ok ? "Passing" : "Needs attention") + '</span></div>' + validationBody + '</section></div></div>';
891
1139
  main.querySelector("#commit-workspace")?.addEventListener("click", openCommitDialog);
892
1140
  main.querySelectorAll("[data-git-action]").forEach((button) => button.addEventListener("click", () => runRepositoryGitAction(button.dataset.gitAction)));
893
1141
  main.querySelector("#start-onboarding")?.addEventListener("click", requestOnboarding);
@@ -980,23 +1228,14 @@ function resourceGuide(type) {
980
1228
  const definition = state.model.resources[type];
981
1229
  const guidance = definition?.guidance;
982
1230
  if (!definition || !guidance) return "";
1231
+ const instructions = stagePageSummary({ type });
983
1232
  const sources = (guidance.sourceResourceIds || [])
984
1233
  .map((id) => state.resources.find(({ record }) => record.id === id))
985
1234
  .filter(Boolean);
986
- const activityTypes = new Set(guidance.obligationActivityTypes || []);
987
- const obligations = activityTypes.size
988
- ? resourcesOfType("obligation").filter(({ record }) => record.status === "active" && activityTypes.has(record.activityType))
989
- : [];
990
1235
  const sourceLinks = sources.length
991
1236
  ? '<div class="guide-links">' + sources.map(({ record }) => '<a href="#/resource/' + encodeURIComponent(record.type) + '/' + encodeURIComponent(record.id) + '">' + esc(record.title) + '</a>').join("") + '</div>'
992
1237
  : "";
993
- const obligationLinks = obligations.length
994
- ? '<div class="guide-links">' + obligations.map(({ record }) => {
995
- const cadence = formatCadence(record.recurrence);
996
- return '<a href="#/resource/obligation/' + encodeURIComponent(record.id) + '">' + esc(record.title) + (cadence ? ' · ' + esc(cadence) : "") + '</a>';
997
- }).join("") + '</div>'
998
- : "";
999
- return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use ' + esc(definition.pluralTitle) + '" hidden><div><span>Use</span><p>' + esc(definition.description) + '</p></div><div><span>Policy basis</span><p>' + esc(guidance.policyBasis) + '</p>' + sourceLinks + '</div><div><span>Timing</span><p>' + esc(guidance.cadence) + '</p>' + obligationLinks + '</div></section>';
1238
+ return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use ' + esc(definition.pluralTitle) + '" hidden><div><span>Instructions</span><p>' + esc(instructions) + '</p></div><div><span>Use</span><p>' + esc(definition.description) + '</p></div><div><span>Policy basis</span><p>' + esc(guidance.policyBasis) + '</p>' + sourceLinks + '</div></section>';
1000
1239
  }
1001
1240
 
1002
1241
  function setupResourceGuide(main) {
@@ -1116,114 +1355,113 @@ function requestOnboarding() {
1116
1355
 
1117
1356
  function initialOnboardingDraft() {
1118
1357
  const systemEntry = resourcesOfType("system").find(({ record }) => record.inScope && record.status !== "retired");
1119
- const auditEntry = systemEntry
1120
- ? resourcesOfType("audit").find(({ record }) => ["planned", "in-progress", "fieldwork"].includes(record.status) && record.systemIds?.includes(systemEntry.record.id))
1121
- : null;
1122
1358
  const owner = resourcesOfType("person").find(({ record }) => record.status === "active")?.record;
1123
- const independentApprover = resourcesOfType("person").find(({ record }) => record.id === "person-independent-approver")?.record;
1124
1359
  return {
1125
1360
  systemId: systemEntry?.record.id || "",
1126
- auditId: auditEntry?.record.id || "",
1127
1361
  serviceName: systemEntry?.record.title || "",
1128
1362
  scope: systemEntry?.record.description || "",
1129
- ownerId: systemEntry?.record.ownerIds?.[0] || auditEntry?.record.ownerIds?.[0] || owner?.id || "",
1363
+ ownerId: systemEntry?.record.ownerIds?.[0] || owner?.id || "",
1130
1364
  criticality: systemEntry?.record.criticality || "high",
1131
1365
  dataClassification: systemEntry?.record.dataClassification || "Confidential",
1132
1366
  internetExposed: systemEntry?.record.internetExposed === false ? "false" : "true",
1133
- auditGoal: auditGoalFromKind(auditEntry?.record.auditKind),
1134
- independentApproverName: independentApprover?.title === "Independent Approver" ? "" : independentApprover?.title || "",
1135
- independentApproverEmail: independentApprover?.email || ""
1367
+ programGoal: programGoalFromKind(state.workspace.assuranceGoal)
1136
1368
  };
1137
1369
  }
1138
1370
 
1139
1371
  function onboardingSteps() {
1372
+ const files = {
1373
+ target: ".repo-chip",
1374
+ kicker: "Mental model",
1375
+ title: "Files are the program",
1376
+ body: "You or an agent add JSON records, Markdown, and evidence attachments under data/. This renderer edits those files, and Git records their history.",
1377
+ points: [
1378
+ "Use the UI, an editor, the CLI, or an agent; every path changes the same files.",
1379
+ "JSON holds structured records. Markdown holds policies, plans, minutes, and other long-form work.",
1380
+ "Repository pulls with rebase, then commits and pushes reviewed changes together.",
1381
+ "Agents and terminal users run git pull --rebase, git commit, and git push directly.",
1382
+ "The dashboard derives program status from the current repository state."
1383
+ ]
1384
+ };
1385
+ const path = {
1386
+ target: ".readiness-map",
1387
+ kicker: "Program model",
1388
+ title: "Follow the audit chain",
1389
+ body: "The shortest dependable path is to define scope, approve policies, implement controls, prepare the evidence process, and operate the program before audit fieldwork begins.",
1390
+ points: [
1391
+ "Scope starts with the people and oversight team, applicable criteria, commitments, material vendors, and in-scope systems.",
1392
+ "Program operation includes current risk assessments and risks, which may add or change controls as conditions change.",
1393
+ "Evidence preparation means cataloging authoritative systems, documenting extraction, and testing captures before the candidate period.",
1394
+ "The CPA firm, formal report period, fieldwork, and final report are the last stage. Engage earlier only when timing or scope needs outside input."
1395
+ ]
1396
+ };
1397
+ const obligations = {
1398
+ target: ".obligation-panel",
1399
+ kicker: "Obligations",
1400
+ title: "Work the policy queue",
1401
+ body: "Recurring policy work appears as upcoming, due, or overdue. Each item shows the full allowed completion range, the first overdue date, and a live countdown to that cutoff.",
1402
+ points: [
1403
+ "Quarterly means any date in that cycle is valid unless the policy sets a narrower window.",
1404
+ "Link a dated completion record and its evidence to satisfy one occurrence.",
1405
+ "The UI and FileGRC CLI use the same calculation."
1406
+ ]
1407
+ };
1408
+ const events = {
1409
+ target: ".event-reminder-panel",
1410
+ kicker: "Triggered work",
1411
+ title: "Complete a checklist when key events occur",
1412
+ body: "Use an event reminder for a new worker, role change, departure, personal device, vendor change or incident, material system or data-use change, or security incident. One action item is created for every policy requirement, with its own owner, evidence, due range, and cutoff.",
1413
+ points: [
1414
+ "The checklist stays open until every action is done and has the requested completion record or evidence.",
1415
+ "Hour-based rules keep the event time and exact cutoff; day-based rules keep the policy date range.",
1416
+ "Every action has a policy-based cutoff or a reasonable default deadline for the event.",
1417
+ "Agents start the identical workflow with the FileGRC CLI."
1418
+ ]
1419
+ };
1420
+ const reportTypes = {
1421
+ target: null,
1422
+ kicker: "SOC 2 report types",
1423
+ title: "Choose the report goal",
1424
+ body: [
1425
+ "SOC 2 is an independent CPA report on controls relevant to the selected Trust Services Criteria.",
1426
+ "Most customer requests focus on Security. Add Availability, Processing Integrity, Confidentiality, or Privacy only when the service and customer need call for them."
1427
+ ],
1428
+ sections: [
1429
+ {
1430
+ title: "Type 1",
1431
+ body: "A CPA evaluates whether the controls are suitably designed and implemented at a point in time. Type 1 is optional before Type 2, but it can help with an urgent customer request."
1432
+ },
1433
+ {
1434
+ title: "Type 2",
1435
+ body: "A CPA evaluates whether the controls operated consistently throughout an agreed review period, often six months. Dated evidence must cover that period."
1436
+ }
1437
+ ],
1438
+ afterSections: "Most evidence comes from production, identity, monitoring, and business systems. FileGRC records where it comes from and how it was collected."
1439
+ };
1440
+ const audit = {
1441
+ target: ".audit-panel",
1442
+ kicker: "Final stage",
1443
+ title: "Engage the firm and prepare fieldwork",
1444
+ body: "Once the program is ready and evidence collection is running, record the CPA firm and the agreed scope and period. Then reconcile populations, answer requests, and generate the delivery packet.",
1445
+ points: [
1446
+ "The audit record holds the firm-agreed date or period; the workspace keeps management's earlier candidate dates.",
1447
+ "Audit Readiness identifies missing management documents, populations, exact-period evidence, and request work.",
1448
+ "The CPA firm selects samples, tests controls, evaluates exceptions, and issues the report."
1449
+ ]
1450
+ };
1451
+ const setup = {
1452
+ target: null,
1453
+ kicker: "Initial scope",
1454
+ title: "Describe the service you plan to audit",
1455
+ body: "This records the management goal and creates the first in-scope system. It does not create an audit engagement. Next, finish Step 1 by confirming the starter people and oversight team, criteria, commitments, vendors, and systems."
1456
+ };
1140
1457
  return [
1141
- {
1142
- target: null,
1143
- kicker: "SOC 2 basics",
1144
- title: "SOC 2 is an audit away",
1145
- body: [
1146
- "SOC 2 is just an auditor's report that you have adopted a set of policies meeting certain criteria.",
1147
- "This includes information security criteria, but it can optionally include availability, processing integrity, confidentiality, and privacy criteria. Generally, customers requesting a SOC 2 report just need the information security criteria."
1148
- ],
1149
- sections: [
1150
- {
1151
- title: "Type 1",
1152
- body: "An auditor evaluates whether your policies and controls are suitably designed and in place at a specific point in time. It is a snapshot rather than a test of ongoing operation. Type 1 is not required for Type 2, but Type 1 can be helpful as a short-term solution for urgent customer requests."
1153
- },
1154
- {
1155
- title: "Type 2",
1156
- body: "An auditor evaluates whether your controls operated consistently throughout a review period, most commonly six months. You provide dated evidence showing that the policies were followed across that period."
1157
- }
1158
- ],
1159
- afterSections: "Just about all of the evidence needed for these audits will come from your software's production infrastructure + monitoring, or the business operations tracked in FileGRC."
1160
- },
1161
- {
1162
- target: ".repo-chip",
1163
- kicker: "Mental model",
1164
- title: "Files are the program",
1165
- body: "You or an agent add JSON records, Markdown, and evidence attachments under data/. This renderer edits those files, and Git records their history.",
1166
- points: [
1167
- "Use the UI, an editor, the CLI, or an agent; every path changes the same files.",
1168
- "JSON holds structured records. Markdown holds policies, plans, minutes, and other long-form work.",
1169
- "Repository pulls with rebase, then commits and pushes reviewed changes together.",
1170
- "Agents and terminal users run git pull --rebase, git commit, and git push directly.",
1171
- "The dashboard derives program status from the current repository state."
1172
- ]
1173
- },
1174
- {
1175
- target: ".readiness-map",
1176
- kicker: "Program model",
1177
- title: "Follow the audit chain",
1178
- body: "The program runs in one direction: define the system scope, map the criteria, adopt policies, operate controls, retain dated proof, and give the auditor a coherent record of the period.",
1179
- points: [
1180
- "Criteria are external expectations. Policies are company rules. Controls state the repeatable work that meets both.",
1181
- "Inventories and dated records of reviews, assessments, meetings, tests, and incidents show that controls are operating.",
1182
- "Evidence proves an occurrence; Git proves the history of every artifact."
1183
- ]
1184
- },
1185
- {
1186
- target: ".obligation-panel",
1187
- kicker: "Obligations",
1188
- title: "Work the policy queue",
1189
- body: "Recurring policy work appears as upcoming, due, or overdue. Each item shows the full allowed completion range, the first overdue date, and a live countdown to that cutoff.",
1190
- points: [
1191
- "Quarterly means any date in that cycle is valid unless the policy sets a narrower window.",
1192
- "Link a dated completion record and its evidence to satisfy one occurrence.",
1193
- "The UI and FileGRC CLI use the same calculation."
1194
- ]
1195
- },
1196
- {
1197
- target: ".event-reminder-panel",
1198
- kicker: "Triggered work",
1199
- title: "Complete a checklist when key events occur",
1200
- body: "Use an event reminder for a new worker, role change, departure, personal device, vendor change or incident, material system or data-use change, or security incident. One action item is created for every policy requirement, with its own owner, evidence, due range, and cutoff.",
1201
- points: [
1202
- "The checklist stays open until every action is done and has the requested completion record or evidence.",
1203
- "Hour-based rules keep the event time and exact cutoff; day-based rules keep the policy date range.",
1204
- "Every action has a policy-based cutoff or a reasonable default deadline for the event.",
1205
- "Agents start the identical workflow with the FileGRC CLI."
1206
- ]
1207
- },
1208
- {
1209
- target: ".audit-panel",
1210
- kicker: "Audit",
1211
- title: "Plan the engagement and generate evidence",
1212
- body: "Engage an independent CPA firm early, record the agreed scope and date or period, then generate a scoped delivery packet from the same files used to run the program.",
1213
- points: [
1214
- "Record the firm, scope, exact date or period, and each request in the audit.",
1215
- "Audit Readiness identifies missing management work, source systems, and evidence, including what to export and when.",
1216
- "For Type 2, reconcile every period population after close. A zero count still needs its source export and query.",
1217
- "Generate the same indexed packet from the UI or CLI, with scoped records, Markdown, fixed attachments, delivery indexes, and checksums.",
1218
- "FileGRC checks management preparation and packet integrity. The auditor selects samples, tests controls, evaluates exceptions, and issues the report."
1219
- ]
1220
- },
1221
- {
1222
- target: null,
1223
- kicker: "Initial scope",
1224
- title: "Describe the service you plan to audit",
1225
- body: "This creates or updates one in-scope system and, if selected, one planned audit. It also assigns the external independent reviewer required to approve policies and chair oversight. The reviewer must be separate from the policy owner and control operators."
1226
- }
1458
+ files,
1459
+ path,
1460
+ obligations,
1461
+ events,
1462
+ reportTypes,
1463
+ audit,
1464
+ setup
1227
1465
  ];
1228
1466
  }
1229
1467
 
@@ -1242,7 +1480,10 @@ function renderOnboardingStep() {
1242
1480
  const body = onboardingStep === steps.length - 1
1243
1481
  ? onboardingSetupForm()
1244
1482
  : description + explanation + afterSections;
1245
- onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + '<button class="button primary" type="button" data-onboarding="next">' + (onboardingStep === steps.length - 1 ? "Save setup" : "Next") + '</button></div>';
1483
+ const finalActions = onboardingStep === steps.length - 1
1484
+ ? '<button class="button" type="button" data-onboarding="draft">Save draft</button><button class="button primary" type="button" data-onboarding="next">Complete setup</button>'
1485
+ : '<button class="button primary" type="button" data-onboarding="next">Next</button>';
1486
+ onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
1246
1487
  onboardingDialog.querySelector('[data-onboarding="skip"]').addEventListener("click", cancelOnboarding);
1247
1488
  onboardingDialog.querySelector('[data-onboarding="back"]')?.addEventListener("click", () => {
1248
1489
  captureOnboardingForm();
@@ -1250,12 +1491,13 @@ function renderOnboardingStep() {
1250
1491
  renderOnboardingStep();
1251
1492
  });
1252
1493
  onboardingDialog.querySelector('[data-onboarding="next"]').addEventListener("click", () => {
1253
- if (onboardingStep === steps.length - 1) saveOnboarding();
1494
+ if (onboardingStep === steps.length - 1) saveOnboarding(false);
1254
1495
  else {
1255
1496
  onboardingStep += 1;
1256
1497
  renderOnboardingStep();
1257
1498
  }
1258
1499
  });
1500
+ onboardingDialog.querySelector('[data-onboarding="draft"]')?.addEventListener("click", () => saveOnboarding(true));
1259
1501
  const target = onboardingTarget(step);
1260
1502
  if (target) {
1261
1503
  target.classList.add("onboarding-focus");
@@ -1275,148 +1517,60 @@ function renderOnboardingStep() {
1275
1517
 
1276
1518
  function onboardingSetupForm() {
1277
1519
  const people = resourcesOfType("person").filter(({ record }) => record.status === "active");
1278
- const contactDomain = people
1279
- .map(({ record }) => record.email?.split("@")[1])
1280
- .find(Boolean);
1281
- const approverEmailPlaceholder = contactDomain ? "reviewer@" + contactDomain : "Reviewer email";
1282
1520
  const classifications = Object.keys(state.workspace.classificationDefinitions || {});
1283
1521
  if (onboardingDraft.dataClassification && !classifications.includes(onboardingDraft.dataClassification)) {
1284
1522
  classifications.push(onboardingDraft.dataClassification);
1285
1523
  }
1286
1524
  const currentSystem = onboardingDraft.systemId ? state.resources.find(({ record }) => record.id === onboardingDraft.systemId)?.record : null;
1287
- const currentAudit = onboardingDraft.auditId ? state.resources.find(({ record }) => record.id === onboardingDraft.auditId)?.record : null;
1288
1525
  const existing = [
1289
1526
  currentSystem ? "Updates system " + currentSystem.title + "." : "Creates a new in-scope system.",
1290
- currentAudit ? "Updates planned audit " + currentAudit.title + " when an audit objective is selected." : ""
1527
+ "Records a management program goal without creating an audit engagement."
1291
1528
  ].filter(Boolean).join(" ");
1292
- const gitStatus = state.git.available && state.git.remotes?.length
1529
+ const gitStatus = state.git.available && !state.git.branch
1530
+ ? '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Detached HEAD detected</strong><small>Setup changes can be saved, but browser commits, pulls, and pushes are disabled until you check out a branch.</small></span></div>'
1531
+ : state.git.available && state.git.remotes?.length
1293
1532
  ? '<div class="onboarding-git-status"><span class="status-dot good"></span><span><strong>Git repository and remote detected</strong><small>Setup changes will appear in the workspace diff. Browser commits push to the configured remote.</small></span></div>'
1294
1533
  : state.git.available
1295
1534
  ? '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git remote needed</strong><small>Saving and local commits still work. Add a remote before the browser can push.</small></span></div>'
1296
1535
  : '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Saving still works. Run <code>git init</code> at the workspace root before your first compliance commit.</small></span></div>';
1297
- return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label><span>External independent approver</span><input name="independentApproverName" required maxlength="200" value="' + esc(onboardingDraft.independentApproverName) + '" placeholder="Reviewer name"><small>Must be separate from the policy owner and control operators.</small></label><label><span>Approver email</span><input name="independentApproverEmail" type="email" required maxlength="320" value="' + esc(onboardingDraft.independentApproverEmail) + '" placeholder="' + esc(approverEmailPlaceholder) + '"></label><label class="wide"><span>Audit objective</span><select name="auditGoal" required><option value="none" ' + (onboardingDraft.auditGoal === "none" ? "selected" : "") + '>No Engagement Planned</option><option value="readiness" ' + (onboardingDraft.auditGoal === "readiness" ? "selected" : "") + '>Readiness Assessment</option><option value="type-1" ' + (onboardingDraft.auditGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.auditGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>Dates, auditor, subservice method, and final scope stay unset until the engagement is planned.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Saving writes JSON files but does not commit them.</p>';
1536
+ return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No Assurance Goal Yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program Readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>This records management intent only. It does not create an engagement or establish the formal report period.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Save draft keeps the system planned. Saving writes JSON files but does not commit them. Complete the remaining Step 1 pages next.</p>';
1298
1537
  }
1299
1538
 
1300
1539
  function captureOnboardingForm() {
1301
1540
  const form = onboardingDialog?.querySelector("#onboarding-setup");
1302
1541
  if (!form) return;
1303
1542
  const data = new FormData(form);
1304
- for (const name of ["serviceName", "scope", "ownerId", "criticality", "dataClassification", "internetExposed", "independentApproverName", "independentApproverEmail", "auditGoal"]) {
1543
+ for (const name of ["serviceName", "scope", "ownerId", "criticality", "dataClassification", "internetExposed", "programGoal"]) {
1305
1544
  onboardingDraft[name] = String(data.get(name) || "").trim();
1306
1545
  }
1307
1546
  }
1308
1547
 
1309
- async function saveOnboarding() {
1548
+ async function saveOnboarding(draft = false) {
1310
1549
  if (onboardingBusy) return;
1311
1550
  const form = onboardingDialog?.querySelector("#onboarding-setup");
1312
1551
  if (!form?.reportValidity()) return;
1313
1552
  captureOnboardingForm();
1314
- const policyOwner = state.resources.find(({ record }) => record.id === "person-policy-owner")?.record;
1315
- const sameName = policyOwner?.title?.trim().toLowerCase() === onboardingDraft.independentApproverName.toLowerCase();
1316
- const sameEmail = policyOwner?.email?.trim().toLowerCase() === onboardingDraft.independentApproverEmail.toLowerCase();
1317
- if (sameName || sameEmail) {
1318
- const field = form.elements[sameName ? "independentApproverName" : "independentApproverEmail"];
1319
- field.setCustomValidity("The independent approver must be a different person from the policy owner.");
1320
- field.reportValidity();
1321
- field.addEventListener("input", () => field.setCustomValidity(""), { once: true });
1322
- return;
1323
- }
1324
- setOnboardingBusy(true, "Saving…");
1553
+ setOnboardingBusy(true, draft ? "Saving draft…" : "Completing…");
1325
1554
  try {
1555
+ const response = await localFetch("/api/setup", {
1556
+ method: "POST",
1557
+ headers: { "content-type": "application/json" },
1558
+ body: JSON.stringify({
1559
+ serviceName: onboardingDraft.serviceName,
1560
+ boundary: onboardingDraft.scope,
1561
+ ownerId: onboardingDraft.ownerId,
1562
+ criticality: onboardingDraft.criticality,
1563
+ dataClassification: onboardingDraft.dataClassification,
1564
+ internetExposed: onboardingDraft.internetExposed === "true",
1565
+ programGoal: onboardingDraft.programGoal,
1566
+ systemId: onboardingDraft.systemId,
1567
+ draft
1568
+ })
1569
+ });
1570
+ if (!response.ok) throw new Error(await responseMessage(response));
1326
1571
  state = await fetchJson("/api/state");
1327
- const approverEntry = state.resources.find(({ record }) => record.id === "person-independent-approver");
1328
- const oversightTeamExists = state.resources.some(({ record }) => record.id === "team-security-risk-oversight");
1329
- const independentApprover = {
1330
- ...(approverEntry?.record || {}),
1331
- schemaVersion: 1,
1332
- id: "person-independent-approver",
1333
- type: "person",
1334
- title: onboardingDraft.independentApproverName,
1335
- status: "external",
1336
- email: onboardingDraft.independentApproverEmail,
1337
- role: "External Security and Risk Oversight Reviewer",
1338
- employmentType: "external-reviewer",
1339
- ...(oversightTeamExists ? { teamIds: ["team-security-risk-oversight"] } : {})
1340
- };
1341
- await writeOnboardingResource(independentApprover, approverEntry);
1342
- state = await fetchJson("/api/state");
1343
-
1344
- let systemEntry = onboardingDraft.systemId
1345
- ? state.resources.find(({ record }) => record.type === "system" && record.id === onboardingDraft.systemId)
1346
- : state.resources.find(({ record }) => (
1347
- record.type === "system"
1348
- && record.inScope === true
1349
- && record.title.trim().toLowerCase() === onboardingDraft.serviceName.toLowerCase()
1350
- ));
1351
- const systemId = systemEntry?.record.id || createResourceId("system", onboardingDraft.serviceName, state.resources.map(({ record }) => record.id));
1352
- onboardingDraft.systemId = systemId;
1353
- const system = {
1354
- ...(systemEntry?.record || {}),
1355
- schemaVersion: 1,
1356
- id: systemId,
1357
- type: "system",
1358
- title: onboardingDraft.serviceName,
1359
- status: systemEntry?.record.status || "active",
1360
- criticality: onboardingDraft.criticality,
1361
- ownerIds: [onboardingDraft.ownerId],
1362
- description: onboardingDraft.scope,
1363
- systemKind: systemEntry?.record.systemKind || "service",
1364
- dataClassification: onboardingDraft.dataClassification,
1365
- internetExposed: onboardingDraft.internetExposed === "true",
1366
- inScope: true
1367
- };
1368
- await writeOnboardingResource(system, systemEntry);
1369
- state = await fetchJson("/api/state");
1370
- systemEntry = state.resources.find(({ record }) => record.id === systemId);
1371
-
1372
- const controlsToLink = resourcesOfType("control").filter(({ record }) => (
1373
- !["not-applicable", "retired"].includes(record.status)
1374
- && !(record.systemIds || []).includes(systemId)
1375
- ));
1376
- if (controlsToLink.length) {
1377
- for (const controlEntry of controlsToLink) {
1378
- await writeOnboardingResource({
1379
- ...controlEntry.record,
1380
- systemIds: [...new Set([...(controlEntry.record.systemIds || []), systemId])]
1381
- }, controlEntry);
1382
- }
1383
- state = await fetchJson("/api/state");
1384
- }
1385
- if (onboardingDraft.auditGoal !== "none") {
1386
- let auditEntry = onboardingDraft.auditId
1387
- ? state.resources.find(({ record }) => record.type === "audit" && record.id === onboardingDraft.auditId)
1388
- : state.resources.find(({ record }) => (
1389
- record.type === "audit"
1390
- && record.auditKind === auditKindFromGoal(onboardingDraft.auditGoal)
1391
- && record.title === onboardingDraft.serviceName + " " + auditTitleFromGoal(onboardingDraft.auditGoal)
1392
- ));
1393
- const kind = auditKindFromGoal(onboardingDraft.auditGoal);
1394
- const title = onboardingDraft.serviceName + " " + auditTitleFromGoal(onboardingDraft.auditGoal);
1395
- const auditId = auditEntry?.record.id || createResourceId("audit", title, state.resources.map(({ record }) => record.id));
1396
- onboardingDraft.auditId = auditId;
1397
- const audit = {
1398
- ...(auditEntry?.record || {}),
1399
- schemaVersion: 1,
1400
- id: auditId,
1401
- type: "audit",
1402
- title: auditEntry?.record.title || title,
1403
- status: auditEntry?.record.status || "planned",
1404
- auditKind: kind,
1405
- frameworkIds: auditEntry?.record.frameworkIds || resourcesOfType("framework").filter(({ record }) => record.status === "active").map(({ record }) => record.id),
1406
- scope: onboardingDraft.scope,
1407
- ownerIds: [onboardingDraft.ownerId],
1408
- systemIds: [...new Set([...(auditEntry?.record.systemIds || []), systemId])],
1409
- requirementIds: auditEntry?.record.requirementIds || resourcesOfType("requirement").filter(({ record }) => record.applicability === "applicable").map(({ record }) => record.id),
1410
- controlIds: auditEntry?.record.controlIds || resourcesOfType("control").filter(({ record }) => !["not-applicable", "retired"].includes(record.status)).map(({ record }) => record.id),
1411
- contactIds: [...new Set([...(auditEntry?.record.contactIds || []), onboardingDraft.ownerId])]
1412
- };
1413
- await writeOnboardingResource(audit, auditEntry);
1414
- state = await fetchJson("/api/state");
1415
- }
1416
-
1417
- await persistOnboardingPreference(false);
1418
1572
  closeOnboarding();
1419
- history.replaceState(null, "", "#/");
1573
+ history.replaceState(null, "", draft ? "#/" : "#/stage/scope");
1420
1574
  render();
1421
1575
  } catch (error) {
1422
1576
  setOnboardingBusy(false);
@@ -1442,11 +1596,11 @@ async function cancelOnboarding() {
1442
1596
  async function persistOnboardingPreference(showOnboarding) {
1443
1597
  const entry = rendererSettingsEntry();
1444
1598
  if (!entry) throw new Error("Renderer settings are unavailable.");
1445
- await writeOnboardingResource({ ...entry.record, showOnboarding }, entry);
1599
+ await writeRendererSettingsResource({ ...entry.record, showOnboarding }, entry);
1446
1600
  state = await fetchJson("/api/state");
1447
1601
  }
1448
1602
 
1449
- async function writeOnboardingResource(record, entry) {
1603
+ async function writeRendererSettingsResource(record, entry) {
1450
1604
  const url = entry
1451
1605
  ? "/api/resource/" + encodeURIComponent(record.type) + "/" + encodeURIComponent(record.id)
1452
1606
  : "/api/resources";
@@ -1458,6 +1612,24 @@ async function writeOnboardingResource(record, entry) {
1458
1612
  if (!response.ok) throw new Error(await responseMessage(response));
1459
1613
  }
1460
1614
 
1615
+ async function toggleStagePageCompletion(button) {
1616
+ const entry = rendererSettingsEntry();
1617
+ if (!entry) throw new Error("Renderer settings are unavailable.");
1618
+ const completed = new Set(entry.record.completedStagePageIds || []);
1619
+ const pageId = button.dataset.stagePageCompletion;
1620
+ if (button.dataset.complete === "true") {
1621
+ completed.delete(pageId);
1622
+ (STAGE_PAGE_ID_ALIASES[pageId] || []).forEach((id) => completed.delete(id));
1623
+ } else {
1624
+ completed.add(pageId);
1625
+ }
1626
+ await writeRendererSettingsResource({
1627
+ ...entry.record,
1628
+ completedStagePageIds: [...completed].sort()
1629
+ }, entry);
1630
+ state = await fetchJson("/api/state");
1631
+ }
1632
+
1461
1633
  function setOnboardingBusy(busy, label = "") {
1462
1634
  if (!onboardingDialog) return;
1463
1635
  onboardingBusy = busy;
@@ -1563,21 +1735,13 @@ function closeOnboarding() {
1563
1735
  onboardingDialog.close();
1564
1736
  }
1565
1737
 
1566
- function auditGoalFromKind(kind) {
1738
+ function programGoalFromKind(kind) {
1567
1739
  if (kind === "soc-2-type-1") return "type-1";
1568
1740
  if (kind === "soc-2-type-2") return "type-2";
1569
1741
  if (kind === "readiness") return "readiness";
1570
1742
  return "none";
1571
1743
  }
1572
1744
 
1573
- function auditKindFromGoal(goal) {
1574
- return goal === "type-1" ? "soc-2-type-1" : goal === "type-2" ? "soc-2-type-2" : "readiness";
1575
- }
1576
-
1577
- function auditTitleFromGoal(goal) {
1578
- return goal === "type-1" ? "SOC 2 Type 1" : goal === "type-2" ? "SOC 2 Type 2" : "SOC 2 readiness assessment";
1579
- }
1580
-
1581
1745
  function openEditor(type, entry = null, options = {}) {
1582
1746
  const definition = state.model.resources[type];
1583
1747
  const fields = { ...state.model.commonFields, ...definition.fields };
@@ -1591,7 +1755,13 @@ function openEditor(type, entry = null, options = {}) {
1591
1755
  ...Object.entries(state.model.commonFields).filter(([, field]) => field.required).map(([name]) => name),
1592
1756
  ...(definition.required || [])
1593
1757
  ]);
1594
- const oneOf = new Set((definition.oneOf || []).flat().filter((name) => !name.startsWith("$markdown:")));
1758
+ const oneOfGroups = (definition.oneOf || []).map((group) => (
1759
+ Array.isArray(group) ? { fields: group } : group
1760
+ ));
1761
+ const oneOf = new Set(oneOfGroups.flatMap((group) => group.fields || []).filter((name) => !name.startsWith("$markdown:")));
1762
+ const activeOneOf = new Set(oneOfGroups
1763
+ .filter((group) => !group.when || conditionMatches(record, group.when))
1764
+ .flatMap((group) => group.fields || []));
1595
1765
  const names = [...new Set([
1596
1766
  "title",
1597
1767
  ...required,
@@ -1603,23 +1773,29 @@ function openEditor(type, entry = null, options = {}) {
1603
1773
  dialog.className = "editor";
1604
1774
  dialog.setAttribute("aria-labelledby", "resource-editor-title");
1605
1775
  const activeMarkdown = markdownDefinitions.filter((markdown) => (
1606
- markdown.required || markdown.oneOf || !entry || entry.content?.[markdown.name]
1776
+ markdown.required || markdown.requiredWhen || markdown.oneOf || !entry || entry.content?.[markdown.name]
1607
1777
  ));
1608
1778
  const recordContent = recordContentDefinition(type);
1609
1779
  const recordContentItem = recordContent ? entry?.content?.[recordContent.slot] : null;
1610
- dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + (entry ? "Edit record" : options.obligationCompletion ? "Record obligation work" : "Create record") + '</p><h2 id="resource-editor-title">' + esc(titleCase(entry?.record.title || record.title || definition.title)) + '</h2></div><button type="button" class="icon-button" data-editor-dismiss aria-label="Close">×</button></div><p>' + esc(options.description || "Fill the core fields below. Git will record the author, time, reason, and diff when you commit this file.") + '</p><div class="form-grid">' + names.map((name) => editorField(type, name, fields[name], record[name], required.has(name) || conditionMatches(record, fields[name].requiredWhen), Boolean(entry), oneOf.has(name))).join("") + '</div>' +
1780
+ dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + (entry ? "Edit record" : options.obligationCompletion ? "Record obligation work" : "Create record") + '</p><h2 id="resource-editor-title">' + esc(titleCase(entry?.record.title || record.title || definition.title)) + '</h2></div><button type="button" class="icon-button" data-editor-dismiss aria-label="Close">×</button></div><p>' + esc(options.description || "Fill the core fields below. Git will record the author, time, reason, and diff when you commit this file.") + '</p><div class="form-grid">' + names.map((name) => editorField(type, name, fields[name], record[name], required.has(name) || conditionMatches(record, fields[name].requiredWhen), Boolean(entry), oneOf.has(name), activeOneOf.has(name))).join("") + '</div>' +
1611
1781
  activeMarkdown.map((markdown) => {
1612
1782
  const generated = !entry?.content?.[markdown.name];
1613
- const source = entry?.content?.[markdown.name]?.source ?? "# " + (record.title || "New " + definition.title) + "\n\nDescribe this " + definition.title.toLowerCase() + " here.\n";
1614
- const requiredMark = markdown.required ? '<span class="required-mark">Required</span>' : markdown.oneOf ? '<span class="required-mark">One Required</span>' : "";
1615
- return '<label class="content-editor-field" data-content-editor="' + esc(markdown.name) + '"><span>' + esc(markdown.label) + ' Markdown' + requiredMark + '</span><textarea data-markdown-slot="' + esc(markdown.name) + '" data-generated-content="' + generated + '" spellcheck="true" ' + (markdown.required ? "required" : "") + '>' + esc(source) + '</textarea></label>';
1783
+ const source = entry?.content?.[markdown.name]?.source
1784
+ ?? (markdown.oneOf ? "" : "# " + (record.title || "New " + definition.title) + "\n\nDescribe this " + definition.title.toLowerCase() + " here.\n");
1785
+ const requiredNow = markdown.required || conditionMatches(record, markdown.requiredWhen);
1786
+ const requiredMark = markdown.required || markdown.requiredWhen
1787
+ ? '<span class="required-mark" ' + (requiredNow ? "" : "hidden") + '>Required</span>'
1788
+ : markdown.oneOf
1789
+ ? '<span class="required-mark" ' + (activeOneOf.has("$markdown:" + markdown.name) ? "" : "hidden") + '>One Required</span>'
1790
+ : "";
1791
+ return '<label class="content-editor-field" data-content-editor="' + esc(markdown.name) + '"><span>' + esc(markdown.label) + ' Markdown' + requiredMark + '</span><textarea data-markdown-slot="' + esc(markdown.name) + '" data-generated-content="' + generated + '" spellcheck="true" ' + (requiredNow ? "required" : "") + '>' + esc(source) + '</textarea></label>';
1616
1792
  }).join("") + renderRecordContentEditor(type, entry, options) +
1617
1793
  '<details class="advanced-editor"><summary>Advanced JSON</summary><p>Use this for optional fields, extensions, or bulk edits. Changes here replace the guided fields above.</p><textarea spellcheck="false" aria-label="Advanced resource JSON">' + esc(JSON.stringify(record, null, 2)) + '</textarea></details><div class="dialog-error" role="alert"></div><div class="dialog-actions"><button type="button" class="button" data-editor-dismiss>Cancel</button><button type="submit" class="button primary" id="save-record">' + esc(options.saveLabel || "Save file") + '</button></div></form>';
1618
1794
  document.body.append(dialog);
1619
1795
  dialog.showModal();
1620
1796
  dialog.addEventListener("close", () => dialog.remove());
1621
1797
  dialog.querySelectorAll("[data-editor-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
1622
- wireEditorRequirements(dialog, record, fields, definition.oneOf || []);
1798
+ wireEditorRequirements(dialog, record, fields, oneOfGroups, markdownDefinitions);
1623
1799
  dialog.querySelector(".advanced-editor textarea").addEventListener("input", () => {
1624
1800
  dialog.dataset.jsonDirty = "true";
1625
1801
  dialog.querySelector("form").noValidate = true;
@@ -1714,7 +1890,9 @@ function seedRecord(type, definition) {
1714
1890
  else if (field.values?.length) record[name] = field.values[0];
1715
1891
  else record[name] = "";
1716
1892
  }
1717
- for (const choices of definition.oneOf || []) {
1893
+ for (const group of definition.oneOf || []) {
1894
+ const choices = Array.isArray(group) ? group : group.fields || [];
1895
+ if (!Array.isArray(group) && !conditionMatches(record, group.when)) continue;
1718
1896
  if (choices.some((name) => record[name] !== undefined)) continue;
1719
1897
  const name = choices.find((candidate) => fields[candidate]);
1720
1898
  if (name) record[name] = fields[name].type === "array" ? [] : "";
@@ -1724,12 +1902,16 @@ function seedRecord(type, definition) {
1724
1902
 
1725
1903
  function dedicatedMarkdownDefinitions(type) {
1726
1904
  const definition = state.model.resources[type];
1727
- const choices = new Set((definition.oneOf || []).flat().filter((name) => name.startsWith("$markdown:")).map((name) => name.slice("$markdown:".length)));
1905
+ const choices = new Set((definition.oneOf || [])
1906
+ .flatMap((group) => Array.isArray(group) ? group : group.fields || [])
1907
+ .filter((name) => name.startsWith("$markdown:"))
1908
+ .map((name) => name.slice("$markdown:".length)));
1728
1909
  return Object.entries(definition.markdown || {}).map(([name, markdown]) => ({
1729
1910
  name,
1730
1911
  label: markdown.label || humanize(name),
1731
1912
  primary: Boolean(markdown.primary),
1732
1913
  required: Boolean(markdown.required),
1914
+ requiredWhen: markdown.requiredWhen || null,
1733
1915
  oneOf: choices.has(name)
1734
1916
  }));
1735
1917
  }
@@ -1757,10 +1939,10 @@ function renderRecordContentEditor(type, entry, options) {
1757
1939
  return '<details class="record-content-details" ' + (open ? "open" : "") + '><summary>' + (item ? "Record Markdown" : "Add Record Markdown") + '</summary><p>Use this when the structured fields do not capture the full record.</p>' + editor + '</details>';
1758
1940
  }
1759
1941
 
1760
- function editorField(type, name, field, value, required, editing, oneOfRequired = false) {
1942
+ function editorField(type, name, field, value, required, editing, oneOfRequired = false, oneOfActive = oneOfRequired) {
1761
1943
  const label = fieldLabel(type, name);
1762
1944
  const requiredMark = required || field.requiredWhen || oneOfRequired
1763
- ? '<span class="required-mark" ' + (required || oneOfRequired ? "" : "hidden") + '>' + (oneOfRequired ? "One Required" : "Required") + '</span>'
1945
+ ? '<span class="required-mark" ' + (required || oneOfActive ? "" : "hidden") + '>' + (oneOfRequired ? "One Required" : "Required") + '</span>'
1764
1946
  : "";
1765
1947
  const help = name === "title"
1766
1948
  ? (editing ? "Renaming this record will not change its stable ID." : "A stable ID and file name will be generated from this value.")
@@ -1819,7 +2001,7 @@ function fieldWrap(name, kind, label, requiredMark, control, help, required) {
1819
2001
  return '<div class="form-field" data-field-group="' + esc(name) + '" data-kind="' + esc(kind) + '" data-required="' + (required ? "true" : "false") + '"><div class="field-label" id="' + esc(labelId) + '">' + esc(label) + requiredMark + '</div>' + labelledControl + (help ? '<small>' + esc(help) + '</small>' : "") + '</div>';
1820
2002
  }
1821
2003
 
1822
- function wireEditorRequirements(dialog, base, fields, oneOfGroups) {
2004
+ function wireEditorRequirements(dialog, base, fields, oneOfGroups, markdownDefinitions = []) {
1823
2005
  const refreshGroup = (group, required) => {
1824
2006
  group.dataset.required = required ? "true" : "false";
1825
2007
  const mark = group.querySelector(".required-mark");
@@ -1853,13 +2035,29 @@ function wireEditorRequirements(dialog, base, fields, oneOfGroups) {
1853
2035
  if (!field.requiredWhen) continue;
1854
2036
  const group = dialog.querySelector('[data-field-group="' + CSS.escape(name) + '"]');
1855
2037
  if (!group) continue;
1856
- const required = Object.entries(field.requiredWhen).every(([conditionName, expected]) => currentValue(conditionName) === expected);
2038
+ const visible = !field.visibleWhen || conditionMatchesValues(field.visibleWhen, currentValue);
2039
+ const applicable = Object.entries(field.requiredWhen)
2040
+ .filter(([conditionName]) => conditionName !== "status")
2041
+ .every(([conditionName, expected]) => conditionValueMatches(currentValue(conditionName), expected));
2042
+ group.hidden = !visible || !applicable;
2043
+ const required = visible && applicable && conditionMatchesValues(field.requiredWhen, currentValue);
1857
2044
  refreshGroup(group, required);
1858
2045
  }
1859
2046
  for (const group of dialog.querySelectorAll('[data-kind="relation-array"][data-required="true"]')) {
1860
2047
  refreshGroup(group, true);
1861
2048
  }
1862
- for (const names of oneOfGroups) {
2049
+ for (const markdown of markdownDefinitions) {
2050
+ if (!markdown.requiredWhen) continue;
2051
+ const required = conditionMatchesValues(markdown.requiredWhen, currentValue);
2052
+ const wrapper = dialog.querySelector('[data-content-editor="' + CSS.escape(markdown.name) + '"]');
2053
+ const control = wrapper?.querySelector("textarea");
2054
+ const mark = wrapper?.querySelector(".required-mark");
2055
+ if (control) control.required = required;
2056
+ if (mark) mark.hidden = !required;
2057
+ }
2058
+ for (const definition of oneOfGroups) {
2059
+ const names = definition.fields || [];
2060
+ const active = !definition.when || conditionMatchesValues(definition.when, currentValue);
1863
2061
  const choices = names.map((name) => {
1864
2062
  const markdownName = name.startsWith("$markdown:") ? name.slice("$markdown:".length) : null;
1865
2063
  const group = dialog.querySelector('[data-field-group="' + CSS.escape(name) + '"]');
@@ -1871,6 +2069,15 @@ function wireEditorRequirements(dialog, base, fields, oneOfGroups) {
1871
2069
  control.required = false;
1872
2070
  control.setCustomValidity("");
1873
2071
  });
2072
+ for (const name of names) {
2073
+ const markdownName = name.startsWith("$markdown:") ? name.slice("$markdown:".length) : null;
2074
+ const wrapper = markdownName
2075
+ ? dialog.querySelector('[data-content-editor="' + CSS.escape(markdownName) + '"]')
2076
+ : dialog.querySelector('[data-field-group="' + CSS.escape(name) + '"]');
2077
+ const mark = wrapper?.querySelector(".required-mark");
2078
+ if (mark) mark.hidden = !active;
2079
+ }
2080
+ if (!active) continue;
1874
2081
  const choice = choices.find(({ present }) => present) || choices[0];
1875
2082
  if (choice) {
1876
2083
  choice.control.required = true;
@@ -1941,11 +2148,28 @@ function openContentEditor(entry, name) {
1941
2148
  }
1942
2149
 
1943
2150
  function bindCommon() {
1944
- root.querySelectorAll(".nav-heading, .nav-subheading").forEach((button) => button.addEventListener("click", () => {
2151
+ root.querySelectorAll("[data-stage-page-completion]").forEach((button) => button.addEventListener("click", async (event) => {
2152
+ event.preventDefault();
2153
+ event.stopPropagation();
2154
+ const label = button.textContent;
2155
+ button.disabled = true;
2156
+ button.textContent = "Saving…";
2157
+ try {
2158
+ await toggleStagePageCompletion(button);
2159
+ render();
2160
+ } catch (error) {
2161
+ button.disabled = false;
2162
+ button.textContent = label;
2163
+ button.title = error.message;
2164
+ }
2165
+ }));
2166
+ root.querySelectorAll(".nav-toggle, .nav-subgroup-toggle").forEach((button) => button.addEventListener("click", () => {
1945
2167
  const group = button.closest(".nav-group");
1946
2168
  const open = group.classList.toggle("open");
1947
2169
  setNavigationGroupOpen(group.dataset.group, open);
1948
2170
  button.setAttribute("aria-expanded", String(open));
2171
+ const label = button.closest(".nav-heading-row, .nav-subheading-row")?.querySelector(".nav-heading, .nav-subheading")?.textContent.trim() || "group";
2172
+ button.setAttribute("aria-label", (open ? "Collapse " : "Expand ") + label);
1949
2173
  }));
1950
2174
  const navButton = root.querySelector(".mobile-nav");
1951
2175
  const sidebar = root.querySelector(".sidebar");
@@ -1967,6 +2191,15 @@ function bindCommon() {
1967
2191
  navButton?.focus();
1968
2192
  });
1969
2193
  const search = root.querySelector("#global-search");
2194
+ const sidebarNavigation = root.querySelector(".sidebar-nav");
2195
+ sidebarNavigation?.addEventListener("scroll", () => {
2196
+ navigationScrollTop = sidebarNavigation.scrollTop;
2197
+ try {
2198
+ window.localStorage.setItem(NAV_SCROLL_STORAGE_KEY, String(navigationScrollTop));
2199
+ } catch {
2200
+ // Browser storage may be unavailable; navigation still preserves the position in memory.
2201
+ }
2202
+ }, { passive: true });
1970
2203
  search?.addEventListener("keydown", (event) => {
1971
2204
  if (event.key === "Enter" && search.value.trim()) {
1972
2205
  event.preventDefault();
@@ -2097,6 +2330,14 @@ function readNavigationGroupState() {
2097
2330
  return {};
2098
2331
  }
2099
2332
  }
2333
+ function readNavigationScrollTop() {
2334
+ try {
2335
+ const value = Number(window.localStorage.getItem(NAV_SCROLL_STORAGE_KEY));
2336
+ return Number.isFinite(value) && value >= 0 ? value : 0;
2337
+ } catch {
2338
+ return 0;
2339
+ }
2340
+ }
2100
2341
  function setNavigationGroupOpen(group, open) {
2101
2342
  navigationGroupState[group] = open;
2102
2343
  try {
@@ -2108,6 +2349,9 @@ function setNavigationGroupOpen(group, open) {
2108
2349
  function groupTitle(id) { return state.model.groups.find((group) => group.id === id)?.title || "Program"; }
2109
2350
  function fieldDefinition(type, name) { return state.model.resources[type]?.fields?.[name] || state.model.commonFields[name]; }
2110
2351
  function fieldLabel(type, name) {
2352
+ if (name === "$operationTracking") return "Operation tracking";
2353
+ if (name === "$workQueueStatus") return "Work Queue";
2354
+ if (type === "obligation" && name === "status") return "Configuration";
2111
2355
  if (name === "title") return state.model.resources[type]?.titleLabel || state.model.commonFields.title.label;
2112
2356
  const recordContent = recordContentDefinition(type);
2113
2357
  if (recordContent && name === recordContent.slot) return recordContent.label;
@@ -2127,13 +2371,32 @@ function titleCase(value) {
2127
2371
  return word.toLowerCase().replace(/(^|[-/])([a-z])/g, (_, boundary, letter) => boundary + letter.toUpperCase());
2128
2372
  }).join(" ");
2129
2373
  }
2130
- function conditionMatches(record, condition) { return Boolean(condition) && Object.entries(condition).every(([name, expected]) => record[name] === expected); }
2374
+ function conditionMatches(record, condition) {
2375
+ return Boolean(condition) && Object.entries(condition).every(([name, expected]) => (
2376
+ Array.isArray(expected) ? expected.includes(record[name]) : record[name] === expected
2377
+ ));
2378
+ }
2379
+
2380
+ function conditionMatchesValues(condition, valueFor) {
2381
+ return Boolean(condition) && Object.entries(condition).every(([name, expected]) => {
2382
+ const actual = valueFor(name);
2383
+ return conditionValueMatches(actual, expected);
2384
+ });
2385
+ }
2386
+
2387
+ function conditionValueMatches(actual, expected) {
2388
+ return Array.isArray(expected) ? expected.includes(actual) : actual === expected;
2389
+ }
2131
2390
  function formatValue(value, field, type) {
2132
2391
  if (value === undefined || value === null || value === "") return '<span class="muted">Not set</span>';
2133
2392
  const definition = fieldDefinition(type, field);
2134
2393
  if (definition?.type === "date") return esc(formatCalendarDate(value));
2135
2394
  if (definition?.type === "timestamp") return esc(formatLocalDateTime(value));
2136
- if (field === "status" || field.endsWith("Rating") || field === "severity" || field === "outcome") return '<span class="badge status-' + esc(String(value)) + '">' + esc(String(value)) + '</span>';
2395
+ if (type === "obligation" && field === "status") {
2396
+ const label = value === "active" ? "Enabled" : properCase(value);
2397
+ return '<span class="badge ' + (value === "active" ? "neutral" : "status-" + esc(String(value))) + '">' + esc(label) + '</span>';
2398
+ }
2399
+ if (field === "status" || field.endsWith("Rating") || field === "severity" || field === "outcome") return '<span class="badge status-' + esc(String(value)) + '">' + esc(properCase(value)) + '</span>';
2137
2400
  if (Array.isArray(value)) return value.length ? value.map((item) => typeof item === "object" ? '<code>' + esc(JSON.stringify(item)) + '</code>' : formatReference(item)).join(" ") : '<span class="muted">None</span>';
2138
2401
  if (field === "sourceReference" && typeof value === "object") {
2139
2402
  const href = safeExternalUrl(value.url);
@@ -2143,8 +2406,64 @@ function formatValue(value, field, type) {
2143
2406
  if (typeof value === "boolean") return value ? "Yes" : "No";
2144
2407
  const reference = state.resources.find(({ record }) => record.id === value);
2145
2408
  if (reference) return '<a class="tag relation" href="#/resource/' + encodeURIComponent(reference.record.type) + '/' + encodeURIComponent(reference.record.id) + '">' + esc(reference.record.title) + '</a>';
2409
+ if (definition?.type === "enum") return esc(properCase(value));
2146
2410
  return esc(String(value));
2147
2411
  }
2412
+ function controlOperationTracking(control) {
2413
+ const obligations = resourcesOfType("obligation")
2414
+ .map(({ record }) => record)
2415
+ .filter((record) => record.status !== "retired" && (record.controlIds || []).includes(control.id));
2416
+ if (!obligations.length) {
2417
+ return '<span class="operation-tracking evidence"><strong>Not scheduled in Work Queue</strong><small>Show operation with evidence records</small></span>';
2418
+ }
2419
+ const counts = obligations.reduce((result, obligation) => {
2420
+ const status = workQueueScheduleStatus(obligation, control);
2421
+ result[status] = (result[status] || 0) + 1;
2422
+ return result;
2423
+ }, {});
2424
+ const scheduleCount = obligations.length + " " + pluralize("schedule", obligations.length);
2425
+ if (counts.running === obligations.length) {
2426
+ return '<a class="operation-tracking running" href="#/stage/run"><strong>Running in Work Queue</strong><small>' + esc(scheduleCount) + '</small></a>';
2427
+ }
2428
+ if (counts["waiting-policy"] === obligations.length) {
2429
+ return '<a class="operation-tracking waiting" href="#/stage/run"><strong>Waiting for policy approval</strong><small>' + esc(scheduleCount) + ' enabled</small></a>';
2430
+ }
2431
+ if (counts["waiting-control"] === obligations.length) {
2432
+ return '<a class="operation-tracking waiting" href="#/stage/run"><strong>Ready when implemented</strong><small>' + esc(scheduleCount) + ' enabled</small></a>';
2433
+ }
2434
+ if (counts.paused === obligations.length) {
2435
+ return '<a class="operation-tracking paused" href="#/stage/run"><strong>Work Queue paused</strong><small>' + esc(scheduleCount) + '</small></a>';
2436
+ }
2437
+ const summary = [
2438
+ counts.running ? counts.running + " running" : "",
2439
+ counts["waiting-policy"] ? counts["waiting-policy"] + " waiting for policy" : "",
2440
+ counts["waiting-control"] ? counts["waiting-control"] + " waiting for implementation" : "",
2441
+ counts.paused ? counts.paused + " paused" : ""
2442
+ ].filter(Boolean).join(" · ");
2443
+ return '<a class="operation-tracking mixed" href="#/stage/run"><strong>Work Queue needs attention</strong><small>' + esc(summary) + '</small></a>';
2444
+ }
2445
+ function workQueueScheduleStatus(obligation, control = null) {
2446
+ if (obligation.status === "paused") return "paused";
2447
+ if (obligation.status !== "active") return obligation.status;
2448
+ const asOf = state.obligations?.asOf || new Date().toISOString().slice(0, 10);
2449
+ const policies = (obligation.policyIds || []).map((id) => state.resources.find(({ record }) => record.id === id)?.record);
2450
+ if (!policies.every((policy) => policy?.type === "policy" && policy.status === "active" && policy.effectiveOn && policy.effectiveOn <= asOf)) {
2451
+ return "waiting-policy";
2452
+ }
2453
+ const linkedControls = (obligation.controlIds || []).map((id) => state.resources.find(({ record }) => record.id === id)?.record);
2454
+ const implemented = control
2455
+ ? control.status === "implemented"
2456
+ : !linkedControls.length || linkedControls.some((record) => record?.type === "control" && record.status === "implemented");
2457
+ return implemented ? "running" : "waiting-control";
2458
+ }
2459
+ function obligationWorkQueueStatus(obligation) {
2460
+ const status = workQueueScheduleStatus(obligation);
2461
+ if (status === "running") return '<span class="operation-tracking running"><strong>Running</strong><small>Enabled and policy effective</small></span>';
2462
+ if (status === "waiting-policy") return '<span class="operation-tracking waiting"><strong>Waiting for policy</strong><small>Schedule enabled</small></span>';
2463
+ if (status === "waiting-control") return '<span class="operation-tracking waiting"><strong>Waiting for control</strong><small>Implement a linked control</small></span>';
2464
+ if (status === "paused") return '<span class="operation-tracking paused"><strong>Paused</strong><small>Schedule disabled</small></span>';
2465
+ return '<span class="operation-tracking"><strong>' + esc(properCase(status)) + '</strong></span>';
2466
+ }
2148
2467
  function formatReference(value) {
2149
2468
  const reference = state.resources.find(({ record }) => record.id === value);
2150
2469
  return reference ? '<a class="tag relation" href="#/resource/' + encodeURIComponent(reference.record.type) + '/' + encodeURIComponent(reference.record.id) + '">' + esc(reference.record.title) + '</a>' : '<span class="tag">' + esc(value) + '</span>';
@@ -2197,34 +2516,40 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
2197
2516
 
2198
2517
  export const APP_STYLES = String.raw`
2199
2518
  :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}
2200
- *{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:15px}.brand small{font-size:11px;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:13px;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:10px;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:10px;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:11px;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:17px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:9px;margin:0}.search{height:39px;max-width:480px;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:13px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:10px}.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:11px;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:28px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:13px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:10px;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:10px;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:25px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:10px;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:14px;margin:4px 0 0}.panel-head>a{font-size:11px;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:30px;font-weight:500;display:block}.progress-number span{font-size:10px;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:9px;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:10px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:11px}.due-list small{font-size:9px;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:10px}.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:10px}.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:31px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:13px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:12px;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:12px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:10px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:11px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:9px;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:9px;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:9px;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:11px;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:10px;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:9px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:29px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:23px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:15px;margin-top:1.7em}.markdown p,.markdown li{font-size:13px;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:11px}.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:9px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:11px;min-width:0}.compact-json{white-space:pre-wrap;font-size:9px}.git-panel>code{font-size:9px;word-break:break-all}.git-panel p{font-size:10px;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:9px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:10px}.history small{font-size:9px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:11px;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:11px;overflow-wrap:anywhere}.diagnostics code{font-size:9px;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:22px;cursor:pointer}.editor form>p{font-size:11px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:11px/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:11px;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}
2201
- .topbar-status{display:flex;align-items:center;gap:8px}.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:11px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
2202
- .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:11px}.audit-progress-empty span{margin-top:4px;color:var(--muted);font-size:9px}
2519
+ *{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:480px;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}.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}
2520
+ .topbar-status{display:flex;align-items:center;gap:8px}.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)}
2521
+ .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}
2203
2522
  .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)}
2204
2523
  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}
2205
2524
  .topbar{height:63px}
2206
- .brand{flex:0 0 auto;margin-bottom:18px}.sidebar-nav{--nav-control-width:14px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding-right:1px}.nav-home{margin-bottom:10px}.nav-stage>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0}.nav-stage>.nav-heading:hover{background:rgba(255,255,255,.08);color:#fff}.nav-stage-number{display:grid;place-items:center;width:22px;height:22px;border:1px solid rgba(255,255,255,.26);border-radius:50%;font-size:8px}.nav-stage-copy,.nav-stage-copy strong,.nav-stage-copy small{display:block;min-width:0}.nav-stage-copy strong{font-size:10px;line-height:1.25}.nav-stage-copy small{margin-top:2px;color:#aeb6d8;font-size:8px;line-height:1.25;font-weight:500}.nav-stage>.nav-items{margin:2px 0 8px 17px;padding:1px 0 5px 12px;border-left:1px solid rgba(255,255,255,.14)}.nav-subgroup>.nav-subheading,.nav-subgroup>.nav-items a{width:100%;display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding-right:6px}.nav-subgroup>.nav-subheading{border:0;background:none;padding-top:7px;padding-bottom:4px;padding-left:7px;color:#919bc4;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:8px;font-weight:780;cursor:pointer}.nav-control,.nav-control-slot{justify-self:end;width:var(--nav-control-width);text-align:right}.nav-subgroup>.nav-items{padding:1px 0 4px 3px}.nav-subgroup>.nav-items a{padding-top:6px;padding-bottom:6px;padding-left:8px;font-size:11px}.nav-group.open>.nav-heading>.chevron:before,.nav-group.open>.nav-subheading>.chevron:before{transform:rotate(45deg)}.nav-stage.open>.nav-items>.nav-subgroup:not(.open)>.nav-items{display:none}.sidebar-footer{flex:0 0 auto;margin:10px -18px -18px;padding:10px 18px 14px;background:#000024;border-top:1px solid rgba(255,255,255,.14)}.organization-nav{display:grid;grid-template-columns:32px minmax(0,1fr) 12px;gap:9px;align-items:center;padding:8px;border-radius:8px;color:#eef1ff;text-decoration:none}.organization-nav:hover,.organization-nav.current{background:rgba(255,255,255,.11)}.organization-mark{display:grid;place-items:center;width:32px;height:32px;border:1px solid rgba(255,255,255,.25);border-radius:50%;background:rgba(255,255,255,.08);font-size:11px;font-weight:800}.organization-nav strong,.organization-nav small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organization-nav strong{font-size:10px}.organization-nav small{margin-top:2px;color:#aeb6d8;font-size:8px}.organization-arrow{color:#aeb6d8;font-size:16px}.organization-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.organization-links{display:grid}.organization-links a{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-top:1px solid var(--line);text-decoration:none}.organization-links a:first-child{padding-top:0;border-top:0}.organization-links strong,.organization-links small{display:block}.organization-links strong{font-size:11px}.organization-links small{margin-top:3px;color:var(--muted);font-size:9px;line-height:1.4}.organization-links b{color:var(--accent);font-size:11px}.organization-links a:hover strong{color:var(--accent)}
2207
- .nav-stage>.nav-items>a.nav-direct{display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;width:100%;padding:6px 6px 6px 7px;font-size:11px}
2525
+ .sidebar-nav{scrollbar-width:none;-ms-overflow-style:none}.sidebar-nav::-webkit-scrollbar{display:none}
2526
+ .brand{flex:0 0 auto;margin-bottom:18px}.sidebar-nav{--nav-control-width:14px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding-right:1px}.nav-home{margin-bottom:10px}.nav-stage>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0}.nav-stage>.nav-heading:hover{background:rgba(255,255,255,.08);color:#fff}.nav-stage-number{display:grid;place-items:center;width:22px;height:22px;border:1px solid rgba(255,255,255,.26);border-radius:50%;font-size:9.6px}.nav-stage-copy,.nav-stage-copy strong,.nav-stage-copy small{display:block;min-width:0}.nav-stage-copy strong{font-size:12px;line-height:1.25}.nav-stage-copy small{margin-top:2px;color:#aeb6d8;font-size:9.6px;line-height:1.25;font-weight:500}.nav-stage>.nav-items{margin:2px 0 8px 17px;padding:1px 0 5px 12px;border-left:1px solid rgba(255,255,255,.14)}.nav-subgroup>.nav-subheading,.nav-subgroup>.nav-items a{width:100%;display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding-right:6px}.nav-subgroup>.nav-subheading{border:0;background:none;padding-top:7px;padding-bottom:4px;padding-left:7px;color:#919bc4;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;cursor:pointer}.nav-control,.nav-control-slot{justify-self:end;width:var(--nav-control-width);text-align:right}.nav-subgroup>.nav-items{padding:1px 0 4px 3px}.nav-subgroup>.nav-items a{padding-top:6px;padding-bottom:6px;padding-left:8px;font-size:13.2px}.nav-group.open>.nav-heading>.chevron:before,.nav-group.open>.nav-subheading>.chevron:before{transform:rotate(45deg)}.nav-stage.open>.nav-items>.nav-subgroup:not(.open)>.nav-items{display:none}.sidebar-footer{flex:0 0 auto;margin:10px -18px -18px;padding:10px 18px 14px;background:#000024;border-top:1px solid rgba(255,255,255,.14)}.organization-nav{display:grid;grid-template-columns:32px minmax(0,1fr) 12px;gap:9px;align-items:center;padding:8px;border-radius:8px;color:#eef1ff;text-decoration:none}.organization-nav:hover,.organization-nav.current{background:rgba(255,255,255,.11)}.organization-mark{display:grid;place-items:center;width:32px;height:32px;border:1px solid rgba(255,255,255,.25);border-radius:50%;background:rgba(255,255,255,.08);font-size:13.2px;font-weight:800}.organization-nav strong,.organization-nav small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organization-nav strong{font-size:12px}.organization-nav small{margin-top:2px;color:#aeb6d8;font-size:9.6px}.organization-arrow{color:#aeb6d8;font-size:19.2px}.organization-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.organization-links{display:grid}.organization-links a{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-top:1px solid var(--line);text-decoration:none}.organization-links a:first-child{padding-top:0;border-top:0}.organization-links strong,.organization-links small{display:block}.organization-links strong{font-size:13.2px}.organization-links small{margin-top:3px;color:var(--muted);font-size:10.8px;line-height:1.4}.organization-links b{color:var(--accent);font-size:13.2px}.organization-links a:hover strong{color:var(--accent)}
2527
+ .nav-stage>.nav-items>a.nav-direct{display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;width:100%;padding:6px 6px 6px 7px;font-size:13.2px}
2528
+ .nav-heading-row{display:grid;grid-template-columns:minmax(0,1fr) 24px;gap:2px;align-items:stretch}.nav-heading-row>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr);gap:6px;align-items:center;width:100%;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0;text-decoration:none}.nav-heading-row>.nav-heading:hover,.nav-heading-row>.nav-heading.current{background:rgba(255,255,255,.1);color:#fff}.nav-toggle{display:grid;place-items:center;width:24px;height:auto;min-height:100%;padding:0;border:0;border-radius:6px;background:none;color:#aeb6d8;cursor:pointer}.nav-toggle:hover{background:rgba(255,255,255,.1);color:#fff}.nav-chevron{display:block;width:12px;height:12px;place-self:center;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}.nav-subheading-row{display:grid;grid-template-columns:minmax(0,1fr) 22px;gap:2px;align-items:center;width:100%;padding:0;border:0;border-radius:6px;background:none;color:#919bc4;cursor:pointer}.nav-subheading-row:hover{background:rgba(255,255,255,.1);color:#fff}.nav-subheading-row>.nav-subheading{display:flex;align-items:center;min-width:0;padding:7px;color:inherit;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780}.nav-group.open>.nav-heading-row .nav-chevron,.nav-group.open>.nav-subheading-row .nav-chevron{transform:rotate(90deg)}
2529
+ .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}
2530
+ .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-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{position:relative;z-index:2;color:var(--accent)}.stage-page-completion.complete{color:#176143;border-color:#b9dac6;background:#edf7f1}.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}
2208
2531
  .button{text-decoration:none}
2209
- .home-page{padding-top:16px;padding-bottom:16px}.overview-hero{min-height:72px;padding:10px 20px;align-items:center}.overview-hero h2{font-size:22px;margin:3px 0 2px}.overview-hero p:not(.kicker){font-size:10px}.home-page .readiness-map{padding:12px 15px}.home-page .readiness-map-head{margin-bottom:8px}.home-page .readiness-flow a{padding:7px}
2532
+ .home-page{padding-top:16px;padding-bottom:16px}.overview-hero{min-height:72px;padding:10px 20px;align-items:center}.overview-hero h2{font-size:26.4px;margin:3px 0 2px}.overview-hero p:not(.kicker){font-size:12px}.home-page .readiness-map{padding:12px 15px}.home-page .readiness-map-head{margin-bottom:8px}.home-page .readiness-flow a{padding:7px}
2210
2533
  .overview-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px}.overview-grid>.audit-panel{grid-column:1/-1}
2211
- .overview-grid>.panel{padding:15px}.overview-grid .panel-head{margin-bottom:10px}.overview-grid .audit-progress{gap:7px 20px}.overview-grid .progress-number strong{font-size:26px}.overview-grid .audit-engagement{padding:8px 11px}
2212
- .nav-close,.nav-scrim{display:none}.pagination{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:14px}.pagination[hidden]{display:none}.page-status{color:var(--muted);font-size:10px;min-width:150px;text-align:center}.button:disabled{cursor:not-allowed;opacity:.45}.search-pagination{padding-top:2px}
2534
+ .overview-grid>.panel{padding:15px}.overview-grid .panel-head{margin-bottom:10px}.overview-grid .audit-progress{gap:7px 20px}.overview-grid .progress-number strong{font-size:31.2px}.overview-grid .audit-engagement{padding:8px 11px}
2535
+ .nav-close,.nav-scrim{display:none}.pagination{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:14px}.pagination[hidden]{display:none}.page-status{color:var(--muted);font-size:12px;min-width:150px;text-align:center}.button:disabled{cursor:not-allowed;opacity:.45}.search-pagination{padding-top:2px}
2213
2536
  .list-tools{flex-wrap:wrap}.list-tools label{min-width:220px}.list-header-tools{flex:1;justify-content:flex-end;margin:0 0 0 28px}.list-header-tools label{flex:1 1 220px;max-width:360px}.list-header-tools select{max-width:190px}.list-header-tools .button{white-space:nowrap}
2214
- .setup-banner{margin:14px 0;background:#eef1ff;border:1px solid #ccd4ff;border-radius:11px;padding:19px 22px;display:grid;grid-template-columns:1fr 1.3fr;gap:25px;align-items:center}.setup-banner h3{margin:5px 0 6px;font-size:15px}.setup-banner p:not(.kicker){margin:0;color:var(--muted);font-size:11px;line-height:1.5}.setup-banner ol{margin:0;padding-left:22px;display:grid;gap:7px}.setup-banner li{font-size:11px}.setup-banner a{color:var(--accent);font-weight:700}.due-list time.overdue{color:var(--red)}.content-label{display:flex;align-items:center;justify-content:space-between;gap:12px}.text-button{border:0;background:none;color:var(--accent);font-size:9px;text-transform:uppercase;letter-spacing:.06em;font-weight:750;cursor:pointer;white-space:nowrap}.tag{white-space:normal;overflow-wrap:anywhere;max-width:100%}.editor{max-height:calc(100vh - 30px);overflow:auto}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin:20px 0}.form-field>.field-label,.content-editor-field>span{display:flex;align-items:center;justify-content:space-between;gap:8px;color:#3e4557;font-size:10px;font-weight:700;margin-bottom:6px}.required-mark{font-size:8px;color:var(--accent);text-transform:uppercase;letter-spacing:.06em}.form-field input,.form-field select,.editor .form-field textarea{width:100%;height:auto;min-height:40px;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--ink);padding:9px 10px;font:12px/1.4 inherit}.editor .form-field textarea{height:82px}.form-field input[readonly]{background:#eef0f6;color:#5d6475}.form-field>small{display:block;color:#6a7181;font-size:9px;margin-top:5px}.checkbox-list{display:grid;gap:5px;max-height:145px;overflow:auto;border:1px solid var(--line);border-radius:7px;padding:7px}.checkbox-list label{display:flex;align-items:center;gap:8px;padding:5px;border-radius:5px}.checkbox-list input{width:16px;min-height:16px;padding:0;flex:0 0 auto}.checkbox-list label:hover{background:#f2f4fa}.checkbox-list span,.checkbox-list small{display:block;font-size:10px}.checkbox-list small{color:var(--muted);margin-top:2px}.missing-options{padding:11px;border:1px dashed #d7c8a9;background:#fbf5e9;color:#795b23;border-radius:7px;font-size:10px}.content-editor-field{display:block;margin:17px 0}.editor .content-editor-field textarea,.editor .markdown-source{height:260px;background:#10162b;color:#e8ebff;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.advanced-editor{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.advanced-editor summary{cursor:pointer;color:var(--accent);font-size:11px;font-weight:750}.advanced-editor p{font-size:10px;color:var(--muted)}.editor .advanced-editor>textarea{height:320px}.alert-dialog{width:min(520px,calc(100vw - 30px));border:0;border-radius:12px;padding:23px;box-shadow:0 25px 80px rgba(0,0,24,.28)}.alert-dialog>p{font-size:12px;line-height:1.55;color:var(--muted)}.metadata dd{overflow-wrap:anywhere}
2215
- .record-content-action{display:flex;justify-content:flex-start;margin-top:20px}.record-content-details{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.record-content-details summary{cursor:pointer;color:var(--accent);font-size:11px;font-weight:750}.record-content-details>p{color:var(--muted);font-size:10px}.record-content-editor>span small{color:var(--muted);font-size:9px;font-weight:500}
2216
- .program-setup{grid-template-columns:minmax(250px,.75fr) minmax(440px,1.4fr)}.setup-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.setup-steps a{display:grid;grid-template-columns:24px minmax(0,1fr);gap:9px;align-items:start;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);text-decoration:none}.setup-steps a:hover{border-color:var(--accent-light)}.setup-steps a>span:first-child{display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-size:10px}.setup-steps a.done>span:first-child{background:#dcefe4;color:#125733}.setup-steps strong,.setup-steps small{display:block}.setup-steps strong{font-size:10px}.setup-steps small{margin-top:3px;color:var(--muted);font-size:8px;line-height:1.4;font-weight:500}
2217
- .readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,.65fr) minmax(320px,1fr);gap:28px;align-items:end;margin-bottom:17px}.readiness-map-head h3{font-size:15px;margin:5px 0 0}.readiness-map-head>p{max-width:710px;color:var(--muted);font-size:11px;line-height:1.5;margin:0}.readiness-flow{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:8px;font-weight:800}.readiness-flow strong{font-size:10px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:8px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:7px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:11px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:9px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:9px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:9px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:8px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:9px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:14px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:10px}.connections small{margin-top:3px;color:var(--muted);font-size:8px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:8px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:10px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:8px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:11px}.external-source:hover strong{text-decoration:underline}
2218
- .page-title-line{display:flex;align-items:center;gap:8px}.guide-trigger{display:grid;place-items:center;width:24px;height:24px;flex:0 0 auto;padding:0;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted);cursor:pointer}.guide-trigger svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}.guide-trigger:hover{border-color:var(--accent-light);color:var(--accent)}.guide-trigger:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.page-guide{display:grid;grid-template-columns:1.05fr 1.25fr 1fr;gap:0;margin:0;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.resource-guide-popover{position:fixed;z-index:40;overflow:auto;box-shadow:0 18px 50px rgba(0,0,24,.24)}.resource-guide-popover[hidden]{display:none}.page-guide>div{padding:14px 16px;border-left:1px solid var(--line);min-width:0}.page-guide>div:first-child{border-left:0}.page-guide>div>span{display:block;color:var(--accent);text-transform:uppercase;letter-spacing:.09em;font-size:8px;font-weight:780;margin-bottom:6px}.page-guide p{color:var(--muted);font-size:10px;line-height:1.5;margin:0}.guide-links{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.guide-links a{color:var(--accent);background:var(--accent-soft);border-radius:99px;padding:4px 7px;text-decoration:none;font-size:8px;font-weight:700}
2219
- .page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:11px}.repository-sync-status.error{color:var(--red)}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:auto}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:25px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:12px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:11px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:11px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:10px;line-height:1.5}.onboarding-actions{padding:12px 25px 23px}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:11px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:10px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:12px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:9px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:9px;line-height:1.5;margin:12px 25px 0}.onboarding-dialog>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2220
- .page-intro,.detail-head{align-items:center;margin-bottom:12px}.actions{align-items:center}.detail-head>div:first-child{min-width:0}.detail-head h2{margin:7px 0}.detail-head .header-breadcrumbs{margin:0;font-size:9px;line-height:normal;min-height:11px;align-items:center}.header-breadcrumbs span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60ch}
2537
+ .setup-banner{margin:14px 0;background:#eef1ff;border:1px solid #ccd4ff;border-radius:11px;padding:19px 22px;display:grid;grid-template-columns:1fr 1.3fr;gap:25px;align-items:center}.setup-banner h3{margin:5px 0 6px;font-size:18px}.setup-banner p:not(.kicker){margin:0;color:var(--muted);font-size:13.2px;line-height:1.5}.setup-banner ol{margin:0;padding-left:22px;display:grid;gap:7px}.setup-banner li{font-size:13.2px}.setup-banner a{color:var(--accent);font-weight:700}.due-list time.overdue{color:var(--red)}.content-label{display:flex;align-items:center;justify-content:space-between;gap:12px}.text-button{border:0;background:none;color:var(--accent);font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;font-weight:750;cursor:pointer;white-space:nowrap}.tag{white-space:normal;overflow-wrap:anywhere;max-width:100%}.editor{max-height:calc(100vh - 30px);overflow:auto}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin:20px 0}.form-field>.field-label,.content-editor-field>span{display:flex;align-items:center;justify-content:space-between;gap:8px;color:#3e4557;font-size:12px;font-weight:700;margin-bottom:6px}.required-mark{font-size:9.6px;color:var(--accent);text-transform:uppercase;letter-spacing:.06em}.form-field input,.form-field select,.editor .form-field textarea{width:100%;height:auto;min-height:40px;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--ink);padding:9px 10px;font:14.4px/1.4 inherit}.editor .form-field textarea{height:82px}.form-field input[readonly]{background:#eef0f6;color:#5d6475}.form-field>small{display:block;color:#6a7181;font-size:10.8px;margin-top:5px}.checkbox-list{display:grid;gap:5px;max-height:145px;overflow:auto;border:1px solid var(--line);border-radius:7px;padding:7px}.checkbox-list label{display:flex;align-items:center;gap:8px;padding:5px;border-radius:5px}.checkbox-list input{width:16px;min-height:16px;padding:0;flex:0 0 auto}.checkbox-list label:hover{background:#f2f4fa}.checkbox-list span,.checkbox-list small{display:block;font-size:12px}.checkbox-list small{color:var(--muted);margin-top:2px}.missing-options{padding:11px;border:1px dashed #d7c8a9;background:#fbf5e9;color:#795b23;border-radius:7px;font-size:12px}.content-editor-field{display:block;margin:17px 0}.editor .content-editor-field textarea,.editor .markdown-source{height:260px;background:#10162b;color:#e8ebff;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.advanced-editor{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.advanced-editor summary{cursor:pointer;color:var(--accent);font-size:13.2px;font-weight:750}.advanced-editor p{font-size:12px;color:var(--muted)}.editor .advanced-editor>textarea{height:320px}.alert-dialog{width:min(520px,calc(100vw - 30px));border:0;border-radius:12px;padding:23px;box-shadow:0 25px 80px rgba(0,0,24,.28)}.alert-dialog>p{font-size:14.4px;line-height:1.55;color:var(--muted)}.metadata dd{overflow-wrap:anywhere}
2538
+ .record-content-action{display:flex;justify-content:flex-start;margin-top:20px}.record-content-details{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.record-content-details summary{cursor:pointer;color:var(--accent);font-size:13.2px;font-weight:750}.record-content-details>p{color:var(--muted);font-size:12px}.record-content-editor>span small{color:var(--muted);font-size:10.8px;font-weight:500}
2539
+ .program-setup{grid-template-columns:minmax(250px,.75fr) minmax(440px,1.4fr)}.setup-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.setup-steps a{display:grid;grid-template-columns:24px minmax(0,1fr);gap:9px;align-items:start;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);text-decoration:none}.setup-steps a:hover{border-color:var(--accent-light)}.setup-steps a>span:first-child{display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-size:12px}.setup-steps a.done>span:first-child{background:#dcefe4;color:#125733}.setup-steps strong,.setup-steps small{display:block}.setup-steps strong{font-size:12px}.setup-steps small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4;font-weight:500}
2540
+ .readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;align-items:baseline}.readiness-progress-summary>div>span{color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.readiness-progress-summary>div>strong{font-size:9.6px;font-weight:700;line-height:1.2}.readiness-progress-summary .progress,.readiness-progress-summary small{grid-column:1/-1}.readiness-progress-summary small{color:var(--muted);font-size:9.6px}.readiness-progress-summary>.button{white-space:nowrap}.readiness-flow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:9.6px;font-weight:800}.readiness-flow strong{font-size:12px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:9.6px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8.4px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:13.2px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:10.8px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:10.8px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:9.6px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:16.8px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:9.6px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:12px}.connections small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:9.6px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:12px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:13.2px}.external-source:hover strong{text-decoration:underline}
2541
+ .page-title-line{display:flex;align-items:center;gap:8px}.guide-trigger{display:grid;place-items:center;width:24px;height:24px;flex:0 0 auto;padding:0;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted);cursor:pointer}.guide-trigger svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}.guide-trigger:hover{border-color:var(--accent-light);color:var(--accent)}.guide-trigger:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.page-guide{display:grid;grid-template-columns:1.05fr 1.25fr 1fr;gap:0;margin:0;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.resource-guide-popover{position:fixed;z-index:40;overflow:auto;box-shadow:0 18px 50px rgba(0,0,24,.24)}.resource-guide-popover[hidden]{display:none}.page-guide>div{padding:14px 16px;border-left:1px solid var(--line);min-width:0}.page-guide>div:first-child{border-left:0}.page-guide>div>span{display:block;color:var(--accent);text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;margin-bottom:6px}.page-guide p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.guide-links{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.guide-links a{color:var(--accent);background:var(--accent-soft);border-radius:99px;padding:4px 7px;text-decoration:none;font-size:9.6px;font-weight:700}
2542
+ .operation-tracking{display:grid;gap:2px;min-width:0;text-decoration:none}.operation-tracking strong,.operation-tracking small{display:block;overflow-wrap:anywhere}.operation-tracking small{color:var(--muted);line-height:1.35}.operation-tracking.running strong{color:#176143}.operation-tracking.waiting strong,.operation-tracking.mixed strong{color:var(--amber)}.operation-tracking.paused strong{color:var(--red)}a.operation-tracking:hover strong{text-decoration:underline}
2543
+ .page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:auto}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{padding:12px 25px 23px}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 0}.onboarding-dialog>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2544
+ .page-intro,.detail-head{align-items:center;margin-bottom:12px}.actions{align-items:center}.detail-head>div:first-child{min-width:0}.detail-head h2{margin:7px 0}.detail-head .header-breadcrumbs{margin:0;font-size:10.8px;line-height:normal;min-height:11px;align-items:center}.header-breadcrumbs span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60ch}
2221
2545
  @media(max-width:1200px){.readiness-flow{grid-template-columns:repeat(3,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr 1fr}.audit-engagement .button{grid-column:1/-1;justify-self:start}}
2222
2546
  @media(max-width:1100px){.search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
2223
- @media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:20px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{min-width:0}.topbar-status{display:none}.search{display:flex;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
2224
- @media(max-width:760px){.setup-banner,.page-guide{grid-template-columns:1fr}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:8px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}.onboarding-actions{position:sticky;bottom:0;background:var(--panel);border-top:1px solid var(--line)}}
2547
+ @media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{min-width:0}.topbar-status{display:none}.search{display:flex;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
2548
+ @media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid{grid-template-columns:1fr}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}.onboarding-actions{position:sticky;bottom:0;background:var(--panel);border-top:1px solid var(--line)}}
2225
2549
  @media(max-width:520px){.onboarding-form,.onboarding-sections,.setup-steps{grid-template-columns:1fr}.onboarding-form label.wide{grid-column:auto}.onboarding-actions{flex-wrap:wrap}.onboarding-skip{width:100%;order:3;margin:3px 0 0}.readiness-flow{grid-template-columns:1fr}.obligation-card-foot{align-items:flex-start;flex-direction:column}.obligation-action{align-self:flex-start}}
2226
2550
  @media(min-width:761px){.detail-grid{grid-template-columns:minmax(270px,1fr) minmax(0,2fr)}.detail-grid aside{grid-column:1;grid-row:1}.detail-main{grid-column:2;grid-row:1}}
2227
- @media(max-width:760px){.sidebar{visibility:hidden;transition:transform .2s,visibility 0s .2s}.sidebar.shown{visibility:visible;transition-delay:0s}.nav-close{display:grid;place-items:center;position:absolute;top:25px;right:18px;width:34px;height:34px;border:1px solid #5966a4;border-radius:50%;background:#11174a;color:#eef1ff;font-size:20px;cursor:pointer}.nav-scrim{display:block;position:fixed;inset:0;border:0;background:rgba(0,0,24,.38);opacity:0;pointer-events:none;transition:opacity .2s;z-index:15}.sidebar.shown+.nav-scrim{opacity:1;pointer-events:auto}.pagination{justify-content:space-between;gap:8px}.page-status{min-width:0}}
2551
+ @media(min-width:761px){.detail-grid.detail-grid-structured{grid-template-columns:1fr}.detail-grid-structured aside{grid-column:1;grid-row:1;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.detail-grid-structured aside>.panel{align-self:start}}
2552
+ @media(max-width:760px){.sidebar{visibility:hidden;transition:transform .2s,visibility 0s .2s}.sidebar.shown{visibility:visible;transition-delay:0s}.nav-close{display:grid;place-items:center;position:absolute;top:25px;right:18px;width:34px;height:34px;border:1px solid #5966a4;border-radius:50%;background:#11174a;color:#eef1ff;font-size:24px;cursor:pointer}.nav-scrim{display:block;position:fixed;inset:0;border:0;background:rgba(0,0,24,.38);opacity:0;pointer-events:none;transition:opacity .2s;z-index:15}.sidebar.shown+.nav-scrim{opacity:1;pointer-events:auto}.pagination{justify-content:space-between;gap:8px}.page-status{min-width:0}}
2228
2553
  @media(max-width:760px){.topbar{height:56px}.nav-close{font-size:0}.nav-close:before,.nav-close:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.nav-close:after{transform:rotate(-45deg)}}
2229
2554
 
2230
2555
  body,button,input,select,textarea,dialog{color:var(--ink)}
@@ -2286,25 +2611,27 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
2286
2611
  .record-table td[data-label]::before{color:var(--muted)}
2287
2612
  .commit-dialog{width:min(560px,calc(100vw - 30px));max-height:calc(100vh - 32px);border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:var(--shadow);overflow:auto}
2288
2613
  .commit-dialog form{padding:23px}
2289
- .commit-dialog form>p{color:var(--muted);font-size:11px;line-height:1.55}
2290
- .commit-dialog label>span{display:block;font-size:10px;font-weight:720;margin-bottom:6px}
2291
- .commit-dialog input{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:12px}
2614
+ .commit-dialog form>p{color:var(--muted);font-size:13.2px;line-height:1.55}
2615
+ .commit-dialog label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}
2616
+ .commit-dialog input{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}
2292
2617
  .commit-files{display:grid;gap:5px;max-height:160px;overflow:auto;margin-top:14px;padding:10px;background:var(--surface-soft);border-radius:7px}
2293
- .commit-files code{font-size:9px;overflow-wrap:anywhere}
2618
+ .commit-files code{font-size:10.8px;overflow-wrap:anywhere}
2294
2619
  .onboarding-progress{grid-template-columns:repeat(var(--onboarding-step-count),1fr)}
2295
- .onboarding-git-status{display:flex;align-items:flex-start;gap:9px;margin:14px 25px 0;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.onboarding-git-status .status-dot{margin-top:4px}.onboarding-git-status strong,.onboarding-git-status small{display:block}.onboarding-git-status strong{font-size:10px}.onboarding-git-status small{color:var(--muted);font-size:9px;line-height:1.45;margin-top:3px}.onboarding-git-status code{font-size:9px}
2296
- .badge.status-overdue{background:#f7dfdc;color:#873027}.badge.status-due{background:#f6e8c9;color:#79500f}.badge.status-upcoming{background:var(--accent-soft);color:var(--accent)}.badge.status-complete{background:#dcefe4;color:#125733}
2297
- .obligation-preview,.event-reminder-preview{display:grid;gap:8px}.obligation-preview a{display:flex;align-items:flex-start;gap:9px;text-decoration:none;padding:7px 0;border-top:1px solid var(--line)}.obligation-preview a:first-child{border-top:0;padding-top:0}.obligation-preview strong,.obligation-preview small,.event-reminder-preview strong,.event-reminder-preview small{display:block}.obligation-preview strong,.event-reminder-preview strong{font-size:10px}.obligation-preview small,.event-reminder-preview small{font-size:9px;color:var(--muted);margin-top:2px}.event-reminder-preview{grid-template-columns:repeat(2,minmax(0,1fr))}.event-reminder-preview a{padding:10px;border-radius:7px;background:var(--surface-soft);text-decoration:none}
2298
- .obligation-board{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;align-items:start}.obligation-column{min-width:0}.obligation-column-head{display:flex;align-items:center;justify-content:space-between;margin:5px 1px 10px}.obligation-column-head>strong{font:500 22px Georgia,serif}.obligation-cards{display:grid;gap:9px}.obligation-card{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--accent-light);border-radius:9px;padding:14px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.obligation-card.status-overdue{border-left-color:var(--red)}.obligation-card.status-due{border-left-color:#d89021}.obligation-card-head{display:flex;justify-content:space-between;gap:8px;text-transform:uppercase;letter-spacing:.06em;font-size:8px;color:var(--muted)}.obligation-card-head strong{color:var(--ink);text-align:right}.obligation-card h3{font-size:12px;margin:9px 0 7px}.obligation-card h3 a{text-decoration:none}.obligation-card p{font-size:9px;line-height:1.5;color:var(--muted);margin:0}.obligation-links{margin-top:10px}.workflow-section{margin-top:30px}.section-head{display:flex;justify-content:space-between;margin-bottom:13px}.section-head h2{font:500 24px Georgia,serif;margin:6px 0}.section-head p:not(.kicker){font-size:11px;color:var(--muted);margin:0;max-width:720px}.event-trigger-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.event-trigger-card{display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;min-width:0}.event-trigger-card h3{font-size:13px;margin:6px 0}.event-trigger-card p:not(.kicker){font-size:10px;color:var(--muted);line-height:1.5;margin:0}.event-trigger-card ol{padding-left:20px;margin:15px 0;display:grid;gap:8px}.event-trigger-card li span,.event-trigger-card li small{display:block}.event-trigger-card li span{font-size:10px}.event-trigger-card li small{font-size:8px;color:var(--muted);margin-top:2px}.event-trigger-card>.button{margin-top:auto;align-self:flex-start}.event-run-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.event-run{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;min-width:0}.event-run-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.event-run-head h3{font-size:13px;margin:7px 0 3px}.event-run-head h3 a{text-decoration:none}.event-run-head small{font-size:9px;color:var(--muted)}.event-run-head>strong{font:500 22px Georgia,serif}.event-run>.progress{margin:13px 0}.event-actions{display:grid}.event-actions a{display:flex;gap:9px;text-decoration:none;padding:9px 0;border-top:1px solid var(--line);align-items:flex-start}.event-actions strong,.event-actions small{display:block}.event-actions strong{font-size:10px}.event-actions small{font-size:8px;color:var(--muted);margin-top:2px}
2299
- .obligation-card-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:9px;margin-top:10px}.obligation-card-foot .obligation-links{margin-top:0;min-width:0}.obligation-action{flex:0 0 auto;border:0;border-radius:6px;background:var(--accent-soft);color:var(--accent);padding:7px 9px;font-family:inherit;font-size:9px;font-weight:700;line-height:1;text-decoration:none;cursor:pointer}.obligation-action:hover{filter:brightness(1.08)}.obligation-action.blocked{background:var(--surface-muted);color:var(--muted)}.obligation-more{width:100%;margin-top:9px}.workflow-section{scroll-margin-top:92px}
2300
- .event-dialog label{display:block;margin-top:13px}.event-dialog label>span{display:block;font-size:10px;font-weight:720;margin-bottom:6px}.event-dialog input,.event-dialog select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:12px}.event-dialog-steps{display:grid;gap:6px;margin-top:15px;padding:10px;background:var(--surface-soft);border-radius:7px}.event-dialog-steps strong,.event-dialog-steps small{display:block}.event-dialog-steps strong{font-size:10px}.event-dialog-steps small{font-size:8px;color:var(--muted);margin-top:2px}
2301
- .packet-builder form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end}.packet-builder label>span{display:block;font-size:9px;font-weight:720;margin-bottom:6px}.packet-builder input,.packet-builder select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:11px}.packet-note,.packet-output>p{font-size:10px;color:var(--muted);margin:12px 0 0}.packet-output{margin:14px 0}.packet-output h3{overflow-wrap:anywhere}.packet-gaps{display:grid}.packet-gaps>div{display:grid;grid-template-columns:58px 1fr;gap:10px;border-top:1px solid var(--line);padding:10px 0}.packet-gaps>div:first-child{border-top:0}.packet-gaps p{font-size:10px;margin:0}.packet-list{display:grid}.packet-list a{display:block;text-decoration:none;border-top:1px solid var(--line);padding:9px 0}.packet-list a:first-child{border-top:0}.packet-list strong,.packet-list small{display:block}.packet-list strong{font-size:10px}.packet-list small{font-size:8px;color:var(--muted);margin-top:2px}
2302
- .packet-preflight{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:12px}.packet-preflight a{display:flex;align-items:flex-start;gap:9px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);text-decoration:none}.packet-preflight .status-dot{margin-top:4px}.packet-preflight small,.packet-preflight strong{display:block}.packet-preflight small{color:var(--muted);font-size:8px;text-transform:uppercase;letter-spacing:.07em}.packet-preflight strong{margin-top:3px;font-size:10px}
2303
- .audit-preparation{margin-bottom:12px}.audit-preparation .panel-head{align-items:flex-start}.audit-preparation .panel-head h3{margin:3px 0}.audit-preparation .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:9px}.preparation-progress{height:5px;margin:12px 0 0;border-radius:99px;background:var(--surface-muted);overflow:hidden}.preparation-progress span{display:block;height:100%;border-radius:inherit;background:var(--primary-gradient)}.audit-preparation-note{margin:9px 0 0;color:var(--muted);font-size:9px;line-height:1.5}.preparation-stages{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.preparation-stage{border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);overflow:hidden}.preparation-stage summary{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:11px 12px;cursor:pointer;list-style:none}.preparation-stage summary::-webkit-details-marker{display:none}.preparation-stage summary span,.preparation-stage summary strong,.preparation-stage summary small{display:block}.preparation-stage summary strong{font-size:10px}.preparation-stage summary small{margin-top:3px;color:var(--muted);font-size:8px;line-height:1.4}.preparation-stage summary b{flex:none;color:var(--muted);font-size:8px;font-weight:650}.preparation-items{border-top:1px solid var(--line);background:var(--panel)}.preparation-items>a,.preparation-items>div{display:grid;grid-template-columns:22px minmax(0,1fr);gap:9px;padding:10px 12px;border-top:1px solid var(--line);text-decoration:none}.preparation-items>:first-child{border-top:0}.preparation-items strong,.preparation-items small{display:block}.preparation-items strong{font-size:9px}.preparation-items small{margin-top:3px;color:var(--muted);font-size:8px;line-height:1.45}.preparation-status{display:grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--surface-muted);color:var(--muted);font-size:9px;font-weight:800}.preparation-status.complete{background:#dcefe4;color:#125733}.preparation-status.action{background:#f7dfdc;color:#873027}.preparation-status.later{background:#f6e8c9;color:#79500f}.preparation-status.external,.preparation-status.info{background:var(--accent-soft);color:var(--accent)}.audit-preparation-error:empty{display:none}
2304
- @media(max-width:900px){.obligation-board,.event-trigger-grid{grid-template-columns:1fr}.event-run-list{grid-template-columns:1fr}.packet-builder form,.packet-preflight{grid-template-columns:1fr 1fr}.packet-builder .button{align-self:end}}
2620
+ .onboarding-git-status{display:flex;align-items:flex-start;gap:9px;margin:14px 25px 0;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.onboarding-git-status .status-dot{margin-top:4px}.onboarding-git-status strong,.onboarding-git-status small{display:block}.onboarding-git-status strong{font-size:12px}.onboarding-git-status small{color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:3px}.onboarding-git-status code{font-size:10.8px}
2621
+ .badge.status-overdue{background:#f7dfdc;color:#873027}.badge.status-due{background:#f6e8c9;color:#79500f}.badge.status-upcoming,.badge.status-proposed{background:var(--accent-soft);color:var(--accent)}.badge.status-complete{background:#dcefe4;color:#125733}
2622
+ .obligation-preview,.event-reminder-preview{display:grid;gap:8px}.obligation-preview a{display:flex;align-items:flex-start;gap:9px;text-decoration:none;padding:7px 0;border-top:1px solid var(--line)}.obligation-preview a:first-child{border-top:0;padding-top:0}.obligation-preview strong,.obligation-preview small,.event-reminder-preview strong,.event-reminder-preview small{display:block}.obligation-preview strong,.event-reminder-preview strong{font-size:12px}.obligation-preview small,.event-reminder-preview small{font-size:10.8px;color:var(--muted);margin-top:2px}.event-reminder-preview{grid-template-columns:repeat(2,minmax(0,1fr))}.event-reminder-preview a{padding:10px;border-radius:7px;background:var(--surface-soft);text-decoration:none}
2623
+ .obligation-board{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;align-items:start}.obligation-column{min-width:0}.obligation-column-head{display:flex;align-items:center;justify-content:space-between;margin:5px 1px 10px}.obligation-column-head>strong{font:500 26.4px Georgia,serif}.obligation-cards{display:grid;gap:9px}.obligation-card{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--accent-light);border-radius:9px;padding:14px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.obligation-card.status-overdue{border-left-color:var(--red)}.obligation-card.status-due{border-left-color:#d89021}.obligation-card-head{display:flex;justify-content:space-between;gap:8px;text-transform:uppercase;letter-spacing:.06em;font-size:9.6px;color:var(--muted)}.obligation-card-head strong{color:var(--ink);text-align:right}.obligation-card h3{font-size:14.4px;margin:9px 0 7px}.obligation-card h3 a{text-decoration:none}.obligation-card p{font-size:10.8px;line-height:1.5;color:var(--muted);margin:0}.obligation-links{margin-top:10px}.workflow-section{margin-top:30px}.section-head{display:flex;justify-content:space-between;margin-bottom:13px}.section-head h2{font:500 28.8px Georgia,serif;margin:6px 0}.section-head p:not(.kicker){font-size:13.2px;color:var(--muted);margin:0;max-width:720px}.policy-event-feedback{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:10px;align-items:center;margin-top:14px;padding:12px 14px;border:1px solid #9ccfb2;border-radius:8px;background:#e7f5ec}.policy-event-feedback .status-dot{align-self:start;margin-top:4px}.policy-event-feedback strong,.policy-event-feedback p{display:block}.policy-event-feedback strong{font-size:12px}.policy-event-feedback p{margin:3px 0 0;color:#315d44;font-size:10.8px}.policy-event-feedback .icon-button{width:30px;height:30px}.policy-event-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.policy-event-row{position:relative;display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0;padding:9px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.policy-event-name{min-width:0}.policy-event-title{display:flex;align-items:center;gap:6px;min-width:0}.policy-event-title>strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.policy-event-guide{display:inline-flex;flex:0 0 auto}.policy-event-guide .guide-trigger{width:20px;height:20px}.policy-event-guide .guide-trigger svg{width:14px;height:14px}.policy-event-name strong,.policy-event-name>small{display:block}.policy-event-name strong{font-size:12px}.policy-event-name>small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.policy-event-row>.button{flex:none;padding:7px 9px;font-size:10.8px}.policy-event-tooltip{position:absolute;z-index:8;top:calc(100% + 7px);left:0;width:min(420px,calc(100vw - 48px));padding:12px 14px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow);opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s,transform .12s,visibility 0s .12s;pointer-events:none}.policy-event-row:nth-child(3n) .policy-event-tooltip{right:0;left:auto}.policy-event-row:hover,.policy-event-row:focus-within{z-index:9}.policy-event-guide:hover .policy-event-tooltip,.policy-event-guide:focus-within .policy-event-tooltip{opacity:1;visibility:visible;transform:none;transition-delay:0s}.policy-event-tooltip>strong{font-size:12px}.policy-event-tooltip ol{display:grid;gap:7px;margin:9px 0 0;padding-left:20px}.policy-event-tooltip li span,.policy-event-tooltip li small{display:block}.policy-event-tooltip li span{font-size:10.8px}.policy-event-tooltip li small{margin-top:2px;color:var(--muted);font-size:9.6px;line-height:1.4}
2624
+ .obligation-card-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:9px;margin-top:10px}.obligation-card-foot .obligation-links{margin-top:0;min-width:0}.obligation-action{flex:0 0 auto;border:0;border-radius:6px;background:var(--accent-soft);color:var(--accent);padding:7px 9px;font-family:inherit;font-size:10.8px;font-weight:700;line-height:1;text-decoration:none;cursor:pointer}.obligation-action:hover{filter:brightness(1.08)}.obligation-action.blocked{background:var(--surface-muted);color:var(--muted)}.obligation-more{width:100%;margin-top:9px}.workflow-section{scroll-margin-top:92px}
2625
+ .event-dialog label{display:block;margin-top:13px}.event-dialog label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.event-dialog input,.event-dialog select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.event-dialog-steps{display:grid;gap:6px;margin-top:15px;padding:10px;background:var(--surface-soft);border-radius:7px}.event-dialog-steps strong,.event-dialog-steps small{display:block}.event-dialog-steps strong{font-size:12px}.event-dialog-steps small{font-size:9.6px;color:var(--muted);margin-top:2px}
2626
+ .packet-builder form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end}.packet-builder label>span{display:block;font-size:10.8px;font-weight:720;margin-bottom:6px}.packet-builder input,.packet-builder select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:13.2px}.packet-note,.packet-output>p{font-size:12px;color:var(--muted);margin:12px 0 0}.packet-output{margin:14px 0}.packet-output h3{overflow-wrap:anywhere}.packet-gaps{display:grid}.packet-gaps>div{display:grid;grid-template-columns:58px 1fr;gap:10px;border-top:1px solid var(--line);padding:10px 0}.packet-gaps>div:first-child{border-top:0}.packet-gaps p{font-size:12px;margin:0}.packet-list{display:grid}.packet-list a{display:block;text-decoration:none;border-top:1px solid var(--line);padding:9px 0}.packet-list a:first-child{border-top:0}.packet-list strong,.packet-list small{display:block}.packet-list strong{font-size:12px}.packet-list small{font-size:9.6px;color:var(--muted);margin-top:2px}
2627
+ .packet-preflight{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-bottom:12px}.packet-preflight a{display:flex;align-items:flex-start;gap:9px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);text-decoration:none}.packet-preflight .status-dot{margin-top:4px}.packet-preflight small,.packet-preflight strong{display:block}.packet-preflight small{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.07em}.packet-preflight strong{margin-top:3px;font-size:12px}.audit-evidence-paths{margin-bottom:12px}.audit-evidence-paths .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.audit-evidence-path-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.audit-evidence-path-grid>a{display:block;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.audit-evidence-path-grid h4{margin:7px 0 5px;font-size:13.2px}.audit-evidence-path-grid p{margin:0;color:var(--muted);font-size:10.8px;line-height:1.55}
2628
+ .audit-preparation{margin-bottom:12px}.audit-preparation .panel-head{align-items:flex-start}.audit-preparation .panel-head h3{margin:3px 0}.audit-preparation .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.preparation-progress{height:5px;margin:12px 0 0;border-radius:99px;background:var(--surface-muted);overflow:hidden}.preparation-progress span{display:block;height:100%;border-radius:inherit;background:var(--primary-gradient)}.audit-preparation-note{margin:9px 0 0;color:var(--muted);font-size:10.8px;line-height:1.5}.preparation-stages{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.preparation-stage{border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);overflow:hidden}.preparation-stage summary{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:11px 12px;cursor:pointer;list-style:none}.preparation-stage summary::-webkit-details-marker{display:none}.preparation-stage summary span,.preparation-stage summary strong,.preparation-stage summary small{display:block}.preparation-stage summary strong{font-size:12px}.preparation-stage summary small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.preparation-stage summary b{flex:none;color:var(--muted);font-size:9.6px;font-weight:650}.preparation-items{border-top:1px solid var(--line);background:var(--panel)}.preparation-items>a,.preparation-items>div{display:grid;grid-template-columns:22px minmax(0,1fr);gap:9px;padding:10px 12px;border-top:1px solid var(--line);text-decoration:none}.preparation-items>:first-child{border-top:0}.preparation-items strong,.preparation-items small{display:block}.preparation-items strong{font-size:10.8px}.preparation-items small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.45}.preparation-status{display:grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--surface-muted);color:var(--muted);font-size:10.8px;font-weight:800}.preparation-status.complete{background:#dcefe4;color:#125733}.preparation-status.action{background:#f7dfdc;color:#873027}.preparation-status.later{background:#f6e8c9;color:#79500f}.preparation-status.external,.preparation-status.info{background:var(--accent-soft);color:var(--accent)}.audit-preparation-error:empty{display:none}
2629
+ @media(max-width:900px){.obligation-board{grid-template-columns:1fr}.policy-event-list{grid-template-columns:repeat(2,minmax(0,1fr))}.policy-event-row:nth-child(3n) .policy-event-tooltip{right:auto;left:0}.policy-event-row:nth-child(2n) .policy-event-tooltip{right:0;left:auto}.packet-builder form,.packet-preflight{grid-template-columns:1fr 1fr}.packet-builder .button{align-self:end}}
2630
+ @media(max-width:1000px){.stage-overview-layout{grid-template-columns:1fr}.stage-page-grid,.group-destination-grid{grid-template-columns:1fr}}
2305
2631
  @media(max-width:760px){.preparation-stages{grid-template-columns:1fr}}
2306
2632
  @media(max-width:900px){.overview-grid{grid-template-columns:1fr}.overview-grid>.audit-panel{grid-column:auto}}
2307
- @media(max-width:520px){.event-reminder-preview,.packet-builder form,.packet-preflight{grid-template-columns:1fr}.packet-metrics{grid-template-columns:1fr}.obligation-card-head{display:block}.obligation-card-head strong{display:block;text-align:left;margin-top:3px}}
2633
+ @media(max-width:520px){.event-reminder-preview,.policy-event-list,.packet-builder form,.packet-preflight,.audit-evidence-path-grid{grid-template-columns:1fr}.policy-event-row:nth-child(n) .policy-event-tooltip{right:auto;left:0}.packet-metrics{grid-template-columns:1fr}.obligation-card-head{display:block}.obligation-card-head strong{display:block;text-align:left;margin-top:3px}}
2634
+ @media(max-width:520px){.policy-event-feedback{grid-template-columns:auto minmax(0,1fr) auto}.policy-event-feedback>.button{grid-column:2}.policy-event-feedback>.icon-button{grid-column:3;grid-row:1}}
2308
2635
 
2309
2636
  @media(prefers-color-scheme:dark){
2310
2637
  :root{--ink:#f4f5ff;--muted:#b8bfd3;--line:#343d5c;--paper:#000;--panel:#141a2e;--accent:#aab7ff;--accent-soft:#252e52;--accent-light:#9aabff;--focus:#bdc7ff;--amber:#ffd08a;--red:#ffaaa0;--surface-soft:#1b2238;--surface-muted:#252d48;--field:#11172a;--field-readonly:#1c2338;--shadow:0 12px 34px rgba(0,0,0,.3)}
@@ -2312,12 +2639,14 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
2312
2639
  .badge.good{background:#173b2b;color:#a8edc4}
2313
2640
  .badge.warn{background:#483714;color:#ffd991}
2314
2641
  .badge.bad{background:#4a252a;color:#ffb5ad}
2642
+ .badge.neutral{background:#252d48;color:#c8cff0}
2315
2643
  .badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#173b2b;color:#a8edc4}
2316
2644
  .badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#4a252a;color:#ffb5ad}
2317
2645
  .badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#483714;color:#ffd991}
2318
2646
  .badge.status-overdue{background:#4a252a;color:#ffb5ad}
2319
2647
  .badge.status-due{background:#483714;color:#ffd991}
2320
2648
  .badge.status-complete{background:#173b2b;color:#a8edc4}
2649
+ .policy-event-feedback{border-color:#315f48;background:#183426}.policy-event-feedback p{color:#b7d8c3}
2321
2650
  .missing-options{border-color:#77612f;background:#382f19;color:#ffdc92}
2322
2651
  .status-dot.neutral{background:#9aabff}
2323
2652
  }