diff --git a/CLAUDE.md b/CLAUDE.md index fd926ef..fd421aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Browser-local change-assurance tool for project controls reviewers: compares a c ## Constraints - Never fabricate numbers. No random or synthetic values in outputs; missing inputs give NA or an empty result. `test_no_fabricated_outputs.py` AST-guards `Processing/` (only the seeded `decision_support.py` simulation is allowed). - XER files, decisions and conditions stay in the browser. The static site must not probe localhost or hold API keys; the sidecar receives narrative and filters only. -- Schedule findings are deterministic code, not LLM output. Generated briefs fail closed (`summarize.py: evaluate_summary`) and every precedent needs a human Use/Ignore. +- Schedule findings are deterministic code, not LLM output. Generated briefs fail closed (`summarize.py: evaluate_summary`) and public-record context is optional. An attached or dismissed record needs a human Use/Ignore; untouched records remain not reviewed. - Every README number must be reproducible by the command shown next to it. - Internal notes, agent prompts/logs and market research stay out of the tree (see `.gitignore`). UK English in public docs. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..a514b4c --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,11 @@ +# Change assurance interface + +Operate mode. Preserve ProjectLens's warm paper (#f1ecdf), near-black ink (#071a1b), off-white (#fffdf6), rust (#a94d35) and pale blue (#8eb0bd). Rust identifies unresolved evidence; green is reserved for the limited outcome of fixed checks. No causation, entitlement or approval claim follows from colour. + +The review uses Libre Franklin: title 24px, body 14px, compact evidence 13px, metadata at least 12px. Archivo Black stays on the landing page. JetBrains Mono identifies source activity codes. Use 8/16/24/32px spacing, 1px borders, flat rectangular surfaces and no decorative gradients or shadows. + +The top navigation remains Reviews, Decisions and Follow-up, with aria-current. The active review hides the landing pitch. Findings and source evidence share a two-column desk; a selected finding has a labelled pressed state. Mobile uses one pane and a Back to findings control which returns focus. The primary action records a human decision; questions and optional public context are secondary. + +Keep raw source facts and the original narrative visible. Untouched supplemental records are not reviewed; using one never makes it an authoritative precedent. Owner and rationale are required. Conditional approvals require condition, owner and due date. Errors persist with recovery guidance. A deliberate device draft includes XER contents and form fields; saving and deletion are explicit. No automatic upload or localhost probing. + +Focus uses a 3px blue outline, controls have visible hover and disabled states, and navigation transfers focus. Motion only scrolls to revealed work and respects reduced motion. Do not add app heroes, metric cards, invented confidence scores, decorative icons or sidebars. diff --git a/Processing/tests/browser_change_assurance.py b/Processing/tests/browser_change_assurance.py index e47f8ef..b170cbd 100644 --- a/Processing/tests/browser_change_assurance.py +++ b/Processing/tests/browser_change_assurance.py @@ -13,7 +13,7 @@ def run_desktop(browser): - page = browser.new_page(viewport={"width": 1440, "height": 1000}, device_scale_factor=1) + page = browser.new_page(viewport={"width": 1280, "height": 720}, device_scale_factor=1, reduced_motion="reduce") errors = [] failures = [] page.on("console", lambda message: errors.append(message.text) if message.type == "error" else None) @@ -31,20 +31,15 @@ def run_desktop(browser): readiness_status = page.locator("#readinessStatus").inner_text() assert readiness_status.casefold() == "needs evidence before decision", readiness_status assert page.locator("#blockerList .blocker").count() == 3 - assert "73 days" in page.locator("#readinessHeadline").inner_text().casefold() - # precedents surface by default — the RAG differentiator must not hide in a closed disclosure - assert page.locator("#supportingEvidence").get_attribute("open") is not None - assert page.get_by_role("button", name="Review cited precedents").is_visible() - + assert "3 findings" in page.locator("#readinessHeadline").inner_text() + assert "26 Feb 2028" in page.locator("#findingEvidence").inner_text() + assert page.locator("#showDecisionForm").bounding_box()["y"] < 720 + assert page.locator("#supportingEvidence").get_attribute("open") is None assert page.locator("#evidenceSummary .evidence-fact").count() == 6 - # Unconfigured static mode must work without attempting a sidecar request. - page.wait_for_function("() => document.querySelectorAll('#comparableCases .comparable-case:not(.is-skeleton)').length >= 3") - assert page.locator("#comparableCases .comparable-case").count() >= 3 - assert "static" in page.locator("#precedentStatus").inner_text().lower() assert not sidecar_requests, sidecar_requests - # gate label is section-level, not repeated on every card - assert page.locator("#precedentSectionGate").is_visible() - assert page.locator("#comparableCases .precedent-gate i").first.inner_text().casefold() != "awaiting human gate" + page.screenshot(path=str(SCREENSHOTS / "change-assurance-overview.png"), full_page=False) + page.locator("[data-finding='1']").click() + assert "constraint" in page.locator("#findingEvidence").inner_text().lower() page.get_by_role("button", name="Draft questions for the team").click() page.locator("#requestPanel").wait_for(state="visible") @@ -55,28 +50,30 @@ def run_desktop(browser): page.get_by_role("button", name="Record human decision").click() page.locator("#decisionPanel").wait_for(state="visible") - # gate still locked until Use/Ignore - assert page.locator("#decisionForm button[type='submit']").is_disabled() - assert page.locator("#precedentGateBanner").is_visible() - # human gate first — form fields stay locked until every card is marked - # supporting evidence was already opened above - for button in page.locator("#comparableCases button[data-gate='use']").all(): - button.click() + # Supplemental context must never force the reviewer to dismiss unrelated examples. assert page.locator("#decisionForm button[type='submit']").is_enabled() - assert page.locator("#precedentGateBanner").is_hidden() - page.locator("#decisionPanel").scroll_into_view_if_needed() + assert "not reviewed" in page.locator("#precedentGateBanner").inner_text() page.get_by_label("Approve with conditions").check(force=True) page.locator("#decisionOwner").fill("Change Authority Chair") page.locator("#decisionRationale").fill("Proceed only after the named controls are confirmed and owned.") page.locator("#decisionCondition").fill("Protect the regression-test window") page.locator("#conditionOwner").fill("Test Lead") page.locator("#conditionDue").fill("2026-07-29") + page.get_by_role("button", name="Save draft on this device").click() + page.reload() + page.get_by_role("button", name="Resume saved draft").click() + assert page.locator("#decisionOwner").input_value() == "Change Authority Chair" + assert page.locator("#conditionOwner").input_value() == "Test Lead" page.get_by_role("button", name="Save decision record").click() page.wait_for_url("**#decisions") page.locator("#decisionRegister .register-row").wait_for(state="visible") assert page.locator("#decisionRegister .register-row").count() == 1 stored = page.evaluate("JSON.parse(localStorage.getItem('projectlens:change-decisions:v1'))[0]") - assert stored.get("precedentsUsed"), stored + assert stored.get("precedentsNotReviewed") and len(stored["precedentsNotReviewed"]) == 3, stored + assert not stored["precedentsUsed"], stored + assert any(finding["type"] == "Ownership gap" for finding in stored["findings"]) + assert page.evaluate("localStorage.getItem('projectlens:review-draft:v1')") is None + assert len(stored["findings"]) >= 3, stored page.evaluate( """() => { @@ -91,6 +88,9 @@ def run_desktop(browser): page.wait_for_function("() => window.__copied !== null") copied = page.evaluate("() => window.__copied") assert "Verdict: Approve with conditions" in copied, copied + assert "Public-record context not reviewed:" in copied, copied + assert "northstar-current.xer" in copied, copied + assert "relationship:" in copied and "hours" in copied, copied assert "Decision owner: Change Authority Chair" in copied, copied assert "Protect the regression-test window" in copied, copied assert page.locator("#exportFallback").is_hidden() @@ -122,7 +122,7 @@ def run_desktop(browser): def run_mobile(browser): - page = browser.new_page(viewport={"width": 390, "height": 844}, device_scale_factor=1) + page = browser.new_page(viewport={"width": 390, "height": 844}, device_scale_factor=1, reduced_motion="reduce") page.goto(BASE_URL) page.wait_for_load_state("networkidle") assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1") @@ -131,14 +131,88 @@ def run_mobile(browser): page.locator("#readinessWorkspace").wait_for(state="visible") assert page.locator("#blockerList .blocker").count() == 3 assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1") - page.evaluate("document.querySelector('#readinessWorkspace').scrollIntoView()") + page.locator("[data-finding='2']").scroll_into_view_if_needed() + finding_scroll = page.evaluate("window.scrollY") + page.locator("[data-finding='2']").click() + assert page.locator("#findingEvidence").is_visible() + page.get_by_role("button", name="Back to findings").click() + assert page.locator("[data-finding='2']").evaluate("el => el === document.activeElement") + assert abs(page.evaluate("window.scrollY") - finding_scroll) <= 1 + page.evaluate("window.scrollTo(0, 0)") page.screenshot(path=str(SCREENSHOTS / "change-assurance-mobile.png"), full_page=False) page.close() +def run_recovery(browser): + page = browser.new_page(viewport={"width": 1280, "height": 720}, reduced_motion="reduce") + page.goto(BASE_URL) + rejected = page.evaluate("""([beforeText, afterText]) => { + const api = window.ProjectLensChangeAssurance; + const results = {}; + for (const kind of ['identical', 'reversed', 'identity']) { + const before = api.parseXer(beforeText, 'previous.xer'); + const after = api.parseXer(afterText, 'current.xer'); + if (kind === 'identity') after.project.id = 'different'; + try { api.analyseReview(kind === 'reversed' ? after : before, kind === 'identical' ? before : kind === 'reversed' ? before : after, 'On track and unchanged.'); results[kind] = false; } + catch { results[kind] = true; } + } + const before = api.parseXer(beforeText, 'previous.xer'); + const after = api.parseXer(afterText, 'current.xer'); + after.tasks.forEach(task => { task.float = null; }); + results.coverage = api.analyseReview(before, after, 'On track.').blockers.some(blocker => blocker.title.includes('could not be compared')); + const relationshipReview = api.analyseReview(api.parseXer(beforeText, 'previous.xer'), api.parseXer(afterText, 'current.xer'), 'Unchanged.'); + results.relationships = relationshipReview.changes.relationEvidence.length === 5 && relationshipReview.changes.relationEvidence.every(item => item.predecessor && item.successor && item.type); + return results; + }""", [(ROOT / "docs/demo/northstar-previous.xer").read_text(), (ROOT / "docs/demo/northstar-current.xer").read_text()]) + assert all(rejected.values()), rejected + page.get_by_role("button", name="Use my own XER files").click() + page.locator("#assurancePrevious").set_input_files(ROOT / "docs/demo/northstar-previous.xer") + page.locator("#assuranceCurrent").set_input_files(ROOT / "docs/demo/riverside-current.xer") + page.get_by_role("button", name="Check decision readiness").click() + page.locator("#workspaceError").wait_for(state="visible") + assert "project names differ" in page.locator("#workspaceError").inner_text() + assert page.locator("#packIntake").is_visible() + page.locator("#assuranceCurrent").set_input_files(ROOT / "docs/demo/northstar-current.xer") + page.locator("#assuranceNarrative").fill("Unchanged.") + page.get_by_role("button", name="Check decision readiness").click() + page.locator("#readinessWorkspace").wait_for(state="visible") + page.locator("[data-finding='2']").click() + assert "Relationship field evidence (5)" in page.locator("#findingEvidence").inner_text() + assert page.locator("#findingEvidence li").count() == 5 + assert "Type:" in page.locator("#findingEvidence").inner_text() + page.once("dialog", lambda dialog: dialog.accept()) + page.get_by_role("button", name="Start another review").click() + page.locator("#assurancePrevious").set_input_files(ROOT / "docs/demo/riverside-previous.xer") + page.locator("#assuranceCurrent").set_input_files(ROOT / "docs/demo/riverside-current.xer") + page.locator("#assuranceNarrative").fill((ROOT / "docs/demo/riverside-narrative.txt").read_text()) + page.get_by_role("button", name="Check decision readiness").click() + page.locator("#readinessWorkspace").wait_for(state="visible") + page.get_by_role("button", name="Record human decision").click() + page.get_by_label("Approve with conditions").check() + page.locator("#decisionOwner").fill("Review Chair") + page.locator("#decisionRationale").fill("Accept only with owned follow-up.") + page.locator("#decisionCondition").fill("Confirm schedule basis") + page.get_by_role("button", name="Save decision record").click() + assert "owner and a due date" in page.locator("#workspaceError").inner_text() + page.locator("#conditionOwner").fill("Planner") + page.locator("#conditionDue").fill("2026-11-01") + page.evaluate("() => { window.__originalSetItem = Storage.prototype.setItem; Storage.prototype.setItem = function() { throw new DOMException('Quota exceeded', 'QuotaExceededError'); }; }") + page.get_by_role("button", name="Save decision record").click() + assert "could not be saved" in page.locator("#workspaceError").inner_text() + assert page.locator("#decisionOwner").input_value() == "Review Chair" + assert page.evaluate("localStorage.getItem('projectlens:change-decisions:v1')") is None + page.evaluate("() => { Storage.prototype.setItem = window.__originalSetItem; }") + page.get_by_role("button", name="Save decision record").click() + page.wait_for_url("**#decisions") + page.locator("#decisionRegister .register-row").wait_for(state="visible") + assert page.locator("#decisionRegister .register-row").count() == 1 + page.close() + + with sync_playwright() as playwright: chromium = playwright.chromium.launch(headless=True) run_desktop(chromium) run_mobile(chromium) + run_recovery(chromium) chromium.close() print("Change assurance browser test passed: blockers, requests, decisions, conditions and mobile") diff --git a/README.md b/README.md index d3570fd..991a81f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ProjectLens: checks a change pack against its own evidence -For the project controls reviewer who has to tell a board whether a change pack can be trusted: ProjectLens compares the pack's narrative with its Primavera P6 schedules, risks and prior conditions, shows where they disagree, and records the human decision, all in the browser. +For the project controls reviewer who has to tell a board whether a change pack can be trusted: ProjectLens compares the pack's narrative with its previous and current Primavera P6 XER schedules, shows where they disagree, and records the human decision, all in the browser. [![Verify and deploy](https://github.com/MatthewPaver/ProjectLens/actions/workflows/deploy-pages.yml/badge.svg?branch=main&event=push)](https://github.com/MatthewPaver/ProjectLens/actions/workflows/deploy-pages.yml) [![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE) @@ -13,13 +13,13 @@ On the change-assurance page, select **Try the Northstar example**. It is a synt ## The problem -A board often decides on how polished the pack looks, because nobody has time to reconcile the narrative with the schedules, risks, actions and prior conditions submitted with it. The bundled Northstar example reproduces the failure. Its progress report says there is "no change to the finish date", but the current schedule has moved the finish by 73 days. The same pack also carries a high risk with no owner, an overdue action and an approval condition from the last board that is still open. If nobody checks the evidence against the story, the board approves the story. +A board often decides on how polished the pack looks, because nobody has time to reconcile the narrative with the schedules, risks, actions and prior conditions submitted with it. The bundled Northstar example reproduces the failure. Its progress report says there is "no change to the finish date", but the current schedule has moved the finish by 73 days. The change-assurance page checks schedule fields and narrative keywords; the separate board-readiness page covers risks, actions and earlier conditions. If nobody checks the evidence against the story, the board approves the story. ProjectLens gives the reviewer: -- **Source-linked conflicts and gaps.** Each finding names the evidence that produced it: current or previous schedule, risk register, commitments or narrative. +- **Source-linked conflicts and gaps.** Each finding names the evidence that produced it: current or previous schedule fields and the narrative under review. - **A prepared decision, not a dashboard.** One readiness verdict, at most three blockers, and specific questions for the team. -- **A record of the decision.** The decision, its owner, rationale and conditions are kept; each condition stays open until it is closed or formally waived. +- **A record of the decision.** The decision, its owner, rationale and conditions are kept; each condition stays open until it is marked closed. ## Quickstart @@ -32,15 +32,15 @@ python3 -m http.server 8000 --bind 127.0.0.1 --directory docs # open http://127.0.0.1:8000/change-assurance.html and select "Try the Northstar example" ``` -Expected result: the readiness headline reports the 73-day finish movement and the blocker list shows three items. Stop the server with Ctrl+C. +Expected result: the evidence desk shows three findings, including the 73-day finish movement, with source dates beside the selected finding. Stop the server with Ctrl+C. -**Your own schedules.** Export two single-project XER files from Primavera P6 (**File → Export → Primavera XER**): the comparison point and the latest submission. Select **Review my change pack**, choose both files and paste the narrative. Files are parsed in the browser and never uploaded. If you have no XER to hand, use the synthetic [Northstar](https://matthewpaver.github.io/ProjectLens/demo/northstar-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/northstar-current.xer)) or [Riverside](https://matthewpaver.github.io/ProjectLens/demo/riverside-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/riverside-current.xer), [narrative](https://matthewpaver.github.io/ProjectLens/demo/riverside-narrative.txt)) pairs. +**Your own schedules.** Export two single-project XER files from Primavera P6 (**File → Export → Primavera XER**): the comparison point and the latest submission. Select **Use my own XER files**, choose both files and paste the narrative. Files are parsed in the browser and never uploaded. If you have no XER to hand, use the synthetic [Northstar](https://matthewpaver.github.io/ProjectLens/demo/northstar-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/northstar-current.xer)) or [Riverside](https://matthewpaver.github.io/ProjectLens/demo/riverside-previous.xer) ([current](https://matthewpaver.github.io/ProjectLens/demo/riverside-current.xer), [narrative](https://matthewpaver.github.io/ProjectLens/demo/riverside-narrative.txt)) pairs. ## How it works ```mermaid flowchart LR - X[(Two XER exports
+ narrative, risks, conditions)] --> P[Parse XER in the browser] + X[(Two XER exports
+ narrative)] --> P[Parse XER in the browser] P --> R[Deterministic checks
finish movement vs narrative,
constraints, logic, float, ownership] R --> V[Readiness verdict
≤3 blockers + questions] V -. optional .-> S[Precedent sidecar
local FastAPI] @@ -49,7 +49,7 @@ flowchart LR C -- pass, brief shown --> U[Human Use / Ignore
on each precedent] C -- fail, brief dropped --> U V --> D[Decision register
owner, rationale, conditions] - U --> D + U -. optional context .-> D ``` - **Change assurance** (`docs/change-assurance.html`, `.js`): parses both XER files, runs the deterministic checks against the narrative and evidence, and keeps the decision and condition registers in the browser's local storage, with an export of the decision record. @@ -57,7 +57,15 @@ flowchart LR - **Public evidence** (`docs/index.html`, `Processing/gmpp_pipeline.py`): joins seven annual UK Government Major Projects Portfolio (GMPP) releases into one history of delivery-confidence ratings and end-date changes. Method: [`docs/method.md`](docs/method.md). - **Precedent sidecar** (`Processing/precedent_rag/`, optional, local): FastAPI plus LangGraph. It retrieves comparable public records with metadata filters and Gemini embeddings. An optional Gemini brief is shown only if every claim cites a retrieved record and it does not recommend approving or rejecting the pack (`summarize.py: evaluate_summary`). It receives narrative text and filters only, never the XER. Method: [`docs/precedent-rag.md`](docs/precedent-rag.md). -To run the sidecar, copy `.env.example` to `.env`, set `GEMINI_API_KEY` (and `LANGSMITH_API_KEY` for traces), then run `make precedent-rag` and set `window.PROJECTLENS_PRECEDENT_RAG_URL = "http://127.0.0.1:8787"` before `change-assurance.js` loads. Without it, the page shows three static precedent cards and the same human gate. +To run the sidecar, copy `.env.example` to `.env`, set `GEMINI_API_KEY` (and `LANGSMITH_API_KEY` for traces), then run `make precedent-rag` and set `window.PROJECTLENS_PRECEDENT_RAG_URL = "http://127.0.0.1:8787"` before `change-assurance.js` loads. Without it, the page shows three static precedent cards as optional illustrative context. Untouched cards are recorded as not reviewed; Use or Ignore is required only when you choose to attach or dismiss a record. + +## Review, resume and export + +The evidence desk puts three prioritised findings (with any additional findings disclosed and retained in the record) beside their source dates, activity movements and original narrative. The decision maker supplies a decision, owner and rationale. Conditional approval also needs a condition, owner and due date. Export preserves findings, the full SHA-256 input identity and optional context dispositions. Existing records without a hash remain readable and explicitly lack that identity. + +**Save draft on this device** deliberately stores both XER contents, narrative and decision fields in browser storage. Save again after further edits; use **Resume saved draft** after reopening, and **Delete saved draft** when finished. Anyone with access to this browser profile may access that stored data. Normal review processing uploads nothing. Browser storage is not a shared or tamper-proof audit system. + +Project names and source IDs must match. Identical evidence and reversed data dates are rejected. Missing activity finish or float fields produce an evidence gap. Renamed or re-imported projects need exports with consistent identity before comparison. The narrative checks are keyword heuristics, not semantic understanding; this is not critical-path, causation, entitlement or contractual approval analysis. Validate on representative real exports before operational use. ## Results diff --git a/docs/assets/change-assurance-overview.png b/docs/assets/change-assurance-overview.png index 46f893f..a8b7c0b 100644 Binary files a/docs/assets/change-assurance-overview.png and b/docs/assets/change-assurance-overview.png differ diff --git a/docs/change-assurance.css b/docs/change-assurance.css index 3dfc103..2b4fda5 100644 --- a/docs/change-assurance.css +++ b/docs/change-assurance.css @@ -286,3 +286,92 @@ button:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visib .decision-panel.is-gated-locked fieldset, .decision-panel.is-gated-locked .decision-fields { opacity: .45; pointer-events: none; } .decision-panel.is-gated-locked .assurance-primary { opacity: .35; cursor: not-allowed; } + +/* Evidence desk: compact operational hierarchy, preserving paper, ink and rust. */ +.has-review .assurance-hero, .has-review .simple-flow { display: none; } +.readiness-workspace { padding-bottom: 2rem; scroll-margin-top: 90px; } +.review-context { padding: 1rem 2rem; background: var(--assurance-ink); } +.review-context strong { font-size: 1rem; } +.review-context small { color: #c1d2cf; font-size: .75rem; line-height: 1.5; overflow-wrap: anywhere; } +.review-context > div > span { display: none; } +.review-tools { display: flex; flex-wrap: wrap; gap: .5rem; } +.review-context button { font: 500 .75rem/1.3 "Libre Franklin",sans-serif; text-transform: none; min-height: 40px; } +.readiness-card { margin: 1.5rem 2rem; padding: 1.5rem; color: var(--assurance-ink); background: var(--assurance-white); border: 1px solid var(--assurance-line); } +.readiness-card.ready { border-color: var(--assurance-muted); } +.readiness-card h2 { margin: .8rem 0 .5rem; font: 700 1.5rem/1.3 "Libre Franklin",sans-serif; letter-spacing: -.025em; text-wrap: initial; } +.readiness-card > p { margin: 0 0 1.25rem; color: var(--assurance-muted); font-size: .875rem; } +.readiness-status { padding: 0; color: var(--assurance-orange); background: none; font: 600 .75rem/1.4 "Libre Franklin",sans-serif; text-transform: none; } +.readiness-card.ready .readiness-status { color: #12675d; background: none; } +.evidence-desk { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); border-top: 1px solid var(--assurance-line); border-bottom: 1px solid var(--assurance-line); } +.evidence-desk > div { padding: 1rem 1.25rem 1rem 0; } +.evidence-desk h3 { margin: 0 0 .8rem; font: 600 .875rem/1.4 "Libre Franklin",sans-serif; } +.blocker-list { margin: 0; gap: .5rem; } +.blocker { display: block; padding: 0; background: none; border: 0; } +.finding-select { display: block; width: 100%; min-height: 100px; padding: .8rem; text-align: left; color: var(--assurance-ink); background: var(--assurance-white); border: 1px solid var(--assurance-line); cursor: pointer; } +.finding-select[aria-pressed=true] { background: #f1ecdf; border-color: var(--assurance-orange); } +.finding-select:hover { background: #f1ecdf; } +.finding-select > span { color: var(--assurance-orange); font: 600 .75rem/1.4 "Libre Franklin",sans-serif; } +.blocker strong { margin: .3rem 0; font-size: .875rem; line-height: 1.45; } +.blocker small { color: var(--assurance-muted); font-size: .75rem; } +.finding-evidence { min-width: 0; padding: 1rem 0 1rem 1.25rem; border-left: 1px solid var(--assurance-line); } +.finding-evidence p, .finding-evidence blockquote, .finding-evidence details { font-size: .8125rem; line-height: 1.55; } +.finding-evidence h4 { margin: 1.2rem 0 .4rem; font-size: .8125rem; } +.finding-evidence blockquote { margin: 0; max-height: 9rem; overflow-y: auto; white-space: pre-wrap; color: #40514e; } +.source-dates { display: grid; grid-template-columns: 1fr 1fr; margin: 1rem 0; gap: 1rem; } +.source-dates dt { color: var(--assurance-muted); font-size: .75rem; } +.source-dates dd { margin: .25rem 0 0; font-size: 1rem; font-weight: 600; } +.source-files, .source-boundary { color: var(--assurance-muted); overflow-wrap: anywhere; } +.finding-evidence summary { cursor: pointer; text-decoration: underline; text-underline-offset: 3px; } +.finding-evidence ul { max-height: 13rem; padding-left: 1.2rem; overflow-y: auto; } +.finding-evidence li { margin: .6rem 0; } +.finding-evidence li span { color: var(--assurance-muted); } +#backToFindings { display: none; } +.readiness-actions { margin-top: 1.25rem; } +.readiness-actions .assurance-primary { color: var(--assurance-white); background: var(--assurance-orange); border-color: var(--assurance-orange); } +.readiness-actions .assurance-primary:hover { background: #853b28; } +.readiness-actions .assurance-secondary { color: var(--assurance-ink); border-color: var(--assurance-line); } +.readiness-actions button, .decision-submit button { font: 600 .8125rem/1.4 "Libre Franklin",sans-serif; text-transform: none; min-width: 0; min-height: 44px; } +.decision-panel, .request-panel { padding: 2rem; scroll-margin-top: 88px; } +.decision-panel .compact-heading h2, .request-panel .compact-heading h2 { font: 700 1.5rem/1.3 "Libre Franklin",sans-serif; } +.decision-panel fieldset span, .decision-fields input, .decision-fields textarea, .decision-fields label span, .decision-submit p, .precedent-gate-banner { font-size: .8125rem; line-height: 1.5; text-transform: none; } +.decision-panel .compact-heading p { font-size: .875rem; } +.supporting-evidence { margin: 1.5rem 2rem; } +.supporting-evidence summary span, .supporting-evidence summary small { font-size: .75rem; line-height: 1.5; text-transform: none; } +.comparable-section h3 { font: 700 1.5rem/1.3 "Libre Franklin",sans-serif; } +.comparable-case p, .comparable-case small, .precedent-gate-copy, .precedent-reasons, .precedent-evidence { color: #d5ddd9 !important; font-size: .75rem !important; } +.comparable-case.gate-ignored { opacity: 1; } +.precedent-gate i { color: var(--assurance-sand); } +.workspace-notice { margin: 1rem 2rem; padding: 1rem; border: 1px solid var(--assurance-orange); font: 400 .875rem/1.5 "Libre Franklin",sans-serif; scroll-margin-top: 90px; } +.workspace-notice[hidden] { display: none; } +.workspace-notice button { margin: .5rem; padding: .65rem; background: var(--assurance-white); border: 1px solid var(--assurance-line); color: var(--assurance-ink); } +#workspaceError { background: #fff1e8; color: #853b28; } +::selection { color: var(--assurance-white); background: var(--assurance-orange); } +textarea, input { caret-color: var(--assurance-orange); } +@media (max-width: 760px) { + .review-context { padding: 1rem; align-items: start; flex-direction: column; gap: .75rem; } + .readiness-card { margin: 1rem; padding: 1rem; } + .evidence-desk { display: block; } + .evidence-desk > div { padding-right: 0; } + .finding-evidence { display: none; padding: 1rem 0; border-left: 0; } + .evidence-open > div { display: none; } + .evidence-open .finding-evidence { display: block; } + #backToFindings { display: inline-block; margin-bottom: 1rem; min-height: 44px; padding: .5rem .8rem; border: 1px solid var(--assurance-line); background: var(--assurance-paper); color: var(--assurance-ink); } + .decision-panel, .request-panel { padding: 1rem; } + .decision-panel fieldset { grid-template-columns: 1fr 1fr; } + .decision-fields { grid-template-columns: 1fr; } + .decision-fields label:nth-child(2) { grid-row: auto; } + .decision-submit { align-items: start; flex-direction: column; } + .readiness-actions { display: grid; } + .supporting-evidence, .workspace-notice { margin: 1rem; } + .supporting-evidence summary { flex-direction: column; gap: .3rem; } +} +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } +.readiness-actions { margin: 0 0 1rem; } +.finding-evidence blockquote { max-height: 5rem; } +.finding-evidence > details:last-of-type > p { white-space: pre-wrap; max-height: 16rem; overflow-y: auto; } +.review-context { position: static; } +.finding-select { min-height: 82px; } +.blocker small { display: none; } +.assurance-header nav a, .assurance-primary, .assurance-secondary, .assurance-file small, .compact-heading p, .register-row small, .register-row > div > span, .register-row > aside small, .evidence-fact small, .evidence-fact span, .request-item small, .assurance-toast, .precedent-gate-note, .trust-line { font-size: .75rem; } +#blockerList > details { font-size: .8125rem; } +#blockerList > details > summary { padding: .6rem 0; cursor: pointer; } diff --git a/docs/change-assurance.html b/docs/change-assurance.html index 8df4d57..d9e74d5 100644 --- a/docs/change-assurance.html +++ b/docs/change-assurance.html @@ -39,6 +39,12 @@
+ +
@@ -67,7 +73,7 @@

Check the change pack
before you decide.

2Review blockers
-
3Cite precedents
+
3Check source evidence
4Record decision
@@ -105,22 +111,26 @@

Check the change pack
before you decide.

-