filegrc 0.16.22 → 0.16.24
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 +1 -1
- package/src/collection-review.js +3 -22
- package/src/collection-revision.js +14 -7
- package/src/program-readiness.js +3 -0
- package/src/web.js +60 -17
package/package.json
CHANGED
package/src/collection-review.js
CHANGED
|
@@ -3,6 +3,7 @@ import { modelSupports } from "../model/index.js";
|
|
|
3
3
|
import {
|
|
4
4
|
collectionRevision,
|
|
5
5
|
collectionReviewDecisionMatches,
|
|
6
|
+
controlReviewerConflictIds,
|
|
6
7
|
reviewedControlConflictIds
|
|
7
8
|
} from "./collection-revision.js";
|
|
8
9
|
import { collectionRecordProposals, retentionScheduleReviewScope, scopedCollectionRecords } from "./collection-scope.js";
|
|
@@ -433,15 +434,7 @@ async function requireControlReviewReady(loaded, resourceType, programId) {
|
|
|
433
434
|
if (resourceType !== "control") return;
|
|
434
435
|
const { assessProgramReadiness } = await import("./program-readiness.js");
|
|
435
436
|
const readiness = await assessProgramReadiness(loaded, { programId });
|
|
436
|
-
|
|
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")) {
|
|
437
|
+
if (!readiness.controlReviewReady) {
|
|
445
438
|
throw new Error("Complete the Step 3 implementation work before recording the Control collection review. Program content activation follows this review.");
|
|
446
439
|
}
|
|
447
440
|
}
|
|
@@ -458,19 +451,7 @@ async function requireRetentionScheduleReady(loaded, resourceType, program, rete
|
|
|
458
451
|
}
|
|
459
452
|
|
|
460
453
|
function assessControlReviewers(loaded, controls) {
|
|
461
|
-
const
|
|
462
|
-
const controlIds = new Set(controls.map(({ id }) => id));
|
|
463
|
-
const conflictIds = new Set();
|
|
464
|
-
for (const control of controls) {
|
|
465
|
-
for (const id of currentPartyPeople(control.ownerIds || [], byId)) conflictIds.add(id);
|
|
466
|
-
}
|
|
467
|
-
for (const obligation of loaded.resources.filter((record) => (
|
|
468
|
-
record.type === "obligation"
|
|
469
|
-
&& record.status === "active"
|
|
470
|
-
&& (record.controlIds || []).some((id) => controlIds.has(id))
|
|
471
|
-
))) {
|
|
472
|
-
for (const id of currentPartyPeople(obligation.ownerIds || [], byId)) conflictIds.add(id);
|
|
473
|
-
}
|
|
454
|
+
const conflictIds = new Set(controlReviewerConflictIds(loaded, controls));
|
|
474
455
|
const people = loaded.resources.filter(({ type, status }) => type === "person" && status === "active");
|
|
475
456
|
return {
|
|
476
457
|
eligibleReviewerIds: people.map(({ id }) => id).filter((id) => !conflictIds.has(id)),
|
|
@@ -128,14 +128,21 @@ export function reviewedControlConflictIds(loaded, review) {
|
|
|
128
128
|
const controls = scopedCollectionRecords(source, "control", program);
|
|
129
129
|
const controlIds = new Set(controls.map(({ id }) => id));
|
|
130
130
|
const byId = new Map(source.resources.map((record) => [record.id, record]));
|
|
131
|
+
return controlReviewerConflictIds(source, controls, byId, controlIds);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function controlReviewerConflictIds(loaded, controls, byId = new Map(loaded.resources.map((record) => [record.id, record])), controlIds = new Set(controls.map(({ id }) => id))) {
|
|
131
135
|
const conflicts = new Set();
|
|
132
|
-
for (const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
for (const control of controls) {
|
|
137
|
+
for (const id of currentPartyPeople(control.ownerIds || [], byId)) conflicts.add(id);
|
|
138
|
+
}
|
|
139
|
+
for (const obligation of loaded.resources.filter((record) => record.type === "obligation"
|
|
140
|
+
&& record.status === "active"
|
|
141
|
+
&& (record.controlIds || []).some((id) => controlIds.has(id)))) {
|
|
142
|
+
// A team owns the work queue, but membership alone does not mean each member
|
|
143
|
+
// performed its work. Named people and appointment holders remain conflicts.
|
|
144
|
+
const namedOwners = (obligation.ownerIds || []).filter((id) => byId.get(id)?.type !== "team");
|
|
145
|
+
for (const id of currentPartyPeople(namedOwners, byId)) conflicts.add(id);
|
|
139
146
|
}
|
|
140
147
|
return [...conflicts].sort();
|
|
141
148
|
}
|
package/src/program-readiness.js
CHANGED
|
@@ -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
|
@@ -650,6 +650,9 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
|
|
|
650
650
|
});
|
|
651
651
|
main.querySelector("[data-review-policy-activation]")?.addEventListener("click", openPolicyActivationDialog);
|
|
652
652
|
main.querySelector("[data-review-document-activation]")?.addEventListener("click", () => openDocumentActivationDialog());
|
|
653
|
+
if (stage.id === "controls") {
|
|
654
|
+
main.querySelector("[data-review-collection]")?.addEventListener("click", () => openCollectionReviewDialog("control"));
|
|
655
|
+
}
|
|
653
656
|
main.querySelector("[data-review-audit-document-activation]")?.addEventListener("click", (event) => {
|
|
654
657
|
openDocumentActivationDialog(event.currentTarget.dataset.reviewAuditDocumentActivation);
|
|
655
658
|
});
|
|
@@ -1263,7 +1266,10 @@ function collectionReviewPanel(type, force = false) {
|
|
|
1263
1266
|
const proposalBlocker = !current && assessment.incompleteRecordProposals?.length
|
|
1264
1267
|
? "Complete " + assessment.incompleteRecordProposals.length + " record " + (assessment.incompleteRecordProposals.length === 1 ? "proposal" : "proposals") + " before the collection review."
|
|
1265
1268
|
: null;
|
|
1266
|
-
const
|
|
1269
|
+
const controlBlocker = type === "control" && !state.programReadiness?.controlReviewReady
|
|
1270
|
+
? "Complete the Control implementation checks listed in Step 3 before this review."
|
|
1271
|
+
: null;
|
|
1272
|
+
const reviewBlocker = scheduleBlocker || proposalBlocker || controlBlocker;
|
|
1267
1273
|
const needsFirstRecord = collectionNeedsFirstRecord(type);
|
|
1268
1274
|
const reviewerNames = (assessment.review?.reviewedByIds || [])
|
|
1269
1275
|
.map((id) => state.resources.find(({ record }) => record.id === id)?.record.title || id);
|
|
@@ -1322,27 +1328,29 @@ function collectionReviewVisible(type) {
|
|
|
1322
1328
|
}
|
|
1323
1329
|
|
|
1324
1330
|
function renderFinishStepThree() {
|
|
1325
|
-
const
|
|
1331
|
+
const readiness = state.programReadiness;
|
|
1332
|
+
const stage = readiness?.stages?.find(({ id }) => id === "controls");
|
|
1326
1333
|
const items = stage?.items || [];
|
|
1327
1334
|
const oversightItem = items.find(({ id }) => id === "collection-review-control");
|
|
1328
1335
|
const oversight = state.collectionReviews?.control;
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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) {
|
|
1336
|
+
const prerequisiteIds = new Set(readiness?.controlReviewPrerequisiteIds || []);
|
|
1337
|
+
const blockers = items.filter(({ id, status }) => prerequisiteIds.has(id) && status !== "complete");
|
|
1338
|
+
const activationItems = items.filter(({ id }) => id !== "collection-review-control" && !prerequisiteIds.has(id));
|
|
1339
|
+
if (!readiness?.controlReviewReady || !oversightItem) {
|
|
1340
|
+
const next = blockers[0];
|
|
1342
1341
|
const href = next?.id?.startsWith("source-family-")
|
|
1343
1342
|
? "#/evidence-sources"
|
|
1344
1343
|
: next ? workflowItemHref({ ...next, state: next.status }) || "#/resources/control" : "#/resources/control";
|
|
1345
|
-
|
|
1344
|
+
const blockerRows = blockers.map((item) => {
|
|
1345
|
+
const target = item.id.startsWith("source-family-") ? "#/evidence-sources"
|
|
1346
|
+
: workflowItemHref({ ...item, state: item.status }) || "#/resources/control";
|
|
1347
|
+
const resolution = item.nextSteps?.length ? item.nextSteps.join(" ")
|
|
1348
|
+
: item.id.startsWith("source-family-")
|
|
1349
|
+
? item.message + " Open Evidence Sources to assign a matching active source, access owners, and extraction instructions for every listed Control."
|
|
1350
|
+
: item.message;
|
|
1351
|
+
return '<li><a href="' + esc(target) + '"><strong>' + esc(item.title) + '</strong></a><p>' + esc(resolution) + '</p></li>';
|
|
1352
|
+
}).join("");
|
|
1353
|
+
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
1354
|
}
|
|
1347
1355
|
if (!oversight?.complete) {
|
|
1348
1356
|
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 +1876,12 @@ function stagePageCard(stage, destination, index) {
|
|
|
1868
1876
|
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
1877
|
}).join("") + (items.length > 3 ? '<small class="stage-page-tasks-more">+' + (items.length - 3) + ' more on this page</small>' : "") + '</div>'
|
|
1870
1878
|
: "";
|
|
1871
|
-
|
|
1879
|
+
const proposals = stage.id === "controls" && destination.type === "obligation"
|
|
1880
|
+
? stepThreeObligationProposals().length : 0;
|
|
1881
|
+
const proposalNote = proposals
|
|
1882
|
+
? '<p class="stage-page-proposals">' + proposals + ' proposed ' + pluralize("schedule", proposals) + ' to review. These proposals do not each block Control implementation.</p>'
|
|
1883
|
+
: "";
|
|
1884
|
+
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
1885
|
}
|
|
1873
1886
|
|
|
1874
1887
|
function stagePageItemDetail(item) {
|
|
@@ -1957,6 +1970,19 @@ function derivedStagePageState(stage, destination) {
|
|
|
1957
1970
|
}
|
|
1958
1971
|
|
|
1959
1972
|
function stagePageItems(stage, destination) {
|
|
1973
|
+
if (stage.id === "controls" && destination.type === "obligation") {
|
|
1974
|
+
return (state.programReadiness?.stages?.find(({ id }) => id === "controls")?.items || [])
|
|
1975
|
+
.filter(({ id, checks }) => id.startsWith("control-") && checks?.workQueue === false)
|
|
1976
|
+
.map((item) => ({
|
|
1977
|
+
...item,
|
|
1978
|
+
key: item.id + "-work-queue",
|
|
1979
|
+
state: "ready",
|
|
1980
|
+
implementationState: null,
|
|
1981
|
+
title: "Enable a work schedule for " + item.title,
|
|
1982
|
+
message: "Review the linked Obligation proposals and enable a rule that matches this Control's operation pattern before marking it Implemented.",
|
|
1983
|
+
subject: { type: "control", id: item.resourceId }
|
|
1984
|
+
}));
|
|
1985
|
+
}
|
|
1960
1986
|
const activeStates = new Set(["blocked", "due", "later", "open", "overdue", "ready", "scheduled", "upcoming", "waiting-external"]);
|
|
1961
1987
|
const items = [
|
|
1962
1988
|
...(state.workflow?.findings || []),
|
|
@@ -2000,6 +2026,22 @@ function stagePageItems(stage, destination) {
|
|
|
2000
2026
|
));
|
|
2001
2027
|
}
|
|
2002
2028
|
|
|
2029
|
+
function stepThreeObligationProposals() {
|
|
2030
|
+
const selectedControlIds = new Set(state.programReadiness?.scope?.controlIds || []);
|
|
2031
|
+
const proposedIds = new Set(state.resources.filter(({ record }) => (
|
|
2032
|
+
record.type === "obligation"
|
|
2033
|
+
&& record.status === "proposed"
|
|
2034
|
+
&& (record.controlIds || []).some((id) => selectedControlIds.has(id))
|
|
2035
|
+
))
|
|
2036
|
+
.map(({ record }) => record.id));
|
|
2037
|
+
return (state.workflow?.findings || []).filter((item) => (
|
|
2038
|
+
item.stage === "controls"
|
|
2039
|
+
&& item.subject?.type === "obligation"
|
|
2040
|
+
&& proposedIds.has(item.subject.id)
|
|
2041
|
+
&& item.key?.endsWith(".finalize")
|
|
2042
|
+
));
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2003
2045
|
function workflowUiStage(stageId) {
|
|
2004
2046
|
return ["operate", "operation"].includes(stageId) ? "run" : stageId;
|
|
2005
2047
|
}
|
|
@@ -6618,6 +6660,7 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
|
|
|
6618
6660
|
|
|
6619
6661
|
export const APP_STYLES = String.raw`
|
|
6620
6662
|
.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}
|
|
6663
|
+
.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
6664
|
: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
6665
|
*{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
6666
|
.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)}
|