filegrc 0.16.22 → 0.16.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.16.22",
3
+ "version": "0.16.23",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -433,15 +433,7 @@ async function requireControlReviewReady(loaded, resourceType, programId) {
433
433
  if (resourceType !== "control") return;
434
434
  const { assessProgramReadiness } = await import("./program-readiness.js");
435
435
  const readiness = await assessProgramReadiness(loaded, { programId });
436
- const earlierItems = readiness.stages
437
- .find(({ id }) => id === "controls")
438
- ?.items.filter(({ id }) => (
439
- id !== "collection-review-control"
440
- && !id.startsWith("document-activation-")
441
- && !id.startsWith("training-activation-")
442
- && !id.startsWith("policy-activation-")
443
- )) || [];
444
- if (!earlierItems.length || earlierItems.some(({ status }) => status !== "complete")) {
436
+ if (!readiness.controlReviewReady) {
445
437
  throw new Error("Complete the Step 3 implementation work before recording the Control collection review. Program content activation follows this review.");
446
438
  }
447
439
  }
@@ -203,6 +203,7 @@ export async function assessProgramReadiness(input, options = {}) {
203
203
  asOfTime
204
204
  );
205
205
  const controlOversight = collectionReviews.find(({ resourceType }) => resourceType === "control");
206
+ const controlReviewPrerequisiteIds = controlStage.items.map(({ id }) => id);
206
207
  const oversightEligible = Boolean(controlOversight && controlOversightEligible(controlStage.items));
207
208
  if (oversightEligible) {
208
209
  controlStage.items.push(collectionReviewReadinessItem(controlOversight));
@@ -331,6 +332,8 @@ export async function assessProgramReadiness(input, options = {}) {
331
332
  canStartCandidatePeriod,
332
333
  suggestedCandidatePeriodStart: canStartCandidatePeriod ? asOf : null,
333
334
  policyActivations,
335
+ controlReviewReady: oversightEligible,
336
+ controlReviewPrerequisiteIds,
334
337
  documentActivations: governedContent.documentActivations,
335
338
  trainingActivations: governedContent.trainingActivations,
336
339
  policyLibraryProposals: [
package/src/web.js CHANGED
@@ -1263,7 +1263,10 @@ function collectionReviewPanel(type, force = false) {
1263
1263
  const proposalBlocker = !current && assessment.incompleteRecordProposals?.length
1264
1264
  ? "Complete " + assessment.incompleteRecordProposals.length + " record " + (assessment.incompleteRecordProposals.length === 1 ? "proposal" : "proposals") + " before the collection review."
1265
1265
  : null;
1266
- const reviewBlocker = scheduleBlocker || proposalBlocker;
1266
+ const controlBlocker = type === "control" && !state.programReadiness?.controlReviewReady
1267
+ ? "Complete the Control implementation checks listed in Step 3 before this review."
1268
+ : null;
1269
+ const reviewBlocker = scheduleBlocker || proposalBlocker || controlBlocker;
1267
1270
  const needsFirstRecord = collectionNeedsFirstRecord(type);
1268
1271
  const reviewerNames = (assessment.review?.reviewedByIds || [])
1269
1272
  .map((id) => state.resources.find(({ record }) => record.id === id)?.record.title || id);
@@ -1322,27 +1325,29 @@ function collectionReviewVisible(type) {
1322
1325
  }
1323
1326
 
1324
1327
  function renderFinishStepThree() {
1325
- const stage = state.programReadiness?.stages?.find(({ id }) => id === "controls");
1328
+ const readiness = state.programReadiness;
1329
+ const stage = readiness?.stages?.find(({ id }) => id === "controls");
1326
1330
  const items = stage?.items || [];
1327
1331
  const oversightItem = items.find(({ id }) => id === "collection-review-control");
1328
1332
  const oversight = state.collectionReviews?.control;
1329
- const activationItems = items.filter(({ id }) => (
1330
- id.startsWith("document-activation-")
1331
- || id.startsWith("training-activation-")
1332
- || id.startsWith("policy-activation-")
1333
- ));
1334
- const implementationItems = items.filter(({ id }) => (
1335
- id !== "collection-review-control"
1336
- && !id.startsWith("document-activation-")
1337
- && !id.startsWith("training-activation-")
1338
- && !id.startsWith("policy-activation-")
1339
- ));
1340
- const next = implementationItems.find(({ status }) => status !== "complete");
1341
- if (!oversightItem) {
1333
+ const prerequisiteIds = new Set(readiness?.controlReviewPrerequisiteIds || []);
1334
+ const blockers = items.filter(({ id, status }) => prerequisiteIds.has(id) && status !== "complete");
1335
+ const activationItems = items.filter(({ id }) => id !== "collection-review-control" && !prerequisiteIds.has(id));
1336
+ if (!readiness?.controlReviewReady || !oversightItem) {
1337
+ const next = blockers[0];
1342
1338
  const href = next?.id?.startsWith("source-family-")
1343
1339
  ? "#/evidence-sources"
1344
1340
  : next ? workflowItemHref({ ...next, state: next.status }) || "#/resources/control" : "#/resources/control";
1345
- return '<section class="finish-step panel blocked"><div><p class="kicker">Finish Step 3</p><h2>Complete implementation first</h2><p>' + esc(next?.message || "Finish the Step 3 work areas before the final review becomes available.") + '</p></div><a class="button primary" href="' + esc(href) + '">Continue implementation</a></section>';
1341
+ const blockerRows = blockers.map((item) => {
1342
+ const target = item.id.startsWith("source-family-") ? "#/evidence-sources"
1343
+ : workflowItemHref({ ...item, state: item.status }) || "#/resources/control";
1344
+ const resolution = item.nextSteps?.length ? item.nextSteps.join(" ")
1345
+ : item.id.startsWith("source-family-")
1346
+ ? item.message + " Open Evidence Sources to assign a matching active source, access owners, and extraction instructions for every listed Control."
1347
+ : item.message;
1348
+ return '<li><a href="' + esc(target) + '"><strong>' + esc(item.title) + '</strong></a><p>' + esc(resolution) + '</p></li>';
1349
+ }).join("");
1350
+ return '<section class="finish-step panel blocked"><div><p class="kicker">Finish Step 3</p><h2>Complete implementation first</h2><p>' + esc(blockers.length ? blockers.length + " implementation " + pluralize("blocker", blockers.length) + " remain before the Control collection review." : "Finish the Step 3 work areas before the final review becomes available.") + '</p>' + (blockerRows ? '<details class="control-review-blockers"><summary>Review every blocker and its resolution</summary><ol>' + blockerRows + '</ol></details>' : "") + '</div><a class="button primary" href="' + esc(href) + '">Continue implementation</a></section>';
1346
1351
  }
1347
1352
  if (!oversight?.complete) {
1348
1353
  return '<section class="finish-step-heading"><p class="kicker">Finish Step 3</p><h2>Review the implemented Control collection</h2><p>One eligible reviewer confirms the implemented Controls as a batch. Management activates the approved program content after this review.</p></section>' + collectionReviewPanel("control", true);
@@ -1868,7 +1873,12 @@ function stagePageCard(stage, destination, index) {
1868
1873
  return '<a href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(item.implementationState ? controlLaunchLabel(item.implementationState).split(":")[0] : properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong><small>' + esc(stagePageItemDetail(item)) + '</small></span></a>';
1869
1874
  }).join("") + (items.length > 3 ? '<small class="stage-page-tasks-more">+' + (items.length - 3) + ' more on this page</small>' : "") + '</div>'
1870
1875
  : "";
1871
- return '<article class="stage-page-card ' + (complete ? "complete" : "") + '"><div class="stage-page-card-head"><div><small>' + esc(stepLabel) + '</small><h3>' + esc(destination.label) + '</h3></div><div class="stage-page-card-actions">' + completionState + '<a class="stage-page-open" href="' + destination.href + '" aria-label="Open ' + esc(destination.label) + '"><span>Open ›</span></a></div></div><p>' + esc(summary) + '</p>' + taskPreview + '</article>';
1876
+ const proposals = stage.id === "controls" && destination.type === "obligation"
1877
+ ? stepThreeObligationProposals().length : 0;
1878
+ const proposalNote = proposals
1879
+ ? '<p class="stage-page-proposals">' + proposals + ' proposed ' + pluralize("schedule", proposals) + ' to review. These proposals do not each block Control implementation.</p>'
1880
+ : "";
1881
+ return '<article class="stage-page-card ' + (complete ? "complete" : "") + '"><div class="stage-page-card-head"><div><small>' + esc(stepLabel) + '</small><h3>' + esc(destination.label) + '</h3></div><div class="stage-page-card-actions">' + completionState + '<a class="stage-page-open" href="' + destination.href + '" aria-label="Open ' + esc(destination.label) + '"><span>Open ›</span></a></div></div><p>' + esc(summary) + '</p>' + taskPreview + proposalNote + '</article>';
1872
1882
  }
1873
1883
 
1874
1884
  function stagePageItemDetail(item) {
@@ -1957,6 +1967,19 @@ function derivedStagePageState(stage, destination) {
1957
1967
  }
1958
1968
 
1959
1969
  function stagePageItems(stage, destination) {
1970
+ if (stage.id === "controls" && destination.type === "obligation") {
1971
+ return (state.programReadiness?.stages?.find(({ id }) => id === "controls")?.items || [])
1972
+ .filter(({ id, checks }) => id.startsWith("control-") && checks?.workQueue === false)
1973
+ .map((item) => ({
1974
+ ...item,
1975
+ key: item.id + "-work-queue",
1976
+ state: "ready",
1977
+ implementationState: null,
1978
+ title: "Enable a work schedule for " + item.title,
1979
+ message: "Review the linked Obligation proposals and enable a rule that matches this Control's operation pattern before marking it Implemented.",
1980
+ subject: { type: "control", id: item.resourceId }
1981
+ }));
1982
+ }
1960
1983
  const activeStates = new Set(["blocked", "due", "later", "open", "overdue", "ready", "scheduled", "upcoming", "waiting-external"]);
1961
1984
  const items = [
1962
1985
  ...(state.workflow?.findings || []),
@@ -2000,6 +2023,22 @@ function stagePageItems(stage, destination) {
2000
2023
  ));
2001
2024
  }
2002
2025
 
2026
+ function stepThreeObligationProposals() {
2027
+ const selectedControlIds = new Set(state.programReadiness?.scope?.controlIds || []);
2028
+ const proposedIds = new Set(state.resources.filter(({ record }) => (
2029
+ record.type === "obligation"
2030
+ && record.status === "proposed"
2031
+ && (record.controlIds || []).some((id) => selectedControlIds.has(id))
2032
+ ))
2033
+ .map(({ record }) => record.id));
2034
+ return (state.workflow?.findings || []).filter((item) => (
2035
+ item.stage === "controls"
2036
+ && item.subject?.type === "obligation"
2037
+ && proposedIds.has(item.subject.id)
2038
+ && item.key?.endsWith(".finalize")
2039
+ ));
2040
+ }
2041
+
2003
2042
  function workflowUiStage(stageId) {
2004
2043
  return ["operate", "operation"].includes(stageId) ? "run" : stageId;
2005
2044
  }
@@ -6618,6 +6657,7 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
6618
6657
 
6619
6658
  export const APP_STYLES = String.raw`
6620
6659
  .finish-step{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-top:24px;padding:20px 22px}.finish-step h2,.finish-step-heading h2{margin:5px 0 7px;font:500 24px Georgia,serif}.finish-step p:not(.kicker),.finish-step-heading p:not(.kicker){margin:0;color:var(--muted);font-size:12px;line-height:1.55}.finish-step.blocked{border-color:#d8bd78}.finish-step.complete{border-color:#b9dac6}.finish-step-heading{margin-top:28px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}
6660
+ .finish-step.blocked>div{min-width:0;flex:1}.control-review-blockers{margin-top:13px}.control-review-blockers summary{cursor:pointer;color:var(--accent);font-size:11px;font-weight:700}.control-review-blockers ol{max-height:360px;overflow:auto;margin:10px 0 0;padding:0 14px 0 28px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.control-review-blockers li{padding:11px 0;font-size:11px}.control-review-blockers li+li{border-top:1px solid var(--line)}.control-review-blockers li a{color:var(--accent)}.control-review-blockers li p{margin-top:4px!important}.stage-page-proposals{margin-top:auto!important;padding-top:12px;border-top:1px solid var(--line)}
6621
6661
  :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}
6622
6662
  *{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}@keyframes spinner-rotate{to{transform:rotate(360deg)}}.spinner{display:inline-block;flex:0 0 auto;width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-rotate .7s linear infinite}.spinner-large{width:30px;height:30px;border-width:3px}.loading,.detail-loading,.muted.is-loading,.save-status.is-loading,.onboarding-save-status.is-loading,.button.is-loading{display:flex;align-items:center;gap:9px}.loading{min-height:100vh;justify-content:center;color:var(--muted)}.state-loading{display:flex;align-items:flex-start;gap:16px}.state-loading>.spinner{margin-top:5px;color:var(--accent)}.state-loading h2{margin:5px 0 8px}.state-loading p:last-child{margin:0;color:var(--muted)}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2px}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
6623
6663
  .topbar-readiness{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:0 1 220px;min-width:155px;padding:5px 8px;border:1px solid transparent;border-radius:8px;color:var(--ink);text-decoration:none}.topbar-readiness:hover{border-color:var(--accent-light);background:var(--accent-soft)}.topbar-readiness-copy{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}.topbar-readiness-copy>span{overflow:hidden;color:var(--muted);font-size:9.6px;font-weight:700;text-overflow:ellipsis;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.topbar-readiness-copy>strong{font-size:10.8px;line-height:1}.topbar-readiness>.progress{width:100%;height:5px}.topbar-status{display:flex;flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-end;gap:8px;margin-left:auto}.program-select select{max-width:180px;border:1px solid var(--line);border-radius:8px;padding:9px 28px 9px 10px;background:var(--surface);color:var(--ink);font:inherit}.topbar-status .topbar-search{flex:0 1 240px;width:240px;min-width:140px;margin-left:0}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}