filegrc 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.js CHANGED
@@ -5,9 +5,17 @@ import { getResourceDefinition } from "../model/index.js";
5
5
  import { prepareAuditWorkspace } from "./audit-preparation.js";
6
6
  import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
7
7
  import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
8
- import { FAVICON_PNG } from "./favicon.js";
8
+ import { FAVICON_PNG, LOGO_MARK_PNG } from "./favicon.js";
9
9
  import { createResource, deleteResource, updateContent, updateResource } from "./files.js";
10
- import { commitAndPushWorkspace, getFileHistory, pullWorkspace, pushWorkspace } from "./git.js";
10
+ import {
11
+ commitAndPushWorkspace,
12
+ getBrowserRepositoryState,
13
+ getFileHistory,
14
+ pullWorkspace,
15
+ pushWorkspace,
16
+ retryBrowserSync,
17
+ runBrowserMutation
18
+ } from "./git.js";
11
19
  import { completeObligationOccurrence, createObligationEvent, planObligations } from "./obligations.js";
12
20
  import { isWithin, relativeToWorkspace, resolveWorkspacePath } from "./paths.js";
13
21
  import { createAppState } from "./state.js";
@@ -26,7 +34,9 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
26
34
  return json(response, 403, { error: "Cross-origin writes are not allowed." });
27
35
  }
28
36
  if (request.method === "GET" && url.pathname === "/api/state") {
29
- return json(response, 200, await createAppState(input));
37
+ return json(response, 200, await createAppState(input, {
38
+ allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
39
+ }));
30
40
  }
31
41
  if (request.method === "GET" && url.pathname === "/api/history") {
32
42
  const path = url.searchParams.get("path");
@@ -44,17 +54,22 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
44
54
  }));
45
55
  }
46
56
  if (request.method === "POST" && url.pathname === "/api/obligation-events") {
47
- return json(response, 201, await createObligationEvent(input, await readJson(request)));
57
+ const payload = await readJson(request);
58
+ return json(response, 201, await browserMutation(input, options, {
59
+ message: (result) => `Create policy event: ${result.event?.title || payload.title || payload.eventType}`
60
+ }, () => createObligationEvent(input, payload)));
48
61
  }
49
62
  if (request.method === "POST" && url.pathname === "/api/obligation-completions") {
50
63
  const payload = await readJson(request);
51
64
  if (!safeSegment(payload.obligationId)) return json(response, 400, { error: "A safe obligation ID is required." });
52
- const result = await completeObligationOccurrence(input, {
65
+ const result = await browserMutation(input, options, {
66
+ message: () => `Complete ${resourceTypeLabel(payload.record?.type)}: ${payload.record?.title || payload.obligationId}`
67
+ }, () => completeObligationOccurrence(input, {
53
68
  obligationId: payload.obligationId,
54
69
  record: payload.record,
55
70
  content: payload.content,
56
71
  expectedRevision: payload.revision
57
- });
72
+ }));
58
73
  return json(response, 201, result);
59
74
  }
60
75
  if (request.method === "GET" && url.pathname === "/api/evidence-packet") {
@@ -82,34 +97,54 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
82
97
  });
83
98
  }
84
99
  if (request.method === "POST" && url.pathname === "/api/audit-preparation") {
85
- return json(response, 201, await prepareAuditWorkspace(input, await readJson(request)));
100
+ const payload = await readJson(request);
101
+ return json(response, 201, await browserMutation(input, options, {
102
+ message: () => `Prepare audit: ${payload.auditId || "engagement"}`
103
+ }, () => prepareAuditWorkspace(input, payload)));
86
104
  }
87
105
  if (request.method === "POST" && url.pathname === "/api/evidence-test-drafts") {
88
- return json(response, 201, await ensureEvidenceTestDrafts(input));
106
+ return json(response, 201, await browserMutation(input, options, {
107
+ message: "Create evidence collection test drafts"
108
+ }, () => ensureEvidenceTestDrafts(input)));
89
109
  }
90
110
  if (request.method === "POST" && url.pathname === "/api/setup") {
91
- return json(response, 200, await setupWorkspace(input, await readJson(request)));
111
+ const payload = await readJson(request);
112
+ return json(response, 200, await browserMutation(input, options, {
113
+ message: (result) => `${payload.draft === true ? "Save onboarding draft" : "Complete onboarding"} for ${result.workspace.organizationName}`
114
+ }, () => setupWorkspace(input, payload)));
92
115
  }
93
116
  if (request.method === "POST" && url.pathname === "/api/resources") {
94
117
  const payload = await readJson(request);
95
118
  const record = payload.record ?? payload;
96
- const result = await createResource(input, record, { content: payload.record ? payload.content : undefined });
97
- return json(response, 201, { record: result.record });
119
+ const result = await browserMutation(input, options, {
120
+ message: () => `Create ${resourceTypeLabel(record.type)}: ${record.title || record.id}`
121
+ }, () => createResource(input, record, { content: payload.record ? payload.content : undefined }));
122
+ return json(response, 201, { record: result.record, synchronization: result.synchronization });
98
123
  }
99
124
  if (request.method === "POST" && url.pathname === "/api/commit") {
125
+ await requireManualBrowserGit(input, options);
100
126
  const payload = await readJson(request);
101
127
  return json(response, 201, await commitAndPushWorkspace(input, payload.message));
102
128
  }
103
129
  if (request.method === "POST" && url.pathname === "/api/git/pull") {
130
+ await requireManualBrowserGit(input, options);
104
131
  return json(response, 200, await pullWorkspace(input));
105
132
  }
106
133
  if (request.method === "POST" && url.pathname === "/api/git/push") {
134
+ await requireManualBrowserGit(input, options);
107
135
  return json(response, 200, await pushWorkspace(input));
108
136
  }
137
+ if (request.method === "POST" && url.pathname === "/api/git/retry-sync") {
138
+ return json(response, 200, await retryBrowserSync(input, {
139
+ allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
140
+ }));
141
+ }
109
142
  if (request.method === "PUT" && url.pathname === "/api/content") {
110
143
  const payload = await readJson(request);
111
- const result = await updateContent(input, payload.path, payload.source, { expectedRevision: payload.revision });
112
- return json(response, 200, { path: result.dataRelativePath });
144
+ const result = await browserMutation(input, options, {
145
+ message: () => `Update content: ${payload.path}`
146
+ }, () => updateContent(input, payload.path, payload.source, { expectedRevision: payload.revision }));
147
+ return json(response, 200, { path: result.dataRelativePath, synchronization: result.synchronization });
113
148
  }
114
149
  const match = /^\/api\/resource\/([^/]+)\/([^/]+)$/.exec(url.pathname);
115
150
  if (match) {
@@ -124,19 +159,30 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
124
159
  if (request.method === "PUT") {
125
160
  const payload = await readJson(request);
126
161
  const record = payload.record ?? payload;
127
- const result = await updateResource(input, type, id, record, {
162
+ const result = await browserMutation(input, options, {
163
+ message: () => `Update ${resourceTypeLabel(type)}: ${record.title || id}`
164
+ }, () => updateResource(input, type, id, record, {
128
165
  content: payload.record ? payload.content : undefined,
129
166
  expectedRevision: payload.revision,
130
167
  expectedContentRevisions: payload.contentRevisions
131
- });
132
- return json(response, 200, { record: result.record });
168
+ }));
169
+ return json(response, 200, { record: result.record, synchronization: result.synchronization });
133
170
  }
134
171
  if (request.method === "DELETE") {
135
- const result = await deleteResource(input, type, id, { expectedRevision: url.searchParams.get("revision") });
136
- return json(response, 200, { deleted: true, type, id, deletedContent: result.deletedContent });
172
+ const result = await browserMutation(input, options, {
173
+ message: () => `Delete ${resourceTypeLabel(type)}: ${id}`
174
+ }, () => deleteResource(input, type, id, { expectedRevision: url.searchParams.get("revision") }));
175
+ return json(response, 200, {
176
+ deleted: true,
177
+ type,
178
+ id,
179
+ deletedContent: result.deletedContent,
180
+ synchronization: result.synchronization
181
+ });
137
182
  }
138
183
  }
139
184
  if (request.method === "GET" && url.pathname === "/favicon.png") return text(response, 200, FAVICON_PNG, "image/png");
185
+ if (request.method === "GET" && url.pathname === "/logo-mark-white.png") return text(response, 200, LOGO_MARK_PNG, "image/png");
140
186
  if (request.method === "GET" && url.pathname === "/filegrc-app.js") return text(response, 200, APP_SCRIPT, "text/javascript; charset=utf-8");
141
187
  if (request.method === "GET" && url.pathname === "/filegrc.css") return text(response, 200, APP_STYLES, "text/css; charset=utf-8");
142
188
  if (request.method === "GET" && url.pathname.startsWith("/packet/")) {
@@ -184,7 +230,10 @@ export async function serveWorkspace(input = process.cwd(), options = {}) {
184
230
  }
185
231
  const loaded = await loadWorkspace(input);
186
232
  getResourceDefinition(loaded.model, "workspace");
187
- const server = createFilegrcServer(loaded.root, { allowedHosts: [host] });
233
+ const server = createFilegrcServer(loaded.root, {
234
+ allowedHosts: [host],
235
+ allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites === true
236
+ });
188
237
  await new Promise((resolve, reject) => {
189
238
  server.once("error", reject);
190
239
  server.listen(port, host, resolve);
@@ -197,6 +246,26 @@ export async function serveWorkspace(input = process.cwd(), options = {}) {
197
246
  };
198
247
  }
199
248
 
249
+ function browserMutation(input, options, mutationOptions, task) {
250
+ return runBrowserMutation(input, {
251
+ ...mutationOptions,
252
+ allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites === true
253
+ }, task);
254
+ }
255
+
256
+ async function requireManualBrowserGit(input, options) {
257
+ const repository = await getBrowserRepositoryState(input, {
258
+ allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
259
+ });
260
+ if (repository.mode === "trunk") {
261
+ throw new Error("Browser commit, pull, and push controls are disabled in trunk mode. Saved changes synchronize automatically.");
262
+ }
263
+ }
264
+
265
+ function resourceTypeLabel(value) {
266
+ return String(value || "record").replaceAll("-", " ");
267
+ }
268
+
200
269
  async function readJson(request) {
201
270
  const chunks = [];
202
271
  let size = 0;
@@ -295,7 +364,7 @@ function statusFor(error) {
295
364
  if (/exceeds 2 MB/i.test(error.message)) return 413;
296
365
  if (/changed after you opened|source changed|revision changed/i.test(error.message)) return 409;
297
366
  if (/already exists|target file already exists/i.test(error.message)) return 409;
298
- if (/Git could not (?:pull|push)|upstream branch|multiple remotes|no Git remote|check out a branch|before trying to (?:pull|push)/i.test(error.message)) return 409;
367
+ if (/Git could not|upstream branch|multiple remotes|no Git remote|configured repository remote|safe Git name|check out a branch|before trying to (?:pull|push)|authoritative branch|not synchronized|not synced|diverged|waiting to be pushed|Retry sync|outside this FileGRC workspace|worktree has uncommitted changes|development write override|browser commit, pull, and push/i.test(error.message)) return 409;
299
368
  if (/not found|ENOENT/i.test(error.message)) return 404;
300
369
  if (/invalid|required|unsafe|match|workspace|singleton|commit message|no changes|git history|git user|unknown resource type|must use|must be|content path|data path|path leaves|valid .*date|not found|no active obligations|end date|through date|already exists|EEXIST/i.test(error.message)) return 400;
301
370
  return 500;
package/src/startup.js ADDED
@@ -0,0 +1,5 @@
1
+ const GITHUB_STAR_MESSAGE = "\n\x1b[38;2;255;184;0m⭐️ → ❤️ https://github.com/Sunpeak-AI/filegrc\x1b[0m\n";
2
+
3
+ export function printGithubStarMessage() {
4
+ console.log(GITHUB_STAR_MESSAGE);
5
+ }
package/src/state.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { assessAuditPreparation } from "./audit-preparation.js";
4
- import { getGitSummary, getWorkspaceHistories } from "./git.js";
4
+ import { getBrowserRepositoryState, getGitSummary, getWorkspaceHistories } from "./git.js";
5
5
  import { renderMarkdown } from "./markdown.js";
6
6
  import { planObligations } from "./obligations.js";
7
7
  import { resolveDataPath } from "./paths.js";
@@ -42,6 +42,10 @@ export async function createAppState(input = process.cwd(), options = {}) {
42
42
 
43
43
  const git = getGitSummary(loaded.root);
44
44
  delete git.root;
45
+ const repository = await getBrowserRepositoryState(loaded.root, {
46
+ readOnly: options.readOnly,
47
+ allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
48
+ });
45
49
  const workspace = loaded.workspace ?? {
46
50
  schemaVersion: 1,
47
51
  dataModelVersion: loaded.model.modelVersion,
@@ -70,7 +74,8 @@ export async function createAppState(input = process.cwd(), options = {}) {
70
74
  ));
71
75
  return {
72
76
  generatedAt,
73
- readOnly: Boolean(options.readOnly),
77
+ readOnly: Boolean(options.readOnly || (repository.mode === "trunk" && !repository.writesAllowed)),
78
+ repository,
74
79
  workspace,
75
80
  model: loaded.model,
76
81
  resources: entries,
package/src/validate.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { getResourceDefinition } from "../model/index.js";
3
+ import { isSafeGitName } from "./git-name.js";
3
4
  import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
4
5
  import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
5
6
  import { obligationIsRunning } from "./program-lifecycle.js";
@@ -476,6 +477,7 @@ function validateValue(name, value, field, model, path, diagnostics) {
476
477
  }
477
478
  if (field.format === "email" && !EMAIL_PATTERN.test(value)) fail("must be an email address.");
478
479
  if (field.format === "timezone" && !isTimezone(value)) fail("must be an IANA time zone.");
480
+ if (field.format === "git-name" && !isSafeGitName(value)) fail("must be a safe Git name.");
479
481
  }
480
482
 
481
483
  function validateNumericRange(value, field, fail) {
package/src/web.js CHANGED
@@ -174,7 +174,7 @@ function buildNavigation(route) {
174
174
  const organizationCurrent = route.name === "organization" || route.name === "repository" || ["workspace", "renderer-settings"].includes(route.type);
175
175
  const organizationName = state.workspace.organizationName || "Organization";
176
176
  const initial = organizationName.trim().charAt(0).toUpperCase() || "O";
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>';
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="./logo-mark-white.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>';
178
178
  }
179
179
 
180
180
  function readinessStageForRoute(route) {
@@ -217,7 +217,19 @@ function topbar(route) {
217
217
  : route.name === "audit-packet"
218
218
  ? "Audit Readiness"
219
219
  : state.model.resources[route.type]?.pluralTitle || "filegrc";
220
- return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><label class="search"><span aria-hidden="true">⌕</span><input id="global-search" type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><div class="topbar-status"><a class="validation-chip" href="#/repository"><span class="status-dot ' + (state.validation.ok ? "good" : "bad") + '"></span>' + (state.validation.ok ? "Data valid" : state.validation.counts.errors + " validation errors") + '</a><a class="repo-chip" href="#/repository"><span class="status-dot ' + (state.git.clean ? "good" : "warn") + '"></span>' + esc(state.git.available ? ((state.git.branch || "detached") + " · " + state.git.shortCommit) : "Git unavailable") + '</a></div>';
220
+ const repositoryLabel = state.repository?.mode === "trunk"
221
+ ? state.repository.label
222
+ : state.git.available ? ((state.git.branch || "detached") + " · " + state.git.shortCommit) : "Git unavailable";
223
+ const repositoryTone = state.repository?.mode === "trunk"
224
+ ? repositoryStatusTone(state.repository.status)
225
+ : state.git.clean ? "good" : "warn";
226
+ return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><label class="search"><span aria-hidden="true">⌕</span><input id="global-search" type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><div class="topbar-status"><a class="validation-chip" href="#/repository"><span class="status-dot ' + (state.validation.ok ? "good" : "bad") + '"></span>' + (state.validation.ok ? "Data valid" : state.validation.counts.errors + " validation errors") + '</a><a class="repo-chip" href="#/repository"><span class="status-dot ' + repositoryTone + '"></span>' + esc(repositoryLabel) + '</a></div>';
227
+ }
228
+
229
+ function repositoryStatusTone(status) {
230
+ if (status === "synced") return "good";
231
+ if (status === "syncing") return "neutral";
232
+ return "warn";
221
233
  }
222
234
 
223
235
  function renderHome(main) {
@@ -1132,6 +1144,7 @@ function renderOrganization(main) {
1132
1144
  }
1133
1145
 
1134
1146
  function renderRepository(main) {
1147
+ if (state.repository?.mode === "trunk") return renderTrunkRepository(main);
1135
1148
  const settings = rendererSettingsEntry();
1136
1149
  const settingsLink = settings ? '<a class="button" href="#/resource/renderer-settings/' + encodeURIComponent(settings.record.id) + '">Renderer settings</a>' : "";
1137
1150
  const onboardingButton = settings && !state.readOnly ? '<button class="button" type="button" id="start-onboarding">Run onboarding</button>' : "";
@@ -1177,12 +1190,41 @@ function renderRepository(main) {
1177
1190
  main.querySelector("#start-onboarding")?.addEventListener("click", requestOnboarding);
1178
1191
  }
1179
1192
 
1193
+ function renderTrunkRepository(main) {
1194
+ const repository = state.repository;
1195
+ const settings = rendererSettingsEntry();
1196
+ const settingsLink = settings ? '<a class="button" href="#/resource/renderer-settings/' + encodeURIComponent(settings.record.id) + '">Renderer settings</a>' : "";
1197
+ const onboardingButton = settings && repository.writesAllowed
1198
+ ? '<button class="button" type="button" id="start-onboarding">Run onboarding</button>'
1199
+ : "";
1200
+ const retryButton = repository.retrySafe
1201
+ ? '<button class="button primary" type="button" data-git-action="retry-sync">Retry sync</button>'
1202
+ : "";
1203
+ const pending = repository.pendingCommitsFilegrcOnly === false
1204
+ ? empty("Ahead commits include files outside this FileGRC workspace. Reconcile them with Git.")
1205
+ : repository.pendingCommits?.length
1206
+ ? '<ul class="changes">' + repository.pendingCommits.map((commit) => '<li><code>' + esc(commit.shortCommit) + '</code> ' + esc(commit.subject) + '</li>').join("") + '</ul>'
1207
+ : empty("No FileGRC commits are waiting to be pushed.");
1208
+ const lastSync = repository.lastSuccessfulSynchronization
1209
+ ? formatLocalDateTime(repository.lastSuccessfulSynchronization)
1210
+ : "No successful sync recorded by this server";
1211
+ const override = repository.developmentOverride
1212
+ ? '<div class="repository-override"><span class="status-dot warn"></span><div><strong>Development write override active</strong><p>Browser writes stay local. FileGRC will not fetch, commit, or push while this server uses <code>--allow-non-authoritative-writes</code>.</p></div></div>'
1213
+ : "";
1214
+ const validationBody = state.validation.diagnostics.length
1215
+ ? '<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>'
1216
+ : empty("No validation problems.");
1217
+ main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>Browser saves use one authoritative branch. Record status represents draft, proposal, approval, and retirement; Git branches do not.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + retryButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div>' + override + '<section class="panel repository-state-banner"><span class="status-dot ' + repositoryStatusTone(repository.status) + '"></span><div><p class="kicker">Repository status</p><h3>' + esc(repository.label) + '</h3><p>' + esc(repository.message) + '</p></div></section><div class="dashboard-grid"><section class="panel"><div class="panel-head"><h3>Configured Repository</h3></div><dl class="metadata"><div><dt>Branch</dt><dd>' + esc(repository.authoritativeBranch) + '</dd></div><div><dt>Remote</dt><dd>' + esc(repository.remote) + '</dd></div><div><dt>Checkout</dt><dd>' + esc(state.git.branch || (state.git.available ? "Detached HEAD" : "Unavailable")) + '</dd></div><div><dt>Upstream</dt><dd>' + esc(repository.upstream || "Not configured") + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Synchronization</h3></div><dl class="metadata"><div><dt>Current commit</dt><dd><code>' + esc(repository.currentCommit || "Unavailable") + '</code></dd></div><div><dt>Upstream commit</dt><dd><code>' + esc(repository.upstreamCommit || "Unavailable") + '</code></dd></div><div><dt>Ahead</dt><dd>' + esc(repository.ahead ?? "Unknown") + '</dd></div><div><dt>Behind</dt><dd>' + esc(repository.behind ?? "Unknown") + '</dd></div><div><dt>Last sync</dt><dd>' + esc(lastSync) + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Safety Checks</h3></div><dl class="metadata"><div><dt>Whole worktree</dt><dd>' + (repository.wholeWorktreeClean === null ? "Unavailable" : repository.wholeWorktreeClean ? "Clean" : "Has changes") + '</dd></div><div><dt>Git operation</dt><dd>' + esc(repository.operationInProgress || "None") + '</dd></div><div><dt>Pending scope</dt><dd>' + (repository.pendingCommitsFilegrcOnly === false ? "Includes external files" : repository.pendingCommits?.length ? "FileGRC only" : "None") + '</dd></div></dl></section><section class="panel span-2"><div class="panel-head"><h3>Pending FileGRC-only Commits</h3></div>' + pending + '</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>';
1218
+ main.querySelectorAll("[data-git-action]").forEach((button) => button.addEventListener("click", () => runRepositoryGitAction(button.dataset.gitAction)));
1219
+ main.querySelector("#start-onboarding")?.addEventListener("click", requestOnboarding);
1220
+ }
1221
+
1180
1222
  async function runRepositoryGitAction(action) {
1181
1223
  const buttons = [...document.querySelectorAll("[data-git-action]")];
1182
1224
  const disabled = buttons.map((button) => button.disabled);
1183
1225
  const active = buttons.find((button) => button.dataset.gitAction === action);
1184
1226
  const status = document.querySelector(".repository-sync-status");
1185
- const label = action === "pull" ? "Pulling…" : "Pushing…";
1227
+ const label = action === "pull" ? "Pulling…" : action === "retry-sync" ? "Syncing…" : "Pushing…";
1186
1228
  if (status) {
1187
1229
  status.textContent = "";
1188
1230
  status.classList.remove("error");
@@ -1200,10 +1242,12 @@ async function runRepositoryGitAction(action) {
1200
1242
  ? result.updated
1201
1243
  ? "Pulled " + result.upstream + " with rebase at " + result.shortCommit + "."
1202
1244
  : result.branch + " is current with " + result.upstream + "."
1203
- : "Pushed " + result.shortCommit + " to " + result.upstream + ".";
1245
+ : action === "retry-sync"
1246
+ ? "Synchronized " + result.shortCommit + " with " + result.upstream + "."
1247
+ : "Pushed " + result.shortCommit + " to " + result.upstream + ".";
1204
1248
  } catch (cause) {
1205
1249
  buttons.forEach((button, index) => { button.disabled = disabled[index]; });
1206
- if (active) active.textContent = action === "pull" ? "Pull with rebase" : "Push";
1250
+ if (active) active.textContent = action === "pull" ? "Pull with rebase" : action === "retry-sync" ? "Retry sync" : "Push";
1207
1251
  if (status) {
1208
1252
  status.textContent = cause.message;
1209
1253
  status.classList.add("error");
@@ -1413,8 +1457,9 @@ function onboardingSteps() {
1413
1457
  points: [
1414
1458
  "Use the UI, an editor, the CLI, or an agent; every path changes the same files.",
1415
1459
  "JSON holds structured records. Markdown holds policies, plans, minutes, and other long-form work.",
1416
- "Repository pulls with rebase, then commits and pushes reviewed changes together.",
1417
- "Agents and terminal users run git pull --rebase, git commit, and git push directly.",
1460
+ "In trunk mode, each browser save fast-forwards, validates, creates one focused commit, and pushes it.",
1461
+ "Record status represents approval. Draft, proposed, approved, and retired records all stay on the authoritative branch.",
1462
+ "Agents and terminal users continue to manage Git explicitly.",
1418
1463
  "The dashboard derives program status from the current repository state."
1419
1464
  ]
1420
1465
  };
@@ -1562,14 +1607,12 @@ function onboardingSetupForm() {
1562
1607
  currentSystem ? "Updates system " + currentSystem.title + "." : "Creates a new in-scope system.",
1563
1608
  "Records a management program goal without creating an audit engagement."
1564
1609
  ].filter(Boolean).join(" ");
1565
- const gitStatus = state.git.available && !state.git.branch
1566
- ? '<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>'
1567
- : state.git.available && state.git.remotes?.length
1568
- ? '<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>'
1569
- : state.git.available
1570
- ? '<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>'
1571
- : '<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>';
1572
- 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 marks the service Planned and In scope. It is selected for scope review, but it is not approved or active. Saving writes JSON files but does not commit them. Complete the remaining Step 1 pages next.</p>';
1610
+ const gitStatus = state.repository?.mode === "trunk"
1611
+ ? '<div class="onboarding-git-status ' + (state.repository.status === "synced" ? "" : "warning") + '"><span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span><span><strong>' + esc(state.repository.label) + '</strong><small>' + esc(state.repository.status === "synced" ? "Completing onboarding will save its related workspace, system, and renderer changes in one commit and push it." : state.repository.message) + '</small></span></div>'
1612
+ : state.git.available && state.git.branch
1613
+ ? '<div class="onboarding-git-status"><span class="status-dot good"></span><span><strong>Manual repository mode</strong><small>Setup changes will stay local until you commit and synchronize them.</small></span></div>'
1614
+ : '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Manual-mode writes still work, but Git history is unavailable until the repository is configured.</small></span></div>';
1615
+ 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 marks the service Planned and In scope. It is selected for scope review, but it is not approved or active. ' + (state.repository?.mode === "trunk" ? "The browser saves and synchronizes the related files together." : "Manual mode leaves the files for you to commit.") + ' Complete the remaining Step 1 pages next.</p>';
1573
1616
  }
1574
1617
 
1575
1618
  function captureOnboardingForm() {
@@ -2562,10 +2605,35 @@ async function responseMessage(response) {
2562
2605
  try { return JSON.parse(source).error || source; } catch { return source; }
2563
2606
  }
2564
2607
  async function localFetch(url, options) {
2608
+ const method = String(options?.method || "GET").toUpperCase();
2609
+ const synchronizing = state?.repository?.mode === "trunk"
2610
+ && ["POST", "PUT", "DELETE"].includes(method)
2611
+ && url !== "/api/evidence-packet";
2612
+ const chip = synchronizing ? document.querySelector(".repo-chip") : null;
2613
+ const previousChip = chip?.innerHTML;
2614
+ let repositoryRefreshed = false;
2615
+ if (chip) chip.innerHTML = '<span class="status-dot neutral"></span>Syncing';
2565
2616
  try {
2566
- return await fetch(url, options);
2617
+ const response = await fetch(url, options);
2618
+ if (synchronizing && !response.ok) {
2619
+ try {
2620
+ const stateResponse = await fetch("/api/state");
2621
+ if (stateResponse.ok) {
2622
+ state = await stateResponse.json();
2623
+ if (chip?.isConnected) {
2624
+ chip.innerHTML = '<span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span>' + esc(state.repository.label);
2625
+ repositoryRefreshed = true;
2626
+ }
2627
+ }
2628
+ } catch {
2629
+ // The original response contains the useful mutation error.
2630
+ }
2631
+ }
2632
+ return response;
2567
2633
  } catch {
2568
2634
  throw new Error("The filegrc server is unavailable. Restart npm run serve, or pnpm dev in the monorepo, and try again.");
2635
+ } finally {
2636
+ if (!repositoryRefreshed && chip?.isConnected && previousChip) chip.innerHTML = previousChip;
2569
2637
  }
2570
2638
  }
2571
2639
  async function fetchJson(url, options) { const response = await localFetch(url, options); if (!response.ok) throw new Error(await responseMessage(response)); return response.json(); }
@@ -2598,7 +2666,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
2598
2666
  .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}
2599
2667
  .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}
2600
2668
  .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}
2601
- .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:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.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;flex:0 0 auto;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-scroll{min-height:0;overflow-y:auto}.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{flex:0 0 auto;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.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 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2669
+ .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)}.repository-state-banner,.repository-override{display:flex;align-items:flex-start;gap:13px;margin-bottom:14px}.repository-state-banner>.status-dot,.repository-override>.status-dot{margin-top:5px}.repository-state-banner h3{margin:3px 0 5px}.repository-state-banner p:last-child,.repository-override p{margin:0;color:var(--muted);line-height:1.5}.repository-override{padding:14px 17px;border:1px solid #e9c888;border-radius:9px;background:#fff8e8;font-size:13.2px}.repository-override strong{display:block;margin-bottom:4px}.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:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.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;flex:0 0 auto;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-scroll{min-height:0;overflow-y:auto}.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{flex:0 0 auto;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.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 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
2602
2670
  .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}
2603
2671
  @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}}
2604
2672
  @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))}}