From c5fbb4303d949df3b4559fcc9194da1b854e822b Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 14:50:01 +0800 Subject: [PATCH 01/15] docs(burndown): record the captain's 2026-09-17 rulings for the UI/UX milestone Group the milestone's PRs by dependency tier rather than one per issue, since every issue touches the single-in-flight web/ surface, and record the standing conn for its triage and merge gates. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- docs/roadmap-burndown/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/roadmap-burndown/README.md b/docs/roadmap-burndown/README.md index 199c7e0d3..6709a9910 100644 --- a/docs/roadmap-burndown/README.md +++ b/docs/roadmap-burndown/README.md @@ -783,6 +783,25 @@ this workflow and override the defaults above wherever the two differ. happens without a confirmation round. Ask only for choices that are hard to reverse or genuinely the captain's to make. "I want to get things done." +### Given 2026-09-17, for the Clean and Cogent UI/UX milestone + +Two rulings, in the captain's own words, scoped to this milestone's burndown. + +- **Group the PRs by dependency tier, not one per issue.** All twelve issues in the milestone touch + `cargento_runtime/web/`, and exactly one in-flight PR may. The `burndown` skill already provides + for it — "the second issue rides that PR or waits for it" — so a tier lands as one branch carrying + several issues. **One-issue-per-branch above is suspended for this milestone only**, and the cost + it was buying is paid another way: the post-merge Linear reconcile still runs **once per issue**, + and the PR body carries one `Implements [DRC-####](url)` line per issue it closes. A tier is the + merge-risk unit; an issue remains the reconcile unit. +- **Standing conn for this milestone: "I pre-approve all the triage and merge gates, just automate + this entire process and do it."** The first officer renders triage and review gate decisions + itself, recorded `agent:first-officer` with the grant quoted, and drives to terminal without + stopping. It still stops for anything only the captain may change: approved scope, accepted value, + thresholds, tolerance, or acceptance criteria — item 5 of `## Review-finding disposition` is not + delegated by this grant. The `pr-merge` merge hook's push approval is covered, because the grant + names the merge gates and says to do it; the draft is still presented before the push. + ## Workflow State View the workflow overview: From f83dd5f572c780d12eae5c5b6115082e58301b0c Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 14:58:00 +0800 Subject: [PATCH 02/15] chore(burndown): raise triage concurrency to 6 for the UI/UX milestone The milestone's twelve issues triage in PR groups, and the default of two serialised a stage that holds no worktree and takes no lock. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- docs/roadmap-burndown/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/roadmap-burndown/README.md b/docs/roadmap-burndown/README.md index 6709a9910..ce5cbdb50 100644 --- a/docs/roadmap-burndown/README.md +++ b/docs/roadmap-burndown/README.md @@ -17,6 +17,7 @@ stages: - name: triage gate: true model: opus + concurrency: 6 - name: implementation worktree: true model: opus From 81351e07f80bd35f4065227a54692f81278ecc58 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 15:07:55 +0800 Subject: [PATCH 03/15] docs(burndown): pin the triage acceptance heading that --ac-scan matches Two triages drew the heading from the Linear issue's own wording, which left the gate's structured acceptance read erroring, and the error surfaces only when the first officer runs the read. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- docs/roadmap-burndown/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/roadmap-burndown/README.md b/docs/roadmap-burndown/README.md index ce5cbdb50..61832a8da 100644 --- a/docs/roadmap-burndown/README.md +++ b/docs/roadmap-burndown/README.md @@ -224,6 +224,15 @@ the only stage whose product is a change to the roadmap records rather than to t clauses, each split as **offline** (a test, command, or on-disk state a fresh agent reproduces) or **interactive** (needs a human or a live drive). The split is declared here, at the gate, so a plan to build a harness that automates an interactive AC is visible before the harness exists. + **The heading is exactly `## Acceptance criteria`, and that is machine-read rather than + stylistic.** `status --read {slug} --ac-scan` matches that string literally: `## Acceptance` + and `## Acceptance criteria, with verification` both return `Error: no ## Acceptance criteria + section in this file`, which leaves the triage gate and every later review gate without their + structured acceptance read — and the error appears only when the FO runs the read, not when + the criteria are written. Measured 2026-09-17 on DRC-4587 and DRC-4588, which drew the heading + from the Linear issue's own `## Acceptance` section and each cost a repair round. The captured + original under `## Linear edits made` keeps whatever heading Linear holds: it is a verbatim + record, so it is exempt, and renaming it would falsify the restore point. - At least one acceptance criterion that is a property a user can see, with its own `Verified by:` clause. When the move is `none`, one sentence in the brief on why no user sees this change instead, and the gate is told so up front. From 746946fb18291f9783a766483323a8ea7c8ccaed Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 15:09:51 +0800 Subject: [PATCH 04/15] docs(burndown): pin the AC bullet shape --ac-scan resolves A correct heading over numbered items scans as an empty list rather than an error, so the quieter half of the failure reads as having no criteria at all. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- docs/roadmap-burndown/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/roadmap-burndown/README.md b/docs/roadmap-burndown/README.md index 61832a8da..eea3091f9 100644 --- a/docs/roadmap-burndown/README.md +++ b/docs/roadmap-burndown/README.md @@ -233,6 +233,19 @@ the only stage whose product is a change to the roadmap records rather than to t from the Linear issue's own `## Acceptance` section and each cost a repair round. The captured original under `## Linear edits made` keeps whatever heading Linear holds: it is a verbatim record, so it is exempt, and renaming it would falsify the restore point. + **And the criteria inside it are a bullet list with a bold `AC-N` label**, because the heading + alone is not enough: a correct heading over numbered `1. **(offline)**` items scans as + `{"acs":[]}`, which reads as "no criteria" rather than as an error and is the quieter half of + the same failure. The shape the scanner resolves, confirmed against archived `drc-4020`: + + ```text + - **AC-1 — offline:** {end-state property}. **Verified by:** {command, test or on-disk state}. + **Falsified by:** {the concrete change that flips it}. + ``` + + Citations resolve from later stage reports that name `AC-N`, so criteria authored here are + expected to scan as unevidenced at this gate. That is correct, not a defect — `implementation` + and `review` supply the evidence, and the cross-check earns its keep at the review gate. - At least one acceptance criterion that is a property a user can see, with its own `Verified by:` clause. When the move is `none`, one sentence in the brief on why no user sees this change instead, and the gate is told so up front. From 192caf818ca2720ed1e463bf0d9c70ac25f08afc Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 15:11:36 +0800 Subject: [PATCH 05/15] docs(burndown): correct the AC scan rule to the hyphenated id The bullet shape pinned in the previous commit was not the cause; an isolated probe of four criterion shapes shows the scanner keys on the hyphen in AC-N and ignores everything else about the line. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- docs/roadmap-burndown/README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/roadmap-burndown/README.md b/docs/roadmap-burndown/README.md index eea3091f9..5c07759cb 100644 --- a/docs/roadmap-burndown/README.md +++ b/docs/roadmap-burndown/README.md @@ -233,16 +233,24 @@ the only stage whose product is a change to the roadmap records rather than to t from the Linear issue's own `## Acceptance` section and each cost a repair round. The captured original under `## Linear edits made` keeps whatever heading Linear holds: it is a verbatim record, so it is exempt, and renaming it would falsify the restore point. - **And the criteria inside it are a bullet list with a bold `AC-N` label**, because the heading - alone is not enough: a correct heading over numbered `1. **(offline)**` items scans as - `{"acs":[]}`, which reads as "no criteria" rather than as an error and is the quieter half of - the same failure. The shape the scanner resolves, confirmed against archived `drc-4020`: + **And each criterion's id must be hyphenated — `AC-1`, never `AC1`** — because the heading + alone is not enough: a correct heading over criteria the scanner cannot see returns + `{"acs":[]}`, which reads as "this entity has no acceptance criteria" rather than as an error, + and is the quieter and more dangerous half of the same failure. Nothing else about the shape + matters. Measured 2026-09-17 in an isolated throwaway workflow carrying four criterion shapes + in one file: ```text - - **AC-1 — offline:** {end-state property}. **Verified by:** {command, test or on-disk state}. - **Falsified by:** {the concrete change that flips it}. + - **AC-1 — offline:** …bullet, hyphenated… -> parsed + **AC-3 — paragraph, no bullet.** (offline) -> parsed + **AC4 — paragraph, unhyphenated.** -> NOT parsed ``` + So bullet-versus-paragraph is free, `Verified by:` styling is free, and the hyphen is not. Two + earlier guesses at this — first the heading, then a required bullet list — were both wrong and + each cost a repair round; the isolated probe settled it in one. **Probe it there, never in the + state checkout**, which has concurrent writers. + Citations resolve from later stage reports that name `AC-N`, so criteria authored here are expected to scan as unevidenced at this gate. That is correct, not a defect — `implementation` and `review` supply the evidence, and the cross-check earns its keep at the review gate. From aafa32d7addfaae4cf435acf8fa3954d383ee4f7 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 15:12:39 +0800 Subject: [PATCH 06/15] docs(burndown): add the wrapped-bold-label case to the AC scan rule A bold label that wraps before its closing marker is skipped, so a hard-wrapped file drops long criteria and returns a partial scan that looks complete. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- docs/roadmap-burndown/README.md | 35 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/roadmap-burndown/README.md b/docs/roadmap-burndown/README.md index 5c07759cb..a1893155f 100644 --- a/docs/roadmap-burndown/README.md +++ b/docs/roadmap-burndown/README.md @@ -233,23 +233,32 @@ the only stage whose product is a change to the roadmap records rather than to t from the Linear issue's own `## Acceptance` section and each cost a repair round. The captured original under `## Linear edits made` keeps whatever heading Linear holds: it is a verbatim record, so it is exempt, and renaming it would falsify the restore point. - **And each criterion's id must be hyphenated — `AC-1`, never `AC1`** — because the heading - alone is not enough: a correct heading over criteria the scanner cannot see returns - `{"acs":[]}`, which reads as "this entity has no acceptance criteria" rather than as an error, - and is the quieter and more dangerous half of the same failure. Nothing else about the shape - matters. Measured 2026-09-17 in an isolated throwaway workflow carrying four criterion shapes - in one file: + **Two further things must hold, and both fail silently.** The heading alone is not enough: a + correct heading over criteria the scanner cannot see returns `{"acs":[]}`, which reads as "this + entity has no acceptance criteria" rather than as an error. Worse, a criterion list where only + some items parse returns a partial scan that looks like a complete one. + + 1. **The id is hyphenated** — `AC-1`, never `AC1`. + 2. **The bold label opens and closes on the same line.** A label whose bold run wraps before + its closing marker is skipped, so in a hard-wrapped file a long property silently drops its + own criterion. + + Write the bullet form, which satisfies both by construction — the bold closes right after the + mark, so the property may wrap freely: ```text - - **AC-1 — offline:** …bullet, hyphenated… -> parsed - **AC-3 — paragraph, no bullet.** (offline) -> parsed - **AC4 — paragraph, unhyphenated.** -> NOT parsed + - **AC-1 — offline:** {end-state property, wraps freely}. **Verified by:** {command, test or + on-disk state, and what it returns today}. **Falsified by:** {the change that flips it}. ``` - So bullet-versus-paragraph is free, `Verified by:` styling is free, and the hyphen is not. Two - earlier guesses at this — first the heading, then a required bullet list — were both wrong and - each cost a repair round; the isolated probe settled it in one. **Probe it there, never in the - state checkout**, which has concurrent writers. + Measured 2026-09-17 in an isolated throwaway workflow, one file per run: unhyphenated `AC4` is + skipped; a bold label wrapped across a newline is skipped; a bullet whose bold closes after the + mark parses however far the property wraps. The live cost was three repair rounds across + DRC-4587 and DRC-4588, two of them spent on first officer guesses — the heading, then a + supposedly-required bullet list — made before anything was measured. The partial-scan state was + found by the DRC-4587 ensign, whose paragraph rewrite resolved exactly one of seven criteria: + the only one short enough to close its bold on a single line. **Probe shapes in a throwaway + workflow, never in the state checkout**, which has concurrent writers. Citations resolve from later stage reports that name `AC-N`, so criteria authored here are expected to scan as unevidenced at this gate. That is correct, not a defect — `implementation` From f6ced2f86449a757f8093bfc49666e7c8052007e Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 17:24:12 +0800 Subject: [PATCH 07/15] feat(web): raise board sentences to a 15px tier and collapse the label tier Board prose rendered at 12.5px, where the audit put even --ink, the brightest colour in the palette, below the body-text requirement. There was no colour left to spend, so size was the only lever. - Add --fs-sentence:15px and --measure:540px; repoint 65 sans prose rules to weight 500 / line-height 1.55, 47 of them capped at --measure. The cap skips rules carrying overflow-wrap:anywhere and the two prose containers, where 540px would clamp the cards rather than the sentences. - Collapse the label tier onto --fs-label:11px: delete --fs-column, --fs-meta, --fs-breadcrumb (all three had zero callers) and --fs-meta-detail, remap 27 literal font-size:10px sites, 4 font: shorthands and 6 sub-10px literals, and normalise 24 tracking declarations onto .09em. Three declarations in the .1em and .08em groups kept their own value because their content is not uppercase. - Move .next-cockpit-held-revision and .next-cockpit-landed-axes out of their
s. Both are absence explanations, which design-next-ui.md already ruled are sentences; neither could reach the sentence tier inside an

's header without dragging the label with it. - Delete the "see ruling R9" comment with the 9px token it defended. No R9 exists under docs/. - Regenerate all nine frontend byte pins from the assets in one pass. docs/design-next-ui.md records which selectors are sentences, so a reviewer argues with a list rather than re-deriving one from the cascade. It also records the gap this change does not close: 17 further sans rules pass the same test at 13px to 14.5px and were left, because two of them are not sentences and the set needs reading one selector at a time. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- CONTRIBUTING.md | 4 +- .../cargento_runtime/web/next-cockpit.js | 8 +- .../cargento/cargento_runtime/web/styles.css | 248 +++++++++--------- cargento/skills/cargento/tests/test_focus.py | 2 +- .../cargento/tests/test_next_cockpit.py | 43 +++ .../skills/cargento/tests/test_next_flag.py | 4 +- .../skills/cargento/tests/test_next_page.py | 10 +- docs/design-next-ui.md | 40 ++- 8 files changed, 218 insertions(+), 141 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3966ccfe8..33b6002a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -260,8 +260,8 @@ allowlist changes only in a PR that makes a reviewed ownership decision. the promotion decision and route grammar. - Keep stylesheet edits inside the region owned by the surface you are changing, including its media queries. [The stylesheet contract](docs/design-next-ui.md#nui-2-one-stylesheet-owns-the-interface) - names all nine regions, the dark-only palette and the type floor. Board sentences use at least - `--fs-xs` (12.5px); compact labels and source metadata retain their smaller design sizes. + names all nine regions, the dark-only palette and the type floor. Board sentences use + `--fs-sentence` (15px); compact labels and source metadata sit on one tier, `--fs-label` (11px). The stylesheet contains both scale tokens and literal sizes; no test currently bans all raw pixel sizes or unused scale steps. The earlier scale-only rule followed twenty ad-hoc values between 8px and 15px; new sizes still need a named role. diff --git a/cargento/skills/cargento/cargento_runtime/web/next-cockpit.js b/cargento/skills/cargento/cargento_runtime/web/next-cockpit.js index 4ba9d8442..db28cbf3a 100644 --- a/cargento/skills/cargento/cargento_runtime/web/next-cockpit.js +++ b/cargento/skills/cargento/cargento_runtime/web/next-cockpit.js @@ -2150,8 +2150,8 @@ function nextCockpitLanded(observed){ `` + `${esc(text)}` + (note ? `${esc(note)}` : "") + ''; - return '

HOW IT LANDED

' + - 'two axes, read separately
' + + return '

HOW IT LANDED

' + + 'two axes, read separately' + '
' + card("END EVIDENCE", landing.endText, landing.endKnown, "") + card("WHO CLAIMS IT FINISHED", landing.claimText, landing.claimKnown, @@ -2408,8 +2408,8 @@ function nextCockpitHeldTo(group, observation){ harness and the session id are what the store keys on, so they are what is shown. */ return '

WHAT YOU ASKED FOR

' + - `${esc(sessKey(session))}` + - `${esc(revision)}
` + + `${esc(sessKey(session))}

` + + `${esc(revision)}` + '
' + NEXT_COCKPIT_HELD_FIELDS.map(spec => nextCockpitHeldField(session, annotation, spec, cap)).join("") + '
' + diff --git a/cargento/skills/cargento/cargento_runtime/web/styles.css b/cargento/skills/cargento/cargento_runtime/web/styles.css index 28fe13b91..679d11f2e 100644 --- a/cargento/skills/cargento/cargento_runtime/web/styles.css +++ b/cargento/skills/cargento/cargento_runtime/web/styles.css @@ -16,12 +16,20 @@ --fs-2xs:11.5px; --fs-xs:12.5px; --fs-sm:14px; --fs-md:15.5px; --fs-lg:17px; --fs-xl:19px; --fs-2xl:23px; --fs-num:28px; --fs-brand:38px; --fs-hero:52px; /* Additional steps come from Cargento-v2.dc.html, not a re-derived scale. */ - /* 9px is the operations table's column header, the smallest label the - prototype draws. It is a label, never a sentence: see ruling R9. */ - --fs-column:9px; --fs-label:9.5px; --fs-meta:10px; --fs-meta-detail:10.5px; --fs-machine:11px; - --fs-breadcrumb:12px; --fs-body:13px; --fs-summary:13.5px; --fs-lead:14.5px; + /* One label tier and one sentence tier. Four of the six sub-12px steps sat + inside a 3px band no reader can rank, and three of them had no callers at + all. 15px rather than a brighter ink because the audit put --ink, the + brightest colour here, below the body-text requirement at 12.5px: the + colour had already been spent. Which selectors are sentences is a list in + docs/design-next-ui.md, so a reviewer argues with it rather than + re-deriving it from the cascade. */ + --fs-label:11px; --fs-machine:11px; --fs-sentence:15px; + --fs-body:13px; --fs-summary:13.5px; --fs-lead:14.5px; --fs-section:15px; --fs-subheading:16px; --fs-project-title:22px; --fs-counter:27px; --fs-figure:32px; + /* 72 characters at the sentence tier. Sans prose only: a machine string a + source published wraps anywhere instead, and keeps the full column. */ + --measure:540px; --sel-bg:color-mix(in oklab,var(--accent) 26%,transparent); --sel-bd:color-mix(in oklab,var(--accent) 52%,transparent); --bg:#14140f; --panel:#1c1c16; --sunk:#11110c; --line:#2c2c23; --line2:#403f33; @@ -77,7 +85,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-menu button{min-block-size:44px;display:flex;justify-content:space-between;width:100%;padding:8px;text-align:left} .next-header nav[aria-label="Primary"] a:focus-visible,.next-menu summary:focus-visible,.next-menu button:focus-visible{outline:2px solid var(--ink);outline-offset:3px} .next-menu kbd{font-family:var(--mono);color:var(--ink3)} -.next-stalled{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:18px;padding:10px 12px;border-left:3px solid var(--clay);background:var(--panel);color:var(--ink2);font-size:var(--fs-xs);line-height:1.45} +.next-stalled{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:18px;padding:10px 12px;border-left:3px solid var(--clay);background:var(--panel);color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55} .next-stalled strong{color:var(--ink);font-weight:500} .next-stalled[data-next-state="history-reset"]{border-left-color:var(--ink3)} .next-stalled button{margin-left:auto;padding:5px 9px;border:1px solid var(--line2);border-radius:3px;background:transparent;color:var(--ink);font:inherit;cursor:pointer} @@ -152,12 +160,12 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-project-scope{overflow-wrap:anywhere;font-size:var(--fs-xs)} .next-project-summary{display:flex;flex-wrap:wrap;gap:4px 10px} .next-project-summary,.next-project-detail-count{color:var(--ink2);font-size:var(--fs-xs)} -.next-project-collision,.next-project-detail-collision{margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-project-collision,.next-project-detail-collision{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-project-sessions{min-width:0} .next-project-session{display:grid;grid-template-columns:6px 64px minmax(80px,1fr) minmax(110px,172px) minmax(110px,150px);align-items:center;gap:9px;width:100%;padding:7px 0;border:0;border-bottom:1px solid var(--line);border-radius:0;background:transparent;color:var(--ink);font:inherit;text-align:left;cursor:pointer} .next-project-session:last-child{border-bottom:0} .next-project-session:hover .next-project-session-title{color:var(--ink);text-decoration:underline} -.next-project-session-harness{color:var(--ink3);font-family:var(--mono);font-size:10px;overflow-wrap:anywhere} +.next-project-session-harness{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);overflow-wrap:anywhere} .next-project-session-title{min-width:0;overflow-wrap:anywhere;font-family:var(--sans);font-size:var(--fs-xs)} .next-project-session-now,.next-project-session-next{min-width:0;overflow-wrap:anywhere;font-size:var(--fs-xs)} .next-project-dot{display:inline-block;flex:none;width:6px;height:6px;border:1px solid var(--project-tone);border-radius:50%;background:var(--project-tone)} @@ -176,14 +184,14 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-project-detail-count,.next-project-detail-collision{flex-basis:100%;margin:0} .next-project-goal{display:flex;flex-direction:column;gap:9px;padding:15px 17px;border:1px solid var(--line);border-radius:10px;background:var(--panel)} .next-project-goal>header{display:flex;justify-content:space-between;align-items:baseline;gap:10px} -.next-project-goal h2,.next-project-activity>h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em} -.next-project-goal-source{color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-project-goal h2,.next-project-activity>h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} +.next-project-goal-source{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-project-goal-text{max-width:640px;font-size:14.5px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere} .next-project-goal-text.next-project-value--known{color:var(--ink)} -.next-project-goal-gap{margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-project-goal-gap{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-project-goal-row{display:flex;flex-direction:column;gap:5px;padding-left:9px;border-left:1px solid var(--line2)} .next-project-goal-row+.next-project-goal-row{margin-top:2px} -.next-project-goal-tag{color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.13em} +.next-project-goal-tag{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} .next-project-activity-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px;align-items:start} .next-project-activity{min-width:0} .next-project-activity>h2{margin-bottom:11px} @@ -197,12 +205,12 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-activity-title>.next-project-value--known,.next-project-ending-title>.next-project-value--known{color:var(--ink)} .next-activity-harness{color:var(--ink3);font-family:var(--mono);font-size:11px} .next-activity-stuck{color:var(--ink2);font-size:var(--fs-xs)} -.next-activity-question{padding-top:7px;border-top:1px solid var(--line);color:var(--ink);font-size:var(--fs-xs);line-height:1.5;white-space:pre-wrap} +.next-activity-question{padding-top:7px;border-top:1px solid var(--line);color:var(--ink);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;white-space:pre-wrap;max-width:var(--measure)} .next-activity-metric{font-size:var(--fs-xs)} .next-activity-instruction{font-size:var(--fs-xs);color:var(--ink2)} .next-project-ending-glyph{color:var(--project-tone)} .next-project-ending-outcome.next-project-value--known{color:var(--ink2)} -.next-project-ending-outcome{font-family:var(--sans);font-size:var(--fs-xs);line-height:1.5} +.next-project-ending-outcome{font-family:var(--sans);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-project-detail-status{margin:0 0 12px;color:var(--ink3);font-size:var(--fs-xs)} .next-project-progress-bar{display:block;width:120px;height:4px;margin:5px 0 9px;accent-color:var(--accent)} .next-project-plan{margin-bottom:30px;border-top:1px solid var(--line2)} @@ -247,7 +255,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-workstream-empty{min-height:42px;margin:0;padding:12px 10px;border-bottom:1px solid var(--line);color:var(--ink2);font-size:var(--fs-xs)} .next-project-detail-empty{padding:24px;border:1px solid var(--line);color:var(--ink2);font-size:var(--fs-sm)} -.next-workstream-header button{gap:12px;align-items:baseline;padding:0 0 11px;font-size:10px;letter-spacing:.1em} +.next-workstream-header button{gap:12px;align-items:baseline;padding:0 0 11px;font-size:var(--fs-label);letter-spacing:.09em} .next-workstream-header small{color:var(--ink3);font-family:var(--sans);font-size:var(--fs-xs);letter-spacing:normal;text-align:right} .next-project-change{display:grid;grid-template-columns:52px 16px minmax(0,1fr) 90px;gap:12px;align-items:center;min-height:30px;padding:6px 0;border-bottom:1px solid var(--line);color:var(--ink2);font-size:var(--fs-xs)} .next-project-change time,.next-project-change-harness{color:var(--ink3);font-family:var(--mono);font-size:10.5px} @@ -286,14 +294,14 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-rail-panel:first-child{padding-top:20px} .next-rail-panel:last-child{border-bottom:0} .next-rail-header{display:flex;align-items:baseline;gap:9px} -.next-rail-header h2{margin:0;color:var(--ink3);font:700 9.5px var(--mono);letter-spacing:.14em} -.next-rail-meta{margin-left:auto;color:var(--ink3);font:10px var(--mono);text-align:right} +.next-rail-header h2{margin:0;color:var(--ink3);font:700 var(--fs-label) var(--mono);letter-spacing:.09em} +.next-rail-meta{margin-left:auto;color:var(--ink3);font:var(--fs-label) var(--mono);text-align:right} .next-rail-meta--sentence{font:var(--fs-xs) var(--sans)} .next-rail-meta--amber{color:var(--amber)} -.next-project-detail-rail .next-rail-reason{margin:0;color:var(--ink3);font-family:var(--sans);font-size:var(--fs-xs);line-height:1.5} +.next-project-detail-rail .next-rail-reason{margin:0;color:var(--ink3);font-family:var(--sans);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-delegation-figure{display:flex;gap:9px;align-items:baseline} .next-delegation-figure>strong{color:var(--accent);font:700 32px/1 var(--mono);letter-spacing:-.02em} -.next-delegation-caption{margin:0;color:var(--ink2);font-size:var(--fs-xs);line-height:1.35} +.next-delegation-caption{margin:0;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-delegation progress{display:block;width:100%;height:5px;border:0;border-radius:3px;overflow:hidden;background:var(--line);accent-color:var(--accent-dim)} .next-delegation progress::-webkit-progress-bar{background:var(--line)} .next-delegation progress::-webkit-progress-value{background:var(--accent-dim)} @@ -301,16 +309,16 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-delegation-metrics{display:flex;flex-wrap:wrap;gap:4px 9px;color:var(--ink2);font:10.5px/1.5 var(--mono)} .next-delegation-withheld{display:flex;flex-direction:column;gap:6px;color:var(--ink2)} .next-delegation-withheld strong{font:700 16px var(--mono)} -.next-delegation-withheld small{color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-delegation-withheld small{color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-delegation-trend{color:var(--ink3);font:var(--fs-xs) var(--mono)} .next-rail-wait{display:flex;flex-direction:column;gap:8px;padding:12px 13px;border:1px solid var(--line);border-left:2px solid var(--amber);border-radius:9px;background:var(--panel)} .next-rail-wait-heading{display:flex;align-items:baseline;gap:9px} -.next-rail-wait-heading a{min-width:0;color:var(--ink);font-size:var(--fs-xs);line-height:1.4;overflow-wrap:anywhere;text-decoration:none} +.next-rail-wait-heading a{min-width:0;color:var(--ink);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;overflow-wrap:anywhere;text-decoration:none} .next-rail-wait-heading a:hover{text-decoration:underline} .next-rail-wait-duration{margin-left:auto;flex-shrink:0;color:var(--amber);font:700 11px var(--mono)} -.next-rail-question{margin:0;color:var(--ink2);font-size:var(--fs-xs);line-height:1.5;overflow-wrap:anywhere} +.next-rail-question{margin:0;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;overflow-wrap:anywhere} .next-rail-wait-controls{display:flex;flex-wrap:wrap;gap:7px;padding-top:2px} -.next-rail-wait-controls button{margin-top:0;padding:5px 10px;border-radius:6px;font-size:10px} +.next-rail-wait-controls button{margin-top:0;padding:5px 10px;border-radius:6px;font-size:var(--fs-label)} .next-rail-capacity-window{display:flex;flex-direction:column;gap:6px} .next-rail-capacity-heading{display:flex;align-items:baseline;gap:8px;color:var(--ink3);font:11px var(--mono)} .next-rail-capacity-heading b,.next-rail-capacity-heading strong{color:var(--ink)} @@ -323,16 +331,16 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-rail-capacity-fill--amber{background:var(--amber)} .next-rail-capacity-fill--unknown{background:var(--ink3)} .next-rail-capacity-tick{position:absolute;top:-2px;bottom:-2px;width:1.5px;background:var(--ink2)} -.next-rail-capacity-caption{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;color:var(--ink3);font:10px var(--mono)} +.next-rail-capacity-caption{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;color:var(--ink3);font:var(--fs-label) var(--mono)} .next-project-detail-rail .next-usage-consent{margin:0;padding:12px;font-size:var(--fs-xs)} .next-project-detail-rail .next-usage-consent-note{font-family:var(--sans);font-size:var(--fs-xs)} .next-project-detail-rail .next-usage-switch{margin:0;font-size:var(--fs-xs)} -.next-control>header{color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-control>header{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-steer{padding-top:12px;border-top:1px solid var(--line)} .next-steer form{display:flex;gap:7px;padding:12px 0} .next-steer label,.next-guardrail-add-input label{flex:1;min-width:0} .next-steer input,.next-guardrail-add-input input{width:100%;min-width:0;padding:8px;border:1px solid var(--line2);border-radius:6px;background:var(--panel);color:var(--ink);font:var(--fs-xs) var(--sans)} -.next-steer button,.next-guardrail-add,.next-guardrail-add-input button{padding:5px 8px;border:1px solid var(--line2);border-radius:6px;background:transparent;color:var(--ink2);font:10px var(--mono);cursor:pointer} +.next-steer button,.next-guardrail-add,.next-guardrail-add-input button{padding:5px 8px;border:1px solid var(--line2);border-radius:6px;background:transparent;color:var(--ink2);font:var(--fs-label) var(--mono);cursor:pointer} .next-guardrail-add{align-self:flex-start;padding:0;border:0;color:var(--ink3);font-size:10.5px} .next-steer button:hover,.next-guardrail-add-input button:hover{border-color:var(--accent)} .next-guardrail-add:hover{color:var(--ink)} @@ -343,9 +351,9 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-guardrail-row{display:grid;grid-template-columns:10px minmax(0,1fr);gap:10px;width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:9px;background:transparent;color:var(--ink2);font:inherit;text-align:left;cursor:pointer} .next-guardrail-row:hover{border-color:var(--line2)} .next-guardrail-row[aria-checked="false"]{color:var(--ink3)} -.next-guardrail-glyph{margin-top:2px;color:var(--ink3);font:10px var(--mono)} -.next-guardrail-copy strong,.next-guardrail-copy small{display:block;overflow-wrap:anywhere;font-size:var(--fs-xs);font-weight:400;line-height:1.45} -.next-guardrail-copy small,.next-guardrail-empty{margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-guardrail-glyph{margin-top:2px;color:var(--ink3);font:var(--fs-label) var(--mono)} +.next-guardrail-copy strong,.next-guardrail-copy small{display:block;overflow-wrap:anywhere;font-size:var(--fs-sentence);font-weight:500;line-height:1.55} +.next-guardrail-copy small,.next-guardrail-empty{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-guardrail-add-input{display:flex;align-items:center;gap:9px} .next-guardrail-add-input button{flex-shrink:0;color:var(--ink)} .next-project-detail-rail :is(a,button,input):focus-visible{outline:2px solid var(--accent);outline-offset:3px} @@ -356,23 +364,23 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o /* ===== SESSIONS ===== */ .next-operations{min-width:0;padding:26px 0 34px} .next-operations-header{display:grid;grid-template-columns:132px minmax(0,1fr);gap:5px 16px;align-items:baseline} -.next-operations-header>span{grid-row:1 / span 2;color:var(--accent-dim);font-family:var(--mono);font-size:9.5px;font-weight:700;letter-spacing:.14em} +.next-operations-header>span{grid-row:1 / span 2;color:var(--accent-dim);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} .next-operations-header h1{margin:0;font-size:23px;font-weight:700;letter-spacing:-.02em} .next-operations-header p{margin:0;color:var(--ink2);font-size:13.5px;line-height:1.5} .next-operations-fleet{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:24px;border-top:1px solid var(--line);border-bottom:1px solid var(--line)} .next-operations-fleet>section{min-width:0;padding:15px 18px} .next-operations-fleet>section+section{border-left:1px solid var(--line)} .next-operations-fleet span,.next-operations-fleet strong,.next-operations-fleet small{display:block} -.next-operations-fleet span{color:var(--ink3);font-family:var(--mono);font-size:9.5px} +.next-operations-fleet span{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-operations-fleet strong{margin-top:7px;font-family:var(--mono);font-size:27px;font-weight:400;line-height:1} -.next-operations-fleet small{margin-top:7px;color:var(--ink3);font-size:var(--fs-xs);line-height:1.4} +.next-operations-fleet small{margin-top:7px;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-operation-group{margin-top:30px} .next-operation-group--history{margin-top:44px} .next-operation-group>header{display:flex;justify-content:space-between;gap:24px;align-items:baseline;padding-bottom:10px;border-bottom:1px solid var(--line2)} .next-operation-group>header h2{margin:0;font-size:16px;font-weight:600} -.next-operation-group>header p{max-width:620px;margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.45;text-align:right} +.next-operation-group>header p{max-width:620px;margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;text-align:right} .next-operations-columns,#app .next-operation-row{grid-template-columns:minmax(0,1.5fr) 190px minmax(0,1fr) minmax(0,1fr) 170px} -.next-operations-columns{display:grid;border-bottom:1px solid var(--line2);color:var(--ink3);font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:.13em} +.next-operations-columns{display:grid;border-bottom:1px solid var(--line2);color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} .next-operations-columns span{padding:9px 12px} #app .next-operation-row{display:grid;grid-template-columns:minmax(0,1.5fr) 190px minmax(0,1fr) minmax(0,1fr) 170px;position:relative;width:100%;min-width:0;border-bottom:1px solid var(--line);color:var(--ink);text-decoration:none;cursor:pointer} .next-operation-row:hover{background:var(--panel)} @@ -389,8 +397,8 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-operation-identity strong,.next-operation-fact strong{margin-top:7px;font-size:var(--fs-sm);font-style:normal;font-weight:500;line-height:1.4} .next-operation-harness{color:var(--ink3);font-family:var(--mono);font-size:10.5px} .next-operation-live-glyph{display:inline-block;margin-right:7px} -.next-operation-assignment{margin-top:8px;color:var(--ink2);font-size:var(--fs-xs);line-height:1.4} -.next-operation-collision{margin-top:8px;color:var(--ink);font-size:var(--fs-xs);line-height:1.4} +.next-operation-assignment{margin-top:8px;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} +.next-operation-collision{margin-top:8px;color:var(--ink);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-operation-unread,.next-operation-scan-only{display:block;margin-top:8px;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-xs);line-height:1.4} .next-operation-fact em{margin-top:6px;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-xs);font-style:normal;line-height:1.4} .next-operation-fact--blocked strong{color:var(--amber)} @@ -412,7 +420,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-capacity{margin-top:22px;border-top:1px solid var(--line2)} .next-capacity-head,.next-capacity-row{display:grid;align-items:center;grid-template-columns:190px 56px minmax(0,1fr) 74px 190px 92px;gap:0 14px} .next-capacity-head{padding:9px 4px 8px;border-bottom:1px solid var(--line2)} -.next-capacity-head span{color:var(--ink3);font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:.06em} +.next-capacity-head span{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.06em} .next-capacity-row{position:relative;padding:13px 4px;border-bottom:1px solid var(--line)} .next-capacity-window{min-width:0} .next-capacity-window b{display:block;font-family:var(--mono);font-weight:700;font-size:12.5px;overflow-wrap:anywhere} @@ -442,19 +450,19 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-capacity-prospect p{margin:0} .next-capacity-prospect p+p{margin-top:7px} .next-capacity-prospect b{font-weight:500;font-family:var(--mono);font-variant-numeric:tabular-nums} -.next-capacity-prospect small{display:block;margin-top:9px;color:var(--ink2);font-family:var(--sans);font-size:var(--fs-xs);line-height:1.45} +.next-capacity-prospect small{display:block;margin-top:9px;color:var(--ink2);font-family:var(--sans);font-size:var(--fs-sentence);font-weight:500;line-height:1.55} .next-capacity-scope{color:var(--ink3);font-family:var(--mono);font-size:10.5px} -.next-operations-fleet span{font-weight:700;letter-spacing:.13em} +.next-operations-fleet span{font-weight:700;letter-spacing:.09em} .next-operation-identity .next-operation-title--unknown{color:var(--ink3)} .next-operation-row--bad{box-shadow:inset 2px 0 var(--clay)} .next-operation-row--want{box-shadow:inset 2px 0 var(--amber)} .next-operation-row--ok{box-shadow:inset 2px 0 var(--accent-dim)} .next-operation-row--unknown{box-shadow:none} -.next-operation-outcome{display:block;margin-top:6px;color:var(--ink2);font-size:var(--fs-xs);line-height:1.5} +.next-operation-outcome{display:block;margin-top:6px;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55} .next-operation-outcome--bad{color:var(--ink2)} .next-operation-outcome--unknown{color:var(--ink3)} -.next-operation-state{padding:13px 8px;text-align:right;color:var(--ink3);font:700 9.5px var(--mono);letter-spacing:.1em} +.next-operation-state{padding:13px 8px;text-align:right;color:var(--ink3);font:700 var(--fs-label) var(--mono);letter-spacing:.09em} .next-operation-group--history .next-operations-columns,#app .next-operation-row[data-next-operation-history="true"]{grid-template-columns:minmax(0,1.5fr) 190px minmax(0,1fr) 92px} .next-capacity-window button{display:block;padding:0;border:0;background:transparent;color:var(--ink);text-align:left;font:inherit;cursor:pointer} .next-capacity-window button::after{content:"";position:absolute;inset:0;z-index:1} @@ -536,16 +544,16 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-attention-heading,.next-attention-section-heading{display:flex;align-items:baseline;gap:14px} .next-attention-heading{margin-bottom:24px} .next-attention-heading h1{margin:0;font-size:var(--fs-2xl);font-weight:700;letter-spacing:-.02em} -.next-attention-heading p,.next-attention-section-heading p{margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-attention-heading p,.next-attention-section-heading p{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-attention-brief{display:grid;gap:10px;padding:16px 18px;border:1px solid var(--line);border-left:2px solid var(--accent-dim);border-radius:10px;background:var(--panel)} .next-attention-brief p{margin:0;color:var(--ink2);font-size:var(--fs-summary);line-height:1.6} .next-attention-brief>p,.next-attention-coverage>p{display:grid;grid-template-columns:104px minmax(0,1fr);gap:16px;align-items:baseline} .next-attention-brief>p>span:last-child{color:var(--ink)} .next-attention-quiet{color:var(--ink2)} .next-attention-coverage{padding-top:10px;border-top:1px solid var(--line)} -.next-attention-brief-label,.next-attention-label{display:block;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);letter-spacing:.13em;text-transform:uppercase} +.next-attention-brief-label,.next-attention-label{display:block;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);letter-spacing:.09em;text-transform:uppercase} .next-attention-coverage-details{margin:10px 0 0 120px;color:var(--ink3);font-size:var(--fs-xs)} -.next-attention-coverage-details summary{display:list-item;width:max-content;max-width:100%;font-family:var(--mono);font-size:var(--fs-meta-detail);cursor:pointer} +.next-attention-coverage-details summary{display:list-item;width:max-content;max-width:100%;font-family:var(--mono);font-size:var(--fs-label);cursor:pointer} .next-attention-coverage-details ul{margin:0;padding:0;list-style:none} .next-attention-coverage-details li{display:grid;grid-template-columns:96px 20px minmax(0,1fr);gap:12px;align-items:center;min-height:27px;padding:5px 0;border-bottom:1px solid var(--line);color:var(--ink2);font-size:var(--fs-xs)} .next-attention-coverage-details strong{font-family:var(--mono);font-size:var(--fs-machine);font-weight:700} @@ -553,7 +561,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-attention-square{width:6px;height:6px;border-radius:1px;background:var(--line2)} .next-attention-square[data-known="true"]{background:var(--accent-dim)} .next-attention-caveats{display:grid;gap:4px;padding-top:9px} -.next-attention-caveats p{color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-attention-caveats p{color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-attention-section{min-width:0;margin-top:24px} .next-attention-section-heading{margin-bottom:10px} .next-attention-section-heading h2:after{content:"";flex:1;min-width:20px;height:1px;background:var(--line)} @@ -570,11 +578,11 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-attention-item>h3,.next-attention-item>p,.next-attention-item>div{min-width:0;margin:0} .next-attention-index{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-machine)} .next-attention-risk-identity h3{margin:0 0 5px;font-size:var(--fs-summary);font-weight:600;line-height:1.4} -.next-attention-risk-identity>span,.next-attention-risk-source{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-meta-detail);line-height:1.5} -.next-attention-risk-assignment{margin:5px 0 0;color:var(--ink2);font-size:var(--fs-xs);line-height:1.5} +.next-attention-risk-identity>span,.next-attention-risk-source{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);line-height:1.5} +.next-attention-risk-assignment{margin:5px 0 0;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-attention-risk-identity a,.next-attention-why a{display:flex;align-items:center;color:var(--ink);text-decoration-color:var(--line2);text-underline-offset:4px} .next-attention-risk-observation p{margin:0;color:var(--ink2);font-size:var(--fs-body);line-height:1.5} -.next-attention-risk-detail{display:grid;gap:5px;margin-top:5px;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-attention-risk-detail{display:grid;gap:5px;margin-top:5px;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55} .next-attention-risk-detail small{display:block;font-size:inherit} .next-attention-risk-source{display:grid;gap:5px} .next-attention-risk-next{color:var(--ink2);font-size:var(--fs-xs)} @@ -589,9 +597,9 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-attention a:focus-visible,.next-attention summary:focus-visible,.next-attention-disclosure:focus-visible{outline:2px solid var(--accent);outline-offset:3px} .next-attention-open{margin:0;padding:0;list-style:none} .next-attention-open li{display:grid;grid-template-columns:34px 210px minmax(0,1fr);gap:16px;align-items:baseline;padding:9px 18px;border-bottom:1px solid var(--line)} -.next-attention-open li>span{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-meta-detail);font-weight:700} +.next-attention-open li>span{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700} .next-attention-open strong{color:var(--ink2);font-size:var(--fs-body);font-weight:400} -.next-attention-open p{margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.55} +.next-attention-open p{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} @media(min-width:900px){ .next-attention-item--legacy{grid-template-columns:minmax(150px,.8fr) minmax(180px,1fr) minmax(180px,1.2fr) minmax(140px,.8fr) minmax(130px,.7fr)} } @@ -639,7 +647,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-session-command-facts>section+section{border-left:1px solid var(--line)} .next-session-source-coverage{margin-top:12px;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-2xs)} .next-session-source-coverage summary{width:max-content;cursor:pointer} -.next-session-source-coverage p{margin:8px 0 0;font-family:var(--sans);font-size:var(--fs-xs);line-height:1.5} +.next-session-source-coverage p{margin:8px 0 0;font-family:var(--sans);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} .next-session-health{margin:20px 0 0;padding:14px 16px;border-left:3px solid var(--amber);background:var(--panel);color:var(--ink2);font-size:var(--fs-sm);line-height:1.5} .next-session-health strong{color:var(--ink);font-family:var(--mono);font-size:var(--fs-2xs);font-weight:500} .next-session-health-separator{color:var(--ink3)} @@ -670,13 +678,13 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-session-controls .next-session-raise{font-weight:700} .next-session-section[data-next-session-section="ask"]{padding:13px 16px;border:1px solid var(--line);border-left:2px solid var(--amber);border-radius:10px;background:var(--panel)} .next-session-ask-callout{margin:0;padding:0;border:0;background:transparent} -.next-session-ask-callout>span{color:var(--amber);font-size:var(--fs-label);letter-spacing:.13em} +.next-session-ask-callout>span{color:var(--amber);font-size:var(--fs-label);letter-spacing:.09em} .next-session-wait{color:var(--ink3);font-family:var(--sans);font-size:var(--fs-xs);letter-spacing:normal;text-transform:none} .next-session-wait[data-known="true"]{color:var(--amber);font-family:var(--mono);font-size:var(--fs-label)} .next-session-ask-callout h2{margin:7px 0 0;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:400} .next-session-facts{margin:20px 0 0} .next-session-facts>div{display:grid;grid-template-columns:120px minmax(0,1fr);gap:18px;align-items:baseline;padding:10px 0;border-bottom:1px solid var(--line)} -.next-session-facts dt{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.13em} +.next-session-facts dt{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} .next-session-facts dd{min-width:0;margin:0;color:var(--ink2);font-family:var(--mono);font-size:var(--fs-xs);line-height:1.55;overflow-wrap:anywhere} .next-session-fact-note{display:block;margin-top:4px;color:var(--ink3);font-family:var(--sans);font-size:var(--fs-xs)} .next-session-evidence{margin-top:20px} @@ -704,7 +712,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-project-workflow-definition>small{display:block;padding:0 10px 12px;font-size:var(--fs-2xs)} .next-cockpit-task-subject{padding:22px 12px 14px;border-bottom:1px solid var(--line)} .next-cockpit-task-subject>.next-scope-cue{margin-bottom:7px} -.next-cockpit-task-subject>span,.next-cockpit-now-state span{color:var(--ink3);font:var(--fs-2xs) var(--mono);letter-spacing:.08em} +.next-cockpit-task-subject>span,.next-cockpit-now-state span{color:var(--ink3);font:var(--fs-2xs) var(--mono);letter-spacing:.09em} .next-cockpit-task-subject h2{margin:5px 0 0;font-size:var(--fs-xl);font-weight:600} .next-cockpit-task-subject h2 small{color:var(--ink2);font-size:inherit;font-weight:450} .next-cockpit-tabs{display:flex;gap:24px;border-bottom:1px solid var(--line2);padding:0 12px} @@ -724,7 +732,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-panel[data-next-cockpit-panel="console"]>.next-project-detail-rail{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;width:auto;min-width:0;border-left:0} .next-cockpit-waiting{margin-bottom:12px} .next-cockpit-waiting a{color:var(--ink);text-decoration-color:var(--line2);text-underline-offset:4px} -.next-cockpit-recovery .next-project-value--absent{font:var(--fs-xs)/1.55 var(--sans);letter-spacing:0} +.next-cockpit-recovery .next-project-value--absent{font:500 var(--fs-sentence)/1.55 var(--sans);letter-spacing:0;max-width:var(--measure)} .next-cockpit-active-delegation,.next-cockpit-needs{min-width:0;padding:18px 12px;border-bottom:1px solid var(--line)} .next-cockpit-needs{border-right:1px solid var(--line)} .next-cockpit-active-delegation h2,.next-cockpit-needs h2{margin:0 0 10px;font-size:var(--fs-md);font-weight:600} @@ -778,12 +786,12 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-scope-tree,.next-cockpit-scope-options{display:grid;gap:2px;padding:12px 12px 12px 0} .next-cockpit-scope-switcher{display:none} .next-cockpit-stale-session{margin:24px 0;padding:24px;border:1px solid var(--line)} -.next-cockpit-stale-session>span{color:var(--ink3);font:var(--fs-2xs) var(--mono);letter-spacing:.08em} +.next-cockpit-stale-session>span{color:var(--ink3);font:var(--fs-2xs) var(--mono);letter-spacing:.09em} .next-cockpit-stale-session h1{margin:8px 0;font-size:var(--fs-xl)} .next-cockpit-stale-session p{color:var(--ink3);font:var(--fs-xs) var(--mono);overflow-wrap:anywhere} .next-cockpit-stale-session a{color:var(--ink2);font:var(--fs-xs) var(--mono)} .next-cockpit-scope-name{font:var(--fs-sm) var(--mono)} -.next-cockpit-scope-heading{padding:0 8px 7px;color:var(--ink3);font:var(--fs-2xs) var(--mono);letter-spacing:.08em} +.next-cockpit-scope-heading{padding:0 8px 7px;color:var(--ink3);font:var(--fs-2xs) var(--mono);letter-spacing:.09em} #app .next-cockpit-scope-tree a,#app .next-cockpit-scope-options a{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;padding:7px 8px;color:var(--ink2);text-decoration:none} .next-cockpit-scope-tree a[data-scope-kind="session"],.next-cockpit-scope-options a[data-scope-kind="session"]{padding-left:14px} .next-cockpit-scope-tree a[aria-current="page"],.next-cockpit-scope-options a[aria-current="page"]{box-shadow:inset 2px 0 var(--ink2);background:var(--panel);color:var(--ink)} @@ -811,7 +819,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-project-status details>summary{cursor:pointer;color:var(--ink3)} .next-cockpit-status-empty{color:var(--ink3)} .next-cockpit-recovery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border-bottom:1px solid var(--line)} -.next-cockpit-recovery>header{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:7px 12px;border-bottom:1px solid var(--line);font:var(--fs-2xs) var(--mono);letter-spacing:.08em} +.next-cockpit-recovery>header{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:7px 12px;border-bottom:1px solid var(--line);font:var(--fs-2xs) var(--mono);letter-spacing:.09em} .next-cockpit-recovery>header button{padding:4px 7px;border:1px solid var(--line2);background:transparent;color:var(--ink2);font:inherit;letter-spacing:0;cursor:pointer} .next-cockpit-recovery>div{min-width:0;padding:10px 12px;border-right:1px solid var(--line);font:var(--fs-xs) var(--mono)} .next-cockpit-recovery>div:last-child{border-right:0} @@ -826,7 +834,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-recovery-memos[data-next-cockpit-memo-empty]>button{border:0;background:none;color:var(--ink2);font:inherit;cursor:pointer} .next-cockpit-recovery-memos textarea{grid-column:1/-1;width:100%;min-height:54px;resize:vertical;padding:7px;border:1px solid var(--line2);background:var(--panel);color:var(--ink);font:inherit} .next-cockpit-recovery span,.next-cockpit-recovery strong,.next-cockpit-recovery small{display:block} -.next-cockpit-recovery span{margin-bottom:5px;color:var(--ink3);font-size:var(--fs-2xs);letter-spacing:.08em} +.next-cockpit-recovery span{margin-bottom:5px;color:var(--ink3);font-size:var(--fs-2xs);letter-spacing:.09em} .next-cockpit-recovery strong{font-weight:500;overflow-wrap:anywhere} .next-cockpit-recovery small{margin-top:4px;color:var(--ink3);overflow-wrap:anywhere} .next-cockpit-recovery details{margin-top:5px;color:var(--ink3)} @@ -854,17 +862,17 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o words reach the server. */ .next-cockpit-held{display:flex;flex-direction:column;gap:11px;padding:14px 16px} .next-cockpit-held>header{display:flex;justify-content:space-between;align-items:baseline;gap:10px} -.next-cockpit-held h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em} -.next-cockpit-held-revision{color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-cockpit-held h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} +.next-cockpit-held-revision{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-held-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:start} .next-cockpit-held-field{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:baseline;gap:4px 8px;padding-left:9px;border-left:1px solid var(--line2)} -.next-cockpit-held-label{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.13em} -.next-cockpit-held-sub{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-cockpit-held-label{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} +.next-cockpit-held-sub{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-cockpit-held-field textarea{grid-column:1/-1;width:100%;min-height:56px;box-sizing:border-box;resize:vertical;padding:7px;border:1px solid var(--line2);background:var(--panel);color:var(--ink);font:14px/1.55 var(--sans)} -.next-cockpit-held-count{color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-cockpit-held-count{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-cockpit-held-field button{border:0;background:none;color:var(--ink2);font:var(--fs-xs) var(--mono);cursor:pointer} .next-cockpit-held-field button[hidden]{display:none} -.next-cockpit-held-absent{grid-column:1/-1;margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} +.next-cockpit-held-absent{grid-column:1/-1;margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55} .next-cockpit-held-cue{grid-column:1/-1;color:var(--ink3);font-size:var(--fs-xs)} /* Section scope, outside the field grid: the act is per annotation and the grid has exactly three columns for the counter and the two field controls. @@ -877,18 +885,18 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o so it is sans. */ .next-cockpit-work{display:flex;flex-direction:column;gap:8px;padding:0 16px 14px} .next-cockpit-work>header{display:flex;justify-content:space-between;align-items:baseline;gap:10px} -.next-cockpit-work h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em} +.next-cockpit-work h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} .next-cockpit-work-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:baseline;gap:3px 10px;padding-left:9px;border-left:1px solid var(--line)} .next-cockpit-work-row+.next-cockpit-work-row{margin-top:5px} -.next-cockpit-work-type{color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.1em} +.next-cockpit-work-type{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.1em} .next-cockpit-work-summary{font:var(--fs-xs)/1.55 var(--mono);overflow-wrap:anywhere} /* A model's paraphrase inside the observed record takes the same third treatment a reading does: italic sans on a dotted rule, no colour. */ .next-cockpit-work-derived{font:italic 13px/1.55 var(--sans);color:var(--ink2);padding-left:7px;border-left:1px dotted var(--line2);overflow-wrap:anywhere} -.next-cockpit-work-mix{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} -.next-cockpit-work-at{color:var(--ink3);font-family:var(--mono);font-size:10px} -.next-cockpit-work-source{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:10px} -.next-cockpit-work-absent,.next-cockpit-work-limit,.next-cockpit-work-dropped{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-work-mix{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-work-at{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} +.next-cockpit-work-source{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} +.next-cockpit-work-absent,.next-cockpit-work-limit,.next-cockpit-work-dropped{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* The third treatment, for a model reading: italic Space Grotesk on the secondary ink over a dotted rule, so it reads as neither a published string nor the board stating a fact. No colour under any result, ever, because @@ -898,22 +906,22 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o revisions rather than part of the reading. */ .next-cockpit-reading,.next-cockpit-departures{display:flex;flex-direction:column;gap:8px;padding:0 16px 14px} .next-cockpit-reading>header,.next-cockpit-departures>header{display:flex;justify-content:space-between;align-items:baseline;gap:10px} -.next-cockpit-reading h2,.next-cockpit-departures h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em} -.next-cockpit-reading-stamp{color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-cockpit-reading h2,.next-cockpit-departures h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} +.next-cockpit-reading-stamp{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} /* The review section holds two collections and two more parts, so each is fenced and named. The label takes the mono uppercase every other source label on this panel takes; it names a collection rather than quoting one, which is note 7 of the design's own compromise list and is kept deliberately. */ .next-cockpit-departure-part{display:flex;flex-direction:column;gap:4px;padding:8px 0 0} .next-cockpit-departure-part+.next-cockpit-departure-part{margin-top:6px;border-top:1px solid var(--line)} -.next-cockpit-departure-label{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);letter-spacing:.08em} +.next-cockpit-departure-label{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);letter-spacing:.09em} /* Figures, one per line and never side by side: two numbers on one line invite the division neither of them survives. */ .next-cockpit-departure-counts{display:flex;flex-direction:column;gap:3px} .next-cockpit-count{display:flex;justify-content:space-between;gap:12px} -.next-cockpit-count-label{color:var(--ink2);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-count-label{color:var(--ink2);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-count-value{color:var(--ink);font:var(--fs-xs)/1.5 var(--mono)} -.next-cockpit-departures-kept{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-departures-kept{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-departures-kept a{color:var(--ink);text-decoration-color:var(--line2);text-underline-offset:4px} .next-cockpit-departures-kept a:hover{text-decoration-color:var(--accent)} /* ONE departure row treatment, and the same one the design writes down (note 2 @@ -930,19 +938,19 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o /* The constraint name and the stamp on one line. A float does not survive the flex column above, so the row carries them together. */ .next-session-departure-head{display:flex;justify-content:space-between;align-items:baseline;gap:10px} -.next-cockpit-reading-name,.next-session-departure-name{color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.13em} +.next-cockpit-reading-name,.next-session-departure-name{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} .next-cockpit-reading-clause,.next-session-departure-clause{font:var(--fs-xs)/1.5 var(--mono);color:var(--ink2);overflow-wrap:anywhere} /* The board saying the revision's words are not retained, which is the board talking and not a quotation, so sans for the reason the limit line below gives. */ -.next-cockpit-reading-clause-absent{font:var(--fs-xs)/1.5 var(--sans);color:var(--ink3);overflow-wrap:anywhere} +.next-cockpit-reading-clause-absent{font:500 var(--fs-sentence)/1.55 var(--sans);color:var(--ink3);overflow-wrap:anywhere} .next-cockpit-reading-result,.next-cockpit-reading-detail,.next-session-departure-reading{font:italic 13.5px/1.55 var(--sans);color:var(--ink2);overflow-wrap:anywhere} -.next-cockpit-reading-evidence,.next-session-departure-base{color:var(--ink3);font-family:var(--mono);font-size:10px;overflow-wrap:anywhere} +.next-cockpit-reading-evidence,.next-session-departure-base{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);overflow-wrap:anywhere} /* The board talking, not a string a source published, so sans. The evidence line above it names a type and a source and stays mono. */ -.next-cockpit-reading-limit{color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans);overflow-wrap:anywhere} -.next-cockpit-reading-why{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} -.next-cockpit-reading-stale,.next-session-departure-stale{margin:0;color:var(--amber);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-reading-limit{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} +.next-cockpit-reading-why{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-reading-stale,.next-session-departure-stale{margin:0;color:var(--amber);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-reading button{align-self:flex-start;padding:5px 10px;border:1px solid var(--line2);background:var(--panel);color:var(--ink);font:var(--fs-xs) var(--sans);cursor:pointer} .next-cockpit-reading button[disabled]{color:var(--ink3);cursor:default} /* HOW IT LANDED. Two cards side by side and never one verdict: what ended and @@ -951,9 +959,9 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o any state. */ .next-cockpit-landed{display:flex;flex-direction:column;gap:8px;padding:0 16px 14px} .next-cockpit-landed>header{display:flex;justify-content:space-between;align-items:baseline;gap:10px} -.next-cockpit-landed h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em} -.next-cockpit-landed-axes{color:var(--ink3);font-family:var(--mono);font-size:10px} -.next-cockpit-held-bound{margin-left:auto;color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-cockpit-landed h2{margin:0;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} +.next-cockpit-landed-axes{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-held-bound{margin-left:auto;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} /* What the unasked lane raised. The same label shape as every other fact on this panel, for the reason the delivery block beside it carries one. @@ -966,21 +974,21 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-session-departures{margin:14px 0 0;padding:10px 0 0;border-top:1px solid var(--line2)} .next-session-departures h2{margin:0;color:var(--ink3);font-family:var(--mono); font-size:var(--fs-label);font-weight:400} -.next-session-departures-count{margin:6px 0 0;color:var(--ink2);font:var(--fs-xs)/1.5 var(--sans)} +.next-session-departures-count{margin:6px 0 0;color:var(--ink2);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* The sentence saying why there is nothing. Dimmer than a departure, because a spent cap and a quiet board are both absences and neither is a finding. */ -.next-session-departures-why{margin:6px 0 0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-session-departures-why{margin:6px 0 0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* Everything typographic about this row is set with the cockpit's above. What is left here is spacing and the one element that row has no counterpart for: the stamp saying when the raise happened. */ .next-session-departure{margin:8px 0 0} -.next-session-departure-at{color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-session-departure-at{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-session-departure-reading,.next-session-departure-base{margin:0} /* What later evidence showed. Sans and upright, because it is the board reporting a derivation over its own store and not a model reading, and no colour on any of its four sentences: the common one is unknown, and unknown never takes colour. Dimmer than the reading above it, which is the finding. */ -.next-session-departure-next{margin:2px 0 0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-session-departure-next{margin:2px 0 0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* What became of a raise. The heading carries the count and the paragraphs carry the sentences the server composed. @@ -1000,18 +1008,18 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o panel is stating. Matches .next-session-evidence h2. */ .next-session-delivery h2{margin:0;color:var(--ink3);font-family:var(--mono); font-size:var(--fs-label);font-weight:400} -.next-session-delivery-count{margin:6px 0 0;color:var(--ink2);font:var(--fs-xs)/1.5 var(--sans)} -.next-session-delivery-why{margin:4px 0 0;color:var(--ink);font:var(--fs-xs)/1.5 var(--sans)} +.next-session-delivery-count{margin:6px 0 0;color:var(--ink2);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-session-delivery-why{margin:4px 0 0;color:var(--ink);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* A qualifier on the sentence above, not a sentence of its own weight. */ -.next-session-delivery-note{margin:4px 0 0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-session-delivery-note{margin:4px 0 0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* Dimmer than the outcome above it on purpose. The browser lane is weaker evidence than a server-side outcome, and the two reading alike is what would let the reader take a tab that was open once for a notification that landed. */ -.next-session-delivery-lane{margin:4px 0 0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-session-delivery-lane{margin:4px 0 0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} [data-next-delivery="refused"] .next-session-delivery-why, [data-next-delivery="not-launched"] .next-session-delivery-why{color:var(--amber)} [data-next-delivery="no-lane"] .next-session-delivery-why{color:var(--ink3)} -.next-session-held-link{margin:10px 0 0;font:var(--fs-xs)/1.5 var(--sans)} +.next-session-held-link{margin:10px 0 0;font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* The board's own ink, not the browser's default link colour. Nothing else on this page is blue, and an unstyled anchor announced itself as the one thing here that came from outside the design. */ @@ -1021,7 +1029,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o anchor announces itself as the one thing on the page from outside the design, and the `#app a` rules at the top of this file win on specificity against a class-only selector. */ -.next-cockpit-held-reentry{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-held-reentry{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-held-reentry a{color:var(--ink);text-decoration-color:var(--line2);text-underline-offset:4px} .next-cockpit-held-reentry a:hover{text-decoration-color:var(--accent)} /* The baseline question. Sans and no colour throughout: none of it is a model @@ -1032,32 +1040,32 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o /* The Intent log. A list of the reader's own words, so the words take the published-string register and everything the board says about them is sans. */ .next-intent{display:flex;flex-direction:column;gap:10px} -.next-intent-note{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-intent-note{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-intent-row{display:flex;flex-direction:column;gap:3px;padding-left:9px;border-left:1px solid var(--line2)} -.next-intent-key{font-family:var(--mono);font-size:10px;color:var(--ink3);letter-spacing:.08em} +.next-intent-key{font-family:var(--mono);font-size:var(--fs-label);color:var(--ink3);letter-spacing:.08em} .next-intent-key a{color:var(--ink);text-decoration-color:var(--line2);text-underline-offset:4px} .next-intent-key a:hover{text-decoration-color:var(--accent)} .next-intent-gone{color:var(--ink3)} .next-intent-words{font:var(--fs-xs)/1.5 var(--mono);color:var(--ink2);overflow-wrap:anywhere} -.next-intent-revision,.next-intent-why{color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-intent-revision,.next-intent-why{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-conflict{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border:1px solid var(--line2);background:var(--panel)} -.next-cockpit-conflict-why,.next-cockpit-conflict-settled{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-conflict-why,.next-cockpit-conflict-settled{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* The settle cue, in the held-cue register and its own class, per next-cockpit.js. */ -.next-cockpit-conflict-cue{color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} -.next-cockpit-conflict-open{margin:0;color:var(--ink);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-conflict-cue{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-conflict-open{margin:0;color:var(--ink);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-conflict-row{display:flex;flex-direction:column;gap:2px;padding-left:9px;border-left:1px solid var(--line2)} /* A direction is a string the reader published, so mono, the same register the observed record's own rows take. */ .next-cockpit-conflict-text{font:var(--fs-xs)/1.5 var(--mono);color:var(--ink2);overflow-wrap:anywhere} -.next-cockpit-conflict-at{color:var(--ink3);font-family:var(--mono);font-size:10px} +.next-cockpit-conflict-at{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-cockpit-conflict-choices{display:flex;flex-wrap:wrap;gap:8px} .next-cockpit-conflict-choices button{padding:5px 10px;border:1px solid var(--line2);background:var(--panel);color:var(--ink);font:var(--fs-xs) var(--sans);cursor:pointer} .next-cockpit-landed-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;align-items:start} .next-cockpit-landed-card{display:flex;flex-direction:column;gap:4px;padding-left:9px;border-left:1px solid var(--line2)} -.next-cockpit-landed-label{color:var(--ink3);font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.13em} -.next-cockpit-landed-value{font:var(--fs-xs)/1.5 var(--sans);color:var(--ink)} +.next-cockpit-landed-label{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} +.next-cockpit-landed-value{font:500 var(--fs-sentence)/1.55 var(--sans);color:var(--ink);max-width:var(--measure)} .next-cockpit-landed-value--absent{color:var(--ink3)} -.next-cockpit-landed-note{color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-landed-note{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-scope{display:flex;align-items:baseline;gap:12px;padding:7px 12px;font:var(--fs-xs) var(--mono);color:var(--ink3)} .next-cockpit-scope strong{font-size:var(--fs-2xs);letter-spacing:.08em;color:var(--ink)} .next-cockpit-scope--project{border-bottom:1px solid var(--line)} @@ -1067,10 +1075,10 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-menu>summary[aria-label="More"]:before{content:attr(aria-label);font:var(--fs-xs) var(--sans)} .next-menu>summary[aria-label="More"]:after{content:"▾";font:var(--fs-xs) var(--mono)} .next-menu[open]>.next-menu-items{top:100%} -.next-cockpit-content{font:var(--fs-xs)/1.55 var(--sans)} +.next-cockpit-content{font:500 var(--fs-sentence)/1.55 var(--sans)} .next-cockpit-recovery>div>strong,.next-cockpit-content .next-project-detail-empty{color:var(--ink3)} .next-cockpit-recovery [data-next-cockpit-task-known]>strong,.next-cockpit-recovery .next-cockpit-recovery-evidence>strong{color:var(--ink)} -.next-cockpit-recovery>div{font:var(--fs-xs)/1.55 var(--sans)} +.next-cockpit-recovery>div{font:500 var(--fs-sentence)/1.55 var(--sans)} .next-cockpit-recovery span{font-family:var(--mono)} .next-cockpit-recovery strong,.next-cockpit-recovery small{font-size:var(--fs-xs)} .next-cockpit-recovery-evidence>strong,.next-cockpit-execution-root>strong,.next-cockpit-child-row>strong, @@ -1080,34 +1088,34 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-recovery span.next-cockpit-source{display:inline;margin:0;color:inherit;font-size:inherit;letter-spacing:0} .next-cockpit-scope-tree small[data-next-withheld],.next-cockpit-scope-options small[data-next-withheld]{font-family:var(--sans);color:var(--ink3)} .next-cockpit-authority>strong,.next-cockpit-authority>small{font-family:var(--sans)} -.next-cockpit-authority>span{font:var(--fs-xs)/1.5 var(--sans);letter-spacing:0} +.next-cockpit-authority>span{font:500 var(--fs-sentence)/1.55 var(--sans);letter-spacing:0;max-width:var(--measure)} .next-cockpit-empty,.next-cockpit-decision-summary,.next-cockpit-viewing-session, .next-cockpit-now-state small,.next-project-workflow-definition>small, .next-cockpit-system-details ul,.next-cockpit-memos label>small, -.next-cockpit-evidence-missing{font:var(--fs-xs)/1.55 var(--sans)} -.next-cockpit-content .next-cockpit-evidence-missing{margin:6px 0;color:var(--ink3);font:var(--fs-xs)/1.55 var(--sans)} -.next-cockpit-content .next-course-evidence{font:var(--fs-xs)/1.55 var(--sans)} +.next-cockpit-evidence-missing{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-content .next-cockpit-evidence-missing{margin:6px 0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-content .next-course-evidence{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-content .next-course-evidence .next-cockpit-source{display:inline;font:inherit;font-family:var(--mono)} .next-cockpit-recovery .next-project-goal{margin-top:12px;padding:10px 0;border:0;border-top:1px solid var(--line);border-radius:0;background:transparent} .next-cockpit-recovery .next-project-goal-text{font:var(--fs-xs)/1.55 var(--mono)} .next-cockpit-recovery .next-project-goal-text.next-project-value--absent, -.next-cockpit-recovery .next-project-goal-gap{font:var(--fs-xs)/1.55 var(--sans)} +.next-cockpit-recovery .next-project-goal-gap{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-recovery .next-project-goal-source{font-size:var(--fs-2xs)} .next-cockpit-recovery details>summary,.next-course-evidence>summary, -.next-cockpit-plan-details>summary,.next-cockpit-console-status>summary{align-content:center;padding:4px 0;color:var(--ink2);font:var(--fs-xs)/1.5 var(--sans);text-decoration:underline;text-decoration-color:var(--line2);text-underline-offset:4px} +.next-cockpit-plan-details>summary,.next-cockpit-console-status>summary{align-content:center;padding:4px 0;color:var(--ink2);font:500 var(--fs-sentence)/1.55 var(--sans);text-decoration:underline;text-decoration-color:var(--line2);text-underline-offset:4px;max-width:var(--measure)} .next-cockpit-recovery details>summary:hover,.next-course-evidence>summary:hover{color:var(--ink)} .next-cockpit-content summary:focus-visible,#app .next-cockpit-scope-tree a:focus-visible, #app .next-cockpit-scope-options a:focus-visible,.next-cockpit-scope-switcher>summary:focus-visible{outline:2px solid var(--ink2);outline-offset:3px} .next-cockpit-panel[data-next-cockpit-panel="now"]{gap:18px;padding:18px 12px} .next-cockpit-scope-tree .next-scope-cue--session,.next-cockpit-scope-options .next-scope-cue--session{margin-left:0} -.next-cockpit-scope-switcher>summary{font:var(--fs-xs)/1.5 var(--sans)} +.next-cockpit-scope-switcher>summary{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-content .next-rail-meta--amber{color:var(--ink3)} @media(max-width:1279px){ .next-cockpit-shell{grid-template-columns:1fr} .next-cockpit-content{border-top:1px solid var(--line);border-left:0} .next-cockpit-scope-tree{display:none} .next-cockpit-scope-switcher{display:block;padding:10px 0} - .next-cockpit-scope-switcher>summary{display:flex;justify-content:space-between;gap:12px;padding:8px;color:var(--ink2);font:var(--fs-xs)/1.5 var(--sans);cursor:pointer} + .next-cockpit-scope-switcher>summary{display:flex;justify-content:space-between;gap:12px;padding:8px;color:var(--ink2);font:500 var(--fs-sentence)/1.55 var(--sans);cursor:pointer} .next-cockpit-scope-options{padding:4px 0} } @media(max-width:760px){ @@ -1132,7 +1140,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-terminal-open,.pc-terminal-bar button{padding:7px 10px;border:1px solid var(--line2);border-radius:4px;background:transparent;color:var(--ink2);font:var(--fs-machine) var(--mono);cursor:pointer} .pc-terminal-open:hover,.pc-terminal-bar button:hover{background:var(--panel);border-color:var(--ink3);color:var(--ink)} .pc-terminal-open:focus-visible,.pc-terminal-bar button:focus-visible,.pc-semantic-timeline button:focus-visible,.pc-semantic-timeline summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px} -.pc-substrate-empty,.pc-substrate-reason,.pc-terminal-identity p{color:var(--ink3);font:var(--fs-xs)/1.55 var(--sans);overflow-wrap:anywhere} +.pc-substrate-empty,.pc-substrate-reason,.pc-terminal-identity p{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} .pc-substrate-empty{margin:8px 0} .pc-terminal-absence{padding:4px 0 10px;max-width:70ch} .pc-terminal-absence .pc-substrate-empty:first-child{color:var(--ink2)} @@ -1140,13 +1148,13 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-semantic-graph{min-width:0} .pc-semantic-timeline{--lane-count:1;min-width:0;position:relative;padding:10px 0 4px} .pc-history-band{margin-top:8px;border-top:1px solid var(--line);padding-top:7px} -.pc-history-band>summary,.pc-trail-history>summary,.pc-entry-suppressed>summary,.pc-event-evidence>summary{cursor:pointer;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} +.pc-history-band>summary,.pc-trail-history>summary,.pc-entry-suppressed>summary,.pc-event-evidence>summary{cursor:pointer;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .pc-lane-legend{display:grid;grid-template-columns:74px calc(var(--lane-count) * 24px) minmax(0,1fr);margin-bottom:8px} .pc-lane-labels{grid-column:3;display:flex;gap:6px;flex-wrap:wrap;color:var(--ink2);font:var(--fs-machine) var(--mono)} .pc-lane-labels span{padding:1px 6px;border-left:2px solid var(--line2);white-space:normal;overflow-wrap:anywhere} .pc-graph-row{display:grid;grid-template-columns:74px calc(var(--lane-count) * 24px) minmax(0,1fr);align-items:start;min-height:48px} .pc-graph-row>time,.pc-graph-time{padding:8px 6px 0 0;color:var(--ink3);text-align:right;overflow-wrap:anywhere} -.pc-graph-row>time{font:var(--fs-meta-detail)/1.5 var(--mono)} +.pc-graph-row>time{font:var(--fs-label)/1.5 var(--mono)} .pc-graph-filter{display:flex;justify-content:flex-end;gap:4px;margin-bottom:8px;flex-wrap:wrap} .pc-graph-filter button{padding:4px 8px;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--ink3);font:var(--fs-machine) var(--mono);cursor:pointer} .pc-graph-filter button.selected{border-bottom-color:var(--accent);color:var(--ink)} @@ -1160,7 +1168,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-graph-row>.pc-trail-body{min-width:0;padding:5px 0 15px} .pc-timeline-event>summary{cursor:pointer;border-radius:4px} .pc-trail-summary,.pc-trail-summary>span{display:block} -.pc-entry-details{display:grid;gap:7px;margin-top:9px;padding:10px 12px;border-left:2px solid var(--line2);background:var(--sunk);color:var(--ink3);font:var(--fs-xs)/1.55 var(--sans);overflow-wrap:anywhere} +.pc-entry-details{display:grid;gap:7px;margin-top:9px;padding:10px 12px;border-left:2px solid var(--line2);background:var(--sunk);color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} .pc-entry-details b{color:var(--ink2);font:var(--fs-machine) var(--mono)} .pc-entry-details time,.pc-event-evidence time{font:var(--fs-machine) var(--mono)} .pc-entry-source-list{display:grid;gap:6px;margin-top:6px} @@ -1169,7 +1177,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-trail-top .pc-lane-title{color:var(--ink);font:var(--fs-body)/1.4 var(--mono);overflow-wrap:anywhere} .pc-trail-top span{color:var(--ink3);font:var(--fs-machine)/1.5 var(--mono);text-align:right} .pc-trail-result{margin-top:4px;color:var(--ink2);font:var(--fs-body)/1.5 var(--sans);overflow-wrap:anywhere} -.pc-trail-quiet,.pc-trail-history,.pc-event-evidence{margin-top:5px;color:var(--ink3);font:var(--fs-xs)/1.55 var(--sans);overflow-wrap:anywhere} +.pc-trail-quiet,.pc-trail-history,.pc-event-evidence{margin-top:5px;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} .pc-terminal-open{margin:8px 0 12px} .pc-terminal{display:flex;min-width:0;flex-direction:column;border:1px solid var(--line2);border-radius:6px;background:var(--sunk);color:var(--ink);overflow:hidden} .pc-terminal-bar{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--line);font:var(--fs-machine)/1.5 var(--mono);flex-wrap:wrap} @@ -1179,7 +1187,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-terminal-identity p{margin:3px 0} .pc-terminal-identity code{margin-right:10px} .pc-terminal-viewport{height:340px;overflow:auto;overscroll-behavior:contain} -.pc-terminal-screen{min-width:100%;min-height:340px;padding:6px;box-sizing:content-box;font:var(--fs-xs)/1.55 var(--sans)} +.pc-terminal-screen{min-width:100%;min-height:340px;padding:6px;box-sizing:content-box;font:500 var(--fs-sentence)/1.55 var(--sans)} .pc-terminal-screen>.xterm{width:100%;height:100%} @media(max-width:760px){ .pc-lane-legend{display:none} diff --git a/cargento/skills/cargento/tests/test_focus.py b/cargento/skills/cargento/tests/test_focus.py index c3f18b6ab..7ef0848a4 100644 --- a/cargento/skills/cargento/tests/test_focus.py +++ b/cargento/skills/cargento/tests/test_focus.py @@ -1021,7 +1021,7 @@ def test_the_pinned_assembly_is_untouched(self) -> None: # would fail on the reader rather than on an injected token. self.assertNotIn(b' None: self.assertEqual(0, out["inputs"]) self.assertTrue(out["reason"]) + def test_both_absence_explanations_read_as_sentences_not_header_labels(self) -> None: + """DRC-4587 AC-2. `docs/design-next-ui.md` already rules that an absence + explanation is a sentence and loses to the sentence floor; both of these + were drawn at 10px mono inside an `

`'s own `
`, where no size + change alone can reach them. + """ + out = self.run_fixture( + self.ANNOTATED + + r""" +navigateNext({view:"project", project:"cargento", focus:"codex:focus-1", tab:"held-to"}); +await __settle(); +const html = __els.app.innerHTML; +const headers = [...html.matchAll(/
([\s\S]*?)<\/header>/g)].map(m => m[1]).join("|"); +console.log(JSON.stringify({ + revisionInHeader: headers.includes("next-cockpit-held-revision"), + axesInHeader: headers.includes("next-cockpit-landed-axes"), + revisionPresent: html.includes('class="next-cockpit-held-revision"'), + axesPresent: html.includes('class="next-cockpit-landed-axes"'), +})); +""" + ) + + # Then: both still render, and neither is a child of a `
` any + # more, so each can carry the sentence tier without dragging its `

` + # label with it. + assert isinstance(out, dict) + self.assertTrue(out["revisionPresent"]) + self.assertTrue(out["axesPresent"]) + self.assertFalse(out["revisionInHeader"]) + self.assertFalse(out["axesInHeader"]) + + # And the register they are drawn in is the sentence one, not a label + # bumped to 15px while keeping mono -- the falsifier AC-2 names. + styles = ( + pathlib.Path(__file__).resolve().parents[1] / "cargento_runtime" / "web" / "styles.css" + ).read_text(encoding="utf-8") + for cls in ("next-cockpit-held-revision", "next-cockpit-landed-axes"): + with self.subTest(rule=cls): + rule = next(line for line in styles.split("\n") if line.startswith("." + cls + "{")) + self.assertIn("var(--sans)", rule) + self.assertIn("var(--fs-sentence)", rule) + self.assertNotIn("var(--mono)", rule) + @unittest.skipUnless(shutil.which("node"), "node not available") class CockpitReadingShapeTest(NextPageJsHarness): diff --git a/cargento/skills/cargento/tests/test_next_flag.py b/cargento/skills/cargento/tests/test_next_flag.py index 0430e7c48..9809805c8 100644 --- a/cargento/skills/cargento/tests/test_next_flag.py +++ b/cargento/skills/cargento/tests/test_next_flag.py @@ -64,9 +64,9 @@ def test_retired_next_query_is_not_a_page_alias(self) -> None: def test_the_canonical_loader_is_the_released_ui_bundle(self) -> None: page = frontend_page.load_page() - self.assertEqual(911_302, len(page)) + self.assertEqual(914_344, len(page)) self.assertEqual( - "7c0dbcb341cd1652e60821fde01b3c1080bc58022100bfa9ba25317fefb1c52a", + "2165bf68d82b7f98853561d56b3f0300240095a964bd040d74b080de45080ce6", hashlib.sha256(page).hexdigest(), ) diff --git a/cargento/skills/cargento/tests/test_next_page.py b/cargento/skills/cargento/tests/test_next_page.py index 6ea2d483d..82116cd87 100644 --- a/cargento/skills/cargento/tests/test_next_page.py +++ b/cargento/skills/cargento/tests/test_next_page.py @@ -681,7 +681,7 @@ def test_load_page_preserves_its_byte_oracles(self) -> None: ), "next-cockpit.js": ( 198_105, - "3c3fa0ebd4be8c51ad5b79d8f2919e090022f6832ae094b9bd28322be736853c", + "16f67be92f133b9e837d137a96847723861a0128d4d91a0164ad63d9ab2190f6", ), "next-render.js": ( 8_960, @@ -700,16 +700,16 @@ def test_load_page_preserves_its_byte_oracles(self) -> None: self.assertEqual(digest, hashlib.sha256(data).hexdigest()) styles = frontend_page.asset_path("styles.css").read_bytes() - self.assertEqual(108_008, len(styles)) + self.assertEqual(111_050, len(styles)) self.assertEqual( - "ea8b79ed9addea2fefb6463c2ec563a891ce4282a1fd2f29a2f02719be8de195", + "91a303b22906b2f94fcd0f023a4c001388e840efa9f245d08e22822b1180b754", hashlib.sha256(styles).hexdigest(), ) assembled = frontend_page.load_page() - self.assertEqual(911_302, len(assembled)) + self.assertEqual(914_344, len(assembled)) self.assertEqual( - "7c0dbcb341cd1652e60821fde01b3c1080bc58022100bfa9ba25317fefb1c52a", + "2165bf68d82b7f98853561d56b3f0300240095a964bd040d74b080de45080ce6", hashlib.sha256(assembled).hexdigest(), ) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index e95ec561d..f241b60c3 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -52,7 +52,7 @@ would defeat its purpose as a way to choose a session. The Scope rail now has a 1280px and above, with separate readable space for harness names and session titles; below 1280px it becomes a scope switcher. Missing terminal registration, evidence, history or delegation renders the reason for the missing reading. Source strings use mono; sentences the -board says use sans at 12.5px or larger. Evidence and More remain named disclosures. +board says use sans at 15px or larger. Evidence and More remain named disclosures. The terminal bridge, semantic history and model-assisted goal analysis remain prototypes. Human context and tripwires stay browser-local and deliver no instruction to an agent. Semantic @@ -109,12 +109,38 @@ Assign one owner to each region during parallel work. Keep media queries at the region; a shared responsive block would make every view edit the same tail. Moving a rule between regions is an ownership change, not incidental cleanup. -Board sentences have a 12.5px floor (`--fs-xs`). Labels, identifiers, timestamps, rates and -compact controls retain their smaller design sizes, down to 9px column headers. The prototype -placed some absence explanations at 10px; those are sentences the board asks a person to read, so -the sentence floor wins. The stylesheet retains scale tokens and literal sizes. The asset test -pins the dark palette and checks text inks above 4.5:1 on the ground, panel and inset surfaces; it -does not enforce all font sizes or spacing between contrast steps. +Board sentences have a 15px floor (`--fs-sentence`), at weight 500 and line-height 1.55. Labels, +identifiers, timestamps, rates and compact controls sit on one tier, `--fs-label` at 11px; the +four steps that ran 9px, 9.5px, 10px and 10.5px were a 3px band nobody can rank, and three of the +six sub-12px tokens had no callers at all. The floor left 12.5px because the audit put `--ink`, +the brightest colour in the palette, below the body-text requirement there: brightening the ink +was not available, so size was the only lever left. The absence explanations the prototype placed +at 10px are sentences, so they take the sentence tier, and two of them had to leave an `

`'s +`
` to get there. The stylesheet +retains scale tokens and literal sizes. The asset test pins the dark palette and checks text inks +above 4.5:1 on the ground, panel and inset surfaces; it does not enforce all font sizes or spacing +between contrast steps. + +Which rules are sentences is a test, not a judgement call, so a reviewer argues with a list: +**a rule is on the sentence tier when it sets its text in sans and declares its own prose +line-height.** Mono is a string a source published, and a label carries no line-height of its own. +Sixty-five rules on the old `--fs-xs` step qualify, plus the two absence explanations above, and +all sixty-seven resolve to `var(--fs-sentence)`. Forty-nine of them also cap at `--measure` (540px, +about 72 characters at this tier). The eighteen that do not are the ones a cap would clamp wrongly: +eight carry `overflow-wrap:anywhere`, eight are layout boxes rather than single lines, and +`.next-cockpit-content` and `.next-cockpit-recovery>div` are the prose containers, where 540px +would clamp the cards inside them instead of the sentences. + +**The floor is not yet universal, and this is the gap to close next.** Seventeen further sans rules +pass the same test at 13px to 14.5px, on the `--fs-body`, `--fs-summary` and `--fs-sm` steps plus +two literals. They were left alone: raising them is another seventeen rules of review surface, and +two of the seventeen are not sentences at all (a textarea and the prototype terminal), so the set +needs reading one selector at a time rather than a sweep. + +The uppercase label tier carries one tracking value, `.09em`, on twenty-four declarations. Three +kept their own because their content is not uppercase: `.next-cockpit-work-type` prints a fact +type such as `gate_decision`, `.next-intent-key` a session key, `.next-cockpit-scope strong` a +scope name. Reading each tracking group one selector at a time is what caught them. Space Grotesk and Space Mono subsets travel inside the assembled page as data URLs. A missing or malformed font is a canonical asset failure and prevents startup before the socket binds. There is From 3cc7ef49464f776234623b6ff4afc06d322223a3 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 17:43:33 +0800 Subject: [PATCH 08/15] docs(burndown): record the captain's act-don't-ask and visibility directives A recommendation the first officer then waits on adds a decision rather than removing one, and a deferred gap is worked inside its own milestone. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jared Scott --- docs/roadmap-burndown/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/roadmap-burndown/README.md b/docs/roadmap-burndown/README.md index a1893155f..7704fd3ef 100644 --- a/docs/roadmap-burndown/README.md +++ b/docs/roadmap-burndown/README.md @@ -842,6 +842,25 @@ Two rulings, in the captain's own words, scoped to this milestone's burndown. delegated by this grant. The `pr-merge` merge hook's push approval is covered, because the grant names the merge gates and says to do it; the draft is still presented before the push. +### Given 2026-09-17, in the captain's own words, and they override the defaults above + +- **Take the recommended route; do not wait for a decision you have already made.** "If you have a + strong recommendation, I want you to take that recommended route. Do not wait for me unless you + absolutely need my input." A first officer that recommends one direction and then waits has not + saved the captain a decision, it has added one. Act, and report what was done and why. This + extends the "do not wait for yes, that's fine" directive from anything reversible to anything the + FO holds a clear view on — including acceptance-criterion judgments the FO would otherwise route + up under `## Review-finding disposition` item 5, **unless** the call genuinely turns on product + intent only the captain holds. +- **When input IS genuinely needed, pull it out where it cannot be missed.** "It is EXTREMELY + difficult for me to see that you need me because you are burying the need for my attention within + a lot of prose and content that is not applicable to me." The ask goes at the TOP of the message + under its own heading, before any status, with the question, the recommendation, and what saying + yes does. Everything not waiting on the captain goes after it, or is left out. +- **A filed follow-up is not a disposal route.** "Don't just let it dangle." A gap deferred out of + one PR is worked inside the same milestone, and the milestone is not complete while it is open. + Filing it records the gap; scheduling it is what closes it. + ## Workflow State View the workflow overview: From a251ca4a9872280f2963f65944ff06bcb70ed460 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 19:01:16 +0800 Subject: [PATCH 09/15] docs(design): correct the tracking-inventory claim to the tree The paragraph said the uppercase label tier carries one tracking value and that three rules kept their own. Both halves were false. Nine rules sit off .09em, and six of the nine print capitals: .next-capacity-head span, .next-capacity-models small and .next-capacity-row small at .06em, the course and direction header badges and .next-scope-cue at .07em, and the recovery memo field labels at .04em. Verified one selector at a time against the emitters in next-capacity.js and next-cockpit.js. The cause is worth keeping: the issue inventoried four tracking values and the stylesheet had seven, so sweeping the four named groups never reached the other six declarations. The paragraph now states the smaller true claim, that the uppercase tier is not uniform, and names every rule a reader can check. Docs only. No CSS changed, so no byte pin moves. Signed-off-by: Jared Scott --- docs/design-next-ui.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index f241b60c3..1c3632b0b 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -137,10 +137,20 @@ two literals. They were left alone: raising them is another seventeen rules of r two of the seventeen are not sentences at all (a textarea and the prototype terminal), so the set needs reading one selector at a time rather than a sweep. -The uppercase label tier carries one tracking value, `.09em`, on twenty-four declarations. Three -kept their own because their content is not uppercase: `.next-cockpit-work-type` prints a fact -type such as `gate_decision`, `.next-intent-key` a session key, `.next-cockpit-scope strong` a -scope name. Reading each tracking group one selector at a time is what caught them. +Twenty-four declarations carry `.09em`: the whole `.13em` and `.14em` groups, plus eight of the +eleven in the `.1em` and `.08em` groups. The other three of those eleven keep their own value +because their content is not uppercase. `.next-cockpit-work-type` prints a fact type such as +`gate_decision`, `.next-intent-key` a session key, and `.next-cockpit-scope strong` a scope name. + +**The uppercase label tier is not uniform, and this change did not make it so.** Six further rules +print capitals on some other value: `.next-capacity-head span` (`WINDOW`, `USED`), +`.next-capacity-models small` (`WITHIN THIS WEEKLY BUDGET`) and `.next-capacity-row small` (`USED`, +`PACE`, `BUDGET ENDS`, `RESETS`) at `.06em`; the course and direction header badges +(`EXACT DIRECTION`, `EXACT DECISION`) and `.next-scope-cue` (`PROJECT`, `SESSION`, `SCOPE UNKNOWN`) +at `.07em`; and the recovery memo field labels (`OUTCOME`, `FOCUS`) at `.04em`. They survived +because the issue inventoried four tracking values and the stylesheet had seven, so sweeping the +four named groups never reached the other six declarations. Count the values in the tree, not in +the issue. Space Grotesk and Space Mono subsets travel inside the assembled page as data URLs. A missing or malformed font is a canonical asset failure and prevents startup before the socket binds. There is From 4fb5ee6d62c30e91fa0c6712c7ea29ab0ca52f59 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 19:18:59 +0800 Subject: [PATCH 10/15] fix(web): stop absences outranking the values they replace Correction round 1. Raising a container to the sentence tier without raising the value rules inside it left the absence larger than the fact it stands in for. Measured live on this branch: "assignment unavailable" computed 15px/500 sans in --ink3 while the value that was captured computed 12.5px mono in --ink. This change created that inversion, and it is the one the milestone exists to remove: you can tell a label from its answer, a figure from a gap. Swept for the shape rather than patching the reported cells. Seven value rules now join their absence or caption on the sentence tier: - .next-cockpit-recovery strong, split from the `small` storage cue, which is not a value and stays down - .next-cockpit-recovery .next-project-goal-text, keeping mono - .next-cockpit-now-state strong and the memo field value - .pc-terminal-identity code and strong, each drawn against a 15px absence emitted by the same ternary - .next-attention-open strong Nine more candidates the sweep surfaced are deliberately unchanged: fleet figures are already 27px, section and field captions are meant to sit below their values, and two pairs turned out to be separate components rather than a value and its caption. docs/design-next-ui.md carries the list. Lowering the absences back to 12.5px was the other way out and is refused: it reinstates the sub-floor sentence the header moves exist to remove. Documentation corrections in the same round, all four measured against the tree: the deferred gap is 19 rules rather than 17, because two compose their size, family and line-height across three rules each and no single-rule census sees them; the zero-caller tokens are named rather than counted, since the count turns on whether the 12px step is inside the band; the sub-11px :root steps are stated as four with 9px the smallest; and the APCA justification now carries the audit-only caveat the milestone already carries, beside the recomputed 16.36:1 that shows this was never a contrast defect. Seven byte pins regenerated from the assets. next-cockpit.js is untouched this round, so its size and digest were verified unchanged rather than rewritten. Signed-off-by: Jared Scott --- .../cargento/cargento_runtime/web/styles.css | 19 ++-- cargento/skills/cargento/tests/test_focus.py | 2 +- .../cargento/tests/test_next_cockpit.py | 89 +++++++++++++++++++ .../skills/cargento/tests/test_next_flag.py | 4 +- .../skills/cargento/tests/test_next_page.py | 8 +- docs/design-next-ui.md | 41 ++++++--- 6 files changed, 138 insertions(+), 25 deletions(-) diff --git a/cargento/skills/cargento/cargento_runtime/web/styles.css b/cargento/skills/cargento/cargento_runtime/web/styles.css index 679d11f2e..f8223ab38 100644 --- a/cargento/skills/cargento/cargento_runtime/web/styles.css +++ b/cargento/skills/cargento/cargento_runtime/web/styles.css @@ -598,7 +598,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-attention-open{margin:0;padding:0;list-style:none} .next-attention-open li{display:grid;grid-template-columns:34px 210px minmax(0,1fr);gap:16px;align-items:baseline;padding:9px 18px;border-bottom:1px solid var(--line)} .next-attention-open li>span{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700} -.next-attention-open strong{color:var(--ink2);font-size:var(--fs-body);font-weight:400} +.next-attention-open strong{color:var(--ink2);font-size:var(--fs-sentence);font-weight:400} .next-attention-open p{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} @media(min-width:900px){ .next-attention-item--legacy{grid-template-columns:minmax(150px,.8fr) minmax(180px,1fr) minmax(180px,1.2fr) minmax(140px,.8fr) minmax(130px,.7fr)} @@ -725,7 +725,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-now-state>div{min-width:0;padding:10px 12px;border-right:1px solid var(--line)} .next-cockpit-now-state>div:last-child{border-right:0} .next-cockpit-now-state strong,.next-cockpit-now-state small{display:block;margin-top:4px;overflow-wrap:anywhere} -.next-cockpit-now-state strong{font-size:var(--fs-xs);font-weight:600} +.next-cockpit-now-state strong{font-size:var(--fs-sentence);font-weight:600} .next-cockpit-now-state small{color:var(--ink3);font-size:var(--fs-2xs)} .next-cockpit-panel[data-next-cockpit-panel="now"]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start} .next-cockpit-panel[data-next-cockpit-panel="now"]>.next-project-detail-status,.next-cockpit-panel[data-next-cockpit-panel="now"]>.next-cockpit-plan-details{grid-column:1/-1} @@ -766,7 +766,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-memos>div>[data-next-cockpit-memo-field]{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:baseline;gap:8px;padding:10px 12px} .next-cockpit-memos>div>[data-next-cockpit-memo-field]+[data-next-cockpit-memo-field]{border-left:1px solid var(--line)} .next-cockpit-memos [data-next-cockpit-memo-field]>span,.next-cockpit-memos label>small{color:var(--ink3);font:var(--fs-2xs) var(--mono)} -.next-cockpit-memos [data-next-cockpit-memo-field]>strong{overflow:hidden;color:var(--ink2);font-size:var(--fs-sm);font-weight:450;text-overflow:ellipsis;white-space:nowrap} +.next-cockpit-memos [data-next-cockpit-memo-field]>strong{overflow:hidden;color:var(--ink2);font-size:var(--fs-sentence);font-weight:450;text-overflow:ellipsis;white-space:nowrap} .next-cockpit-memos [data-next-cockpit-action^="memo-"]{border:0;background:none;color:var(--ink2);font:var(--fs-2xs) var(--mono);cursor:pointer} .next-cockpit-memos textarea{grid-column:1/-1;display:block;width:100%;min-height:64px;resize:vertical;padding:9px;border:1px solid var(--line);border-radius:0;background:var(--panel);color:var(--ink);font:inherit;line-height:1.45} .next-cockpit-memos label>small{grid-column:1/3} @@ -1080,7 +1080,13 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-recovery [data-next-cockpit-task-known]>strong,.next-cockpit-recovery .next-cockpit-recovery-evidence>strong{color:var(--ink)} .next-cockpit-recovery>div{font:500 var(--fs-sentence)/1.55 var(--sans)} .next-cockpit-recovery span{font-family:var(--mono)} -.next-cockpit-recovery strong,.next-cockpit-recovery small{font-size:var(--fs-xs)} +/* A stated absence must never read larger than the fact it stands in for. + Lowering the absence back to 12.5px is the other way out and is refused: + it reinstates the sub-floor sentence. The value keeps mono and --ink, so + it still outranks its absence on family and tone. `small` is the storage + cue, not a value, and stays down. */ +.next-cockpit-recovery strong{font-size:var(--fs-sentence)} +.next-cockpit-recovery small{font-size:var(--fs-xs)} .next-cockpit-recovery-evidence>strong,.next-cockpit-execution-root>strong,.next-cockpit-child-row>strong, .next-cockpit-recovery [data-next-cockpit-memo-field]>strong,.next-cockpit-recovery textarea, .next-course-episode p,.next-course-episode ul,.next-course-direction p{font-family:var(--mono)} @@ -1097,7 +1103,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-content .next-course-evidence{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-content .next-course-evidence .next-cockpit-source{display:inline;font:inherit;font-family:var(--mono)} .next-cockpit-recovery .next-project-goal{margin-top:12px;padding:10px 0;border:0;border-top:1px solid var(--line);border-radius:0;background:transparent} -.next-cockpit-recovery .next-project-goal-text{font:var(--fs-xs)/1.55 var(--mono)} +.next-cockpit-recovery .next-project-goal-text{font:var(--fs-sentence)/1.55 var(--mono)} .next-cockpit-recovery .next-project-goal-text.next-project-value--absent, .next-cockpit-recovery .next-project-goal-gap{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-recovery .next-project-goal-source{font-size:var(--fs-2xs)} @@ -1185,7 +1191,8 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-terminal-bar>span{margin-left:auto;color:var(--ink3)} .pc-terminal-identity{flex-basis:100%;min-width:0} .pc-terminal-identity p{margin:3px 0} -.pc-terminal-identity code{margin-right:10px} +.pc-terminal-identity strong{font-size:var(--fs-sentence)} +.pc-terminal-identity code{margin-right:10px;font-size:var(--fs-sentence)} .pc-terminal-viewport{height:340px;overflow:auto;overscroll-behavior:contain} .pc-terminal-screen{min-width:100%;min-height:340px;padding:6px;box-sizing:content-box;font:500 var(--fs-sentence)/1.55 var(--sans)} .pc-terminal-screen>.xterm{width:100%;height:100%} diff --git a/cargento/skills/cargento/tests/test_focus.py b/cargento/skills/cargento/tests/test_focus.py index 7ef0848a4..eb9e0cb53 100644 --- a/cargento/skills/cargento/tests/test_focus.py +++ b/cargento/skills/cargento/tests/test_focus.py @@ -1021,7 +1021,7 @@ def test_the_pinned_assembly_is_untouched(self) -> None: # would fail on the reader rather than on an injected token. self.assertNotIn(b' self.assertNotIn("var(--mono)", rule) +class AnAbsenceNeverOutranksTheValueItReplacesTest(unittest.TestCase): + """DRC-4587 F1. Raising a container to the sentence tier without raising the + value rules inside it leaves the absence larger than the fact it stands in + for, which is the inversion this milestone exists to remove: "you can tell a + label from its answer, a figure from a gap". + + Read off the stylesheet rather than a live board because three of these + panels need data the fixtures do not carry, and the cascade is what decides + the outcome either way. + """ + + # value selector -> the absence or caption drawn beside it in the same cell. + PAIRS = ( + (".next-cockpit-recovery strong", ".next-cockpit-recovery .next-project-value--absent"), + ( + ".next-cockpit-recovery .next-project-goal-text", + ".next-cockpit-recovery .next-project-goal-gap", + ), + (".next-cockpit-now-state strong", ".next-cockpit-now-state small"), + ( + ".next-cockpit-memos [data-next-cockpit-memo-field]>strong", + ".next-cockpit-memos label>small", + ), + (".pc-terminal-identity code", ".pc-terminal-identity p"), + (".pc-terminal-identity strong", ".pc-terminal-identity p"), + (".next-attention-open strong", ".next-attention-open p"), + ) + + css: str + tokens: dict[str, float] + + @classmethod + def setUpClass(cls) -> None: + source = ( + pathlib.Path(__file__).resolve().parents[1] / "cargento_runtime" / "web" / "styles.css" + ).read_text(encoding="utf-8") + # Comments first: the block scan below reads everything before a `{` as + # the selector, so a comment above a rule silently detaches it and the + # rule reads as undeclared rather than as wrong. + cls.css = re.sub(r"/\*.*?\*/", "", source, flags=re.DOTALL) + cls.tokens = { + name: float(value) + for name, value in re.findall(r"--(fs-[a-z0-9-]+):([0-9.]+)px", cls.css) + } + + def resolved_px(self, selector: str) -> float | None: + """The size the cascade lands on: last declaration for this exact + selector wins, which holds here because every rule in `PAIRS` is a + single class or one descendant step. + """ + found: float | None = None + for block in re.finditer(r"([^{}]+)\{([^{}]*)\}", self.css): + if selector not in [head.strip() for head in block.group(1).split(",")]: + continue + body = block.group(2) + token = re.search(r"font-size:var\(--(fs-[a-z0-9-]+)\)", body) or re.search( + r"font:(?:\d+ )?var\(--(fs-[a-z0-9-]+)\)", body + ) + if token: + found = self.tokens[token.group(1)] + continue + literal = re.search(r"font-size:([0-9.]+)px", body) or re.search( + r"font:(?:\d+ )?([0-9.]+)px", body + ) + if literal: + found = float(literal.group(1)) + return found + + def test_no_value_is_drawn_smaller_than_its_absence_or_caption(self) -> None: + for value, companion in self.PAIRS: + with self.subTest(value=value): + value_px = self.resolved_px(value) + companion_px = self.resolved_px(companion) + self.assertIsNotNone( + companion_px, f"{companion} declares no size to compare against" + ) + self.assertIsNotNone( + value_px, + f"{value} declares no size, so it inherits one smaller than {companion}", + ) + assert value_px is not None and companion_px is not None + self.assertGreaterEqual( + value_px, + companion_px, + f"{value} is {value_px}px against {companion} at {companion_px}px: " + "the absence outranks the fact it replaces", + ) + + @unittest.skipUnless(shutil.which("node"), "node not available") class CockpitReadingShapeTest(NextPageJsHarness): """DEC-17's seven rules, one case each (DRC-4511 AC4). diff --git a/cargento/skills/cargento/tests/test_next_flag.py b/cargento/skills/cargento/tests/test_next_flag.py index 9809805c8..b4902a6fc 100644 --- a/cargento/skills/cargento/tests/test_next_flag.py +++ b/cargento/skills/cargento/tests/test_next_flag.py @@ -64,9 +64,9 @@ def test_retired_next_query_is_not_a_page_alias(self) -> None: def test_the_canonical_loader_is_the_released_ui_bundle(self) -> None: page = frontend_page.load_page() - self.assertEqual(914_344, len(page)) + self.assertEqual(914_827, len(page)) self.assertEqual( - "2165bf68d82b7f98853561d56b3f0300240095a964bd040d74b080de45080ce6", + "9a29a48f99fba7eee3ea0ab0b94c40cf332e94cb4eaff14d58d7c468375776ea", hashlib.sha256(page).hexdigest(), ) diff --git a/cargento/skills/cargento/tests/test_next_page.py b/cargento/skills/cargento/tests/test_next_page.py index 82116cd87..af6c5ebed 100644 --- a/cargento/skills/cargento/tests/test_next_page.py +++ b/cargento/skills/cargento/tests/test_next_page.py @@ -700,16 +700,16 @@ def test_load_page_preserves_its_byte_oracles(self) -> None: self.assertEqual(digest, hashlib.sha256(data).hexdigest()) styles = frontend_page.asset_path("styles.css").read_bytes() - self.assertEqual(111_050, len(styles)) + self.assertEqual(111_533, len(styles)) self.assertEqual( - "91a303b22906b2f94fcd0f023a4c001388e840efa9f245d08e22822b1180b754", + "0cdd948320e546ec9b589103f95f080c75a700093f6e39dafcdcb4be0fea7799", hashlib.sha256(styles).hexdigest(), ) assembled = frontend_page.load_page() - self.assertEqual(914_344, len(assembled)) + self.assertEqual(914_827, len(assembled)) self.assertEqual( - "2165bf68d82b7f98853561d56b3f0300240095a964bd040d74b080de45080ce6", + "9a29a48f99fba7eee3ea0ab0b94c40cf332e94cb4eaff14d58d7c468375776ea", hashlib.sha256(assembled).hexdigest(), ) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index 1c3632b0b..ebafef2de 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -110,13 +110,23 @@ region; a shared responsive block would make every view edit the same tail. Movi regions is an ownership change, not incidental cleanup. Board sentences have a 15px floor (`--fs-sentence`), at weight 500 and line-height 1.55. Labels, -identifiers, timestamps, rates and compact controls sit on one tier, `--fs-label` at 11px; the -four steps that ran 9px, 9.5px, 10px and 10.5px were a 3px band nobody can rank, and three of the -six sub-12px tokens had no callers at all. The floor left 12.5px because the audit put `--ink`, -the brightest colour in the palette, below the body-text requirement there: brightening the ink -was not available, so size was the only lever left. The absence explanations the prototype placed -at 10px are sentences, so they take the sentence tier, and two of them had to leave an `

`'s -`
` to get there. The stylesheet +identifiers, timestamps, rates and compact controls sit on one tier, `--fs-label` at 11px. Four +`:root` steps used to sit below 11px, at 9px, 9.5px, 10px and 10.5px, a 3px band nobody can rank, +and 9px was the smallest step in the file. Of the six tokens at or below 12px, three had no callers +anywhere: `--fs-column`, `--fs-meta` and `--fs-breadcrumb`. (`--fs-breadcrumb` is exactly 12px, so +a reader counting strictly below 12px finds two. `--fs-meta-detail` was never one of them; it had +four callers and was folded into `--fs-label`.) + +The floor left 12.5px on the strength of an APCA reading that put `--ink`, the brightest colour in +the palette, below the body-text requirement there. **Those figures are audit-only: no APCA +implementation, table or fixture exists in this repository, so they cannot be reproduced from the +tree**, which is the same caveat the milestone carries. What the tree does say is that this was +never a contrast defect: `--ink` on `--bg` recomputes to 16.36:1 and the asset test asserting more +than 4.5:1 is green. DRC-4596 adds the guardrail that would close the gap, and lands after the +change the figures justify. The absence explanations the prototype placed at 10px are sentences, so +they take the sentence tier, and two of them had to leave an `

`'s `
` to get there. +A value is never drawn smaller than the absence or caption beside it in the same cell, which is +what raising a container without raising its value rules quietly breaks. The stylesheet retains scale tokens and literal sizes. The asset test pins the dark palette and checks text inks above 4.5:1 on the ground, panel and inset surfaces; it does not enforce all font sizes or spacing between contrast steps. @@ -131,11 +141,18 @@ eight carry `overflow-wrap:anywhere`, eight are layout boxes rather than single `.next-cockpit-content` and `.next-cockpit-recovery>div` are the prose containers, where 540px would clamp the cards inside them instead of the sentences. -**The floor is not yet universal, and this is the gap to close next.** Seventeen further sans rules -pass the same test at 13px to 14.5px, on the `--fs-body`, `--fs-summary` and `--fs-sm` steps plus -two literals. They were left alone: raising them is another seventeen rules of review surface, and -two of the seventeen are not sentences at all (a textarea and the prototype terminal), so the set -needs reading one selector at a time rather than a sweep. +**The floor is not yet universal, and this is the gap DRC-4602 sizes.** Nineteen further sans +rules pass the same test at 13px to 14.5px: seven on `--fs-sm`, four on `--fs-body`, two on +`--fs-summary`, four literals (one 14.5px, two 13.5px, one 14px), and **two that no single-rule +census can see**, because their size, family and line-height are composed across three rules each. +Those two are `.next-operation-fact--unknown strong`, which takes 12.5px and mono from one rule, a +flip back to sans from a second and its line-height from a third, and `.next-cockpit-recovery +small`, which takes 12.5px from its own rule and inherits sans and the line-height from the cell. +A census that reads one rule at a time reports seventeen and misses both. + +They were left alone: raising them is another nineteen rules of review surface, and two of the +nineteen are not sentences at all (a textarea and the prototype terminal), so the set needs reading +one selector at a time rather than a sweep. Twenty-four declarations carry `.09em`: the whole `.13em` and `.14em` groups, plus eight of the eleven in the `.1em` and `.08em` groups. The other three of those eleven keep their own value From e32b207d2d3d72087919ee0ab669ff96f31860c0 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 19:51:49 +0800 Subject: [PATCH 11/15] fix(web): raise the six slots whose absence outranked their value Correction round 2. The defect is slot-shaped, not selector-shaped: a value and the absence that replaces it are chosen by a ternary, so they never co-exist in one render. No sweep of co-existing selectors sees the pair, and neither does reading a populated board, because only one branch is ever on screen. Round 1 swept selectors, passed, and missed all six of these. Measured in a browser before and after, both branches built separately from each emitter's own markup: - recovery published value, 11.5 -> 15px, against a 15px absence - rail wait duration, 11 -> 15px - rail token rate, 10.5 -> 15px - rail pace and resets, 11 -> 15px - event time, 11 -> 15px - published value, 12.5 -> 15px Two of round 1's nine exemptions were wrong on exactly this point, and both are in that list: `.next-cockpit-recovery span` was read as a caption when it also sizes every nextProjectValue span, and `.pc-entry-details time` as a timestamp when projectEventTime swaps it for a reason. A rule is a caption only when nothing chooses it as the answer, and only the emitter can say so. Also fixes the caption regression round 1 introduced: `.next-cockpit-recovery strong` is a descendant selector and caught the briefing's own ``, so PROJECT RECOVERY BRIEFING rendered at 15px, level with every value it labels. Scoped to the value slot, it computes 11.5px again. The invariant test is now slot-shaped. It lists the ternaries and resolves each branch through tests/css_cascade.py, a restricted cascade resolver whose every figure was checked against getComputedStyle in a browser before it was trusted. Both halves survive a mutation check. The old test pinned seven co-existing pairs and would have stayed green through all six of these. Prose corrected against the tree: the deferred gap is twenty-one rules at 12.5px to 14.5px, not nineteen at 13px, because the earlier census used a `font:` shorthand pattern that matched the weight instead of the size and silently dropped two rules; the sub-12px band holds seven tokens once `--fs-2xs` is counted, and the paragraph now states the inclusive reading it uses rather than mixing two. Seven byte pins regenerated. No JS changed this round, so next-cockpit.js's size and digest were verified unchanged rather than rewritten. Signed-off-by: Jared Scott --- .../cargento/cargento_runtime/web/styles.css | 12 +- cargento/skills/cargento/tests/css_cascade.py | 198 ++++++++++++++ cargento/skills/cargento/tests/test_focus.py | 2 +- .../cargento/tests/test_next_cockpit.py | 244 +++++++++++++----- .../skills/cargento/tests/test_next_flag.py | 4 +- .../skills/cargento/tests/test_next_page.py | 8 +- docs/design-next-ui.md | 54 ++-- 7 files changed, 424 insertions(+), 98 deletions(-) create mode 100644 cargento/skills/cargento/tests/css_cascade.py diff --git a/cargento/skills/cargento/cargento_runtime/web/styles.css b/cargento/skills/cargento/cargento_runtime/web/styles.css index f8223ab38..4dce63842 100644 --- a/cargento/skills/cargento/cargento_runtime/web/styles.css +++ b/cargento/skills/cargento/cargento_runtime/web/styles.css @@ -306,7 +306,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-delegation progress::-webkit-progress-bar{background:var(--line)} .next-delegation progress::-webkit-progress-value{background:var(--accent-dim)} .next-delegation progress::-moz-progress-bar{background:var(--accent-dim)} -.next-delegation-metrics{display:flex;flex-wrap:wrap;gap:4px 9px;color:var(--ink2);font:10.5px/1.5 var(--mono)} +.next-delegation-metrics{display:flex;flex-wrap:wrap;gap:4px 9px;color:var(--ink2);font:var(--fs-sentence)/1.5 var(--mono)} .next-delegation-withheld{display:flex;flex-direction:column;gap:6px;color:var(--ink2)} .next-delegation-withheld strong{font:700 16px var(--mono)} .next-delegation-withheld small{color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} @@ -315,7 +315,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-rail-wait-heading{display:flex;align-items:baseline;gap:9px} .next-rail-wait-heading a{min-width:0;color:var(--ink);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;overflow-wrap:anywhere;text-decoration:none} .next-rail-wait-heading a:hover{text-decoration:underline} -.next-rail-wait-duration{margin-left:auto;flex-shrink:0;color:var(--amber);font:700 11px var(--mono)} +.next-rail-wait-duration{margin-left:auto;flex-shrink:0;color:var(--amber);font:700 var(--fs-sentence) var(--mono)} .next-rail-question{margin:0;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;overflow-wrap:anywhere} .next-rail-wait-controls{display:flex;flex-wrap:wrap;gap:7px;padding-top:2px} .next-rail-wait-controls button{margin-top:0;padding:5px 10px;border-radius:6px;font-size:var(--fs-label)} @@ -331,7 +331,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-rail-capacity-fill--amber{background:var(--amber)} .next-rail-capacity-fill--unknown{background:var(--ink3)} .next-rail-capacity-tick{position:absolute;top:-2px;bottom:-2px;width:1.5px;background:var(--ink2)} -.next-rail-capacity-caption{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;color:var(--ink3);font:var(--fs-label) var(--mono)} +.next-rail-capacity-caption{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;color:var(--ink3);font:var(--fs-sentence) var(--mono)} .next-project-detail-rail .next-usage-consent{margin:0;padding:12px;font-size:var(--fs-xs)} .next-project-detail-rail .next-usage-consent-note{font-family:var(--sans);font-size:var(--fs-xs)} .next-project-detail-rail .next-usage-switch{margin:0;font-size:var(--fs-xs)} @@ -835,6 +835,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-recovery-memos textarea{grid-column:1/-1;width:100%;min-height:54px;resize:vertical;padding:7px;border:1px solid var(--line2);background:var(--panel);color:var(--ink);font:inherit} .next-cockpit-recovery span,.next-cockpit-recovery strong,.next-cockpit-recovery small{display:block} .next-cockpit-recovery span{margin-bottom:5px;color:var(--ink3);font-size:var(--fs-2xs);letter-spacing:.09em} +.next-cockpit-recovery .next-project-value{font-size:var(--fs-sentence);letter-spacing:0} .next-cockpit-recovery strong{font-weight:500;overflow-wrap:anywhere} .next-cockpit-recovery small{margin-top:4px;color:var(--ink3);overflow-wrap:anywhere} .next-cockpit-recovery details{margin-top:5px;color:var(--ink3)} @@ -1085,7 +1086,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o it reinstates the sub-floor sentence. The value keeps mono and --ink, so it still outranks its absence on family and tone. `small` is the storage cue, not a value, and stays down. */ -.next-cockpit-recovery strong{font-size:var(--fs-sentence)} +.next-cockpit-recovery div strong{font-size:var(--fs-sentence)} .next-cockpit-recovery small{font-size:var(--fs-xs)} .next-cockpit-recovery-evidence>strong,.next-cockpit-execution-root>strong,.next-cockpit-child-row>strong, .next-cockpit-recovery [data-next-cockpit-memo-field]>strong,.next-cockpit-recovery textarea, @@ -1176,7 +1177,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-trail-summary,.pc-trail-summary>span{display:block} .pc-entry-details{display:grid;gap:7px;margin-top:9px;padding:10px 12px;border-left:2px solid var(--line2);background:var(--sunk);color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} .pc-entry-details b{color:var(--ink2);font:var(--fs-machine) var(--mono)} -.pc-entry-details time,.pc-event-evidence time{font:var(--fs-machine) var(--mono)} +.pc-entry-details time,.pc-event-evidence time{font:var(--fs-sentence) var(--mono)} .pc-entry-source-list{display:grid;gap:6px;margin-top:6px} .pc-trail-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px} .pc-trail-top strong{color:var(--ink);font-size:var(--fs-body)} @@ -1192,6 +1193,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-terminal-identity{flex-basis:100%;min-width:0} .pc-terminal-identity p{margin:3px 0} .pc-terminal-identity strong{font-size:var(--fs-sentence)} +.pc-semantic-timeline .pc-source{font-size:var(--fs-sentence)} .pc-terminal-identity code{margin-right:10px;font-size:var(--fs-sentence)} .pc-terminal-viewport{height:340px;overflow:auto;overscroll-behavior:contain} .pc-terminal-screen{min-width:100%;min-height:340px;padding:6px;box-sizing:content-box;font:500 var(--fs-sentence)/1.55 var(--sans)} diff --git a/cargento/skills/cargento/tests/css_cascade.py b/cargento/skills/cargento/tests/css_cascade.py new file mode 100644 index 000000000..e132b2cbb --- /dev/null +++ b/cargento/skills/cargento/tests/css_cascade.py @@ -0,0 +1,198 @@ +"""A restricted CSS cascade resolver, for asking what size a slot renders at. + +A value and the absence that replaces it never co-exist in one render: a ternary +in the emitter picks one class or the other. No sweep of co-existing selectors +and no reading of a populated board can see such a pair, so the only way to +compare them is to resolve each branch on its own. That is what this module is +for, and `AnAbsenceNeverOutranksTheValueItReplacesTest` is its caller. + +Restricted deliberately. It handles the selector forms this stylesheet uses, +descendant and child combinators over tags, classes and attribute presence, and +refuses sibling combinators rather than guessing. Every number it returns for +that test's census was checked against `getComputedStyle` in a real browser +before it was committed; a form it cannot express belongs here and gets checked +the same way, never approximated. + +`@media` blocks are dropped on purpose: the census is the default viewport, and +a rule applying at one width only would otherwise outrank the base rule and +report a size nobody sees. +""" + +from __future__ import annotations + +import pathlib +import re + +Rule = tuple[str, str, int] +Node = dict[str, object] +Compound = dict[str, object] + +_SIMPLE = re.compile(r"[.#][\w-]+|\[[^\]]+\]|::?[\w-]+(?:\([^)]*\))?") +_BLOCK = re.compile(r"([^{}]+)\{([^{}]*)\}") + + +def _strip_media(css: str) -> str: + out: list[str] = [] + index = 0 + while index < len(css): + if not css.startswith("@media", index): + out.append(css[index]) + index += 1 + continue + cursor = css.find("{", index) + depth = 0 + while cursor < len(css): + if css[cursor] == "{": + depth += 1 + elif css[cursor] == "}": + depth -= 1 + if depth == 0: + break + cursor += 1 + index = cursor + 1 + return "".join(out) + + +def load(path: pathlib.Path | str) -> tuple[dict[str, float], list[Rule]]: + """Return the `--fs-*` token table and every non-media rule, in source order.""" + source = pathlib.Path(path).read_text(encoding="utf-8") + body = _strip_media(re.sub(r"/\*.*?\*/", "", source, flags=re.DOTALL)) + tokens = { + name: float(value) for name, value in re.findall(r"--(fs-[a-z0-9-]+):([0-9.]+)px", body) + } + rules: list[Rule] = [] + for order, block in enumerate(_BLOCK.finditer(body)): + for selector in block.group(1).split(","): + cleaned = selector.strip() + if cleaned and not cleaned.startswith("@"): + rules.append((cleaned, block.group(2), order)) + return tokens, rules + + +def _compound(text: str) -> Compound: + tag: str | None = None + classes: set[str] = set() + attrs: set[str] = set() + ids = 0 + pseudo = 0 + name = re.match(r"^([a-zA-Z*][\w-]*)", text) + if name: + tag = name.group(1) + text = text[name.end() :] + for part in _SIMPLE.findall(text): + if part.startswith("."): + classes.add(part[1:]) + elif part.startswith("#"): + ids += 1 + elif part.startswith("["): + attr = re.match(r"\[([\w-]+)", part) + if attr: + attrs.add(attr.group(1)) + elif not part.startswith("::"): + pseudo += 1 + return { + "tag": None if tag == "*" else tag, + "classes": classes, + "attrs": attrs, + "ids": ids, + "weight": len(classes) + len(attrs) + pseudo, + } + + +def _steps(selector: str) -> list[tuple[str, Compound]] | None: + """Selector as [(combinator, compound)], the first combinator ignored.""" + parts = [p for p in re.split(r"\s*(>|\+|~)\s*|\s+", selector.strip()) if p] + if any(part in ("+", "~") for part in parts): + return None + steps: list[tuple[str, Compound]] = [] + combinator = " " + for part in parts: + if part == ">": + combinator = ">" + continue + steps.append((combinator, _compound(part))) + combinator = " " + return steps + + +def _node_matches(node: Node, compound: Compound) -> bool: + tag = compound["tag"] + if tag is not None and tag != node["tag"]: + return False + classes = compound["classes"] + attrs = compound["attrs"] + assert isinstance(classes, set) and isinstance(attrs, set) + node_classes = node["classes"] + node_attrs = node["attrs"] + assert isinstance(node_classes, set) and isinstance(node_attrs, set) + return classes <= node_classes and attrs <= node_attrs + + +def _walk(path: list[Node], ancestors: list[tuple[str, Compound]]) -> bool: + """Match the ancestor steps right to left; the leaf is already matched. + + Each entry pairs a compound with the combinator that joins it to the step on + its RIGHT, not the one on its left, which is the off-by-one that makes a + child combinator behave like a descendant one. + """ + index = len(path) - 2 + for combinator, compound in reversed(ancestors): + if combinator == ">": + if index < 0 or not _node_matches(path[index], compound): + return False + index -= 1 + continue + while index >= 0 and not _node_matches(path[index], compound): + index -= 1 + if index < 0: + return False + index -= 1 + return True + + +def matches(path: list[Node], selector: str) -> tuple[int, int, int] | None: + """Specificity if `selector` matches the leaf of `path`, else None.""" + steps = _steps(selector) + if not steps or not _node_matches(path[-1], steps[-1][1]): + return None + ancestors = [(steps[i + 1][0], steps[i][1]) for i in range(len(steps) - 1)] + if not _walk(path, ancestors): + return None + ids = sum(int(step[1]["ids"]) for step in steps) # type: ignore[call-overload] + weight = sum(int(step[1]["weight"]) for step in steps) # type: ignore[call-overload] + tags = sum(1 for step in steps if step[1]["tag"]) + return (ids, weight, tags) + + +def declared_size(body: str, tokens: dict[str, float]) -> float | None: + token = re.search(r"font-size:\s*var\(--(fs-[a-z0-9-]+)\)", body) or re.search( + r"font:[^;]*?var\(--(fs-[a-z0-9-]+)\)", body + ) + if token: + return tokens.get(token.group(1)) + # The lookbehind is load-bearing: without it `font:10.5px/1.5` matched the + # optional weight against `10` and the size resolved to 0.5px. + literal = re.search(r"font-size:\s*([0-9.]+)px", body) or re.search( + r"font:[^;{}]*?(? float | None: + """The font size the leaf of `path` renders at, inheriting where undeclared.""" + for depth in range(len(path), 0, -1): + subject = path[:depth] + best: tuple[tuple[tuple[int, int, int], int], float] | None = None + for selector, body, order in rules: + size = declared_size(body, tokens) + if size is None: + continue + specificity = matches(subject, selector) + if specificity is None: + continue + key = (specificity, order) + if best is None or key > best[0]: + best = (key, size) + if best is not None: + return best[1] + return None diff --git a/cargento/skills/cargento/tests/test_focus.py b/cargento/skills/cargento/tests/test_focus.py index eb9e0cb53..d3c3c54b4 100644 --- a/cargento/skills/cargento/tests/test_focus.py +++ b/cargento/skills/cargento/tests/test_focus.py @@ -1021,7 +1021,7 @@ def test_the_pinned_assembly_is_untouched(self) -> None: # would fail on the reader rather than on an injected token. self.assertNotIn(b' self.assertNotIn("var(--mono)", rule) +def _node(tag: str, *classes: str, attrs: tuple[str, ...] = ()) -> dict[str, object]: + return {"tag": tag, "classes": set(classes), "attrs": set(attrs)} + + +_REC = [_node("section", "next-cockpit-recovery")] +_RAIL = [_node("div", "next-project-detail-rail")] +_TL = [_node("div", "pc-semantic-timeline")] + + class AnAbsenceNeverOutranksTheValueItReplacesTest(unittest.TestCase): - """DRC-4587 F1. Raising a container to the sentence tier without raising the - value rules inside it leaves the absence larger than the fact it stands in - for, which is the inversion this milestone exists to remove: "you can tell a - label from its answer, a figure from a gap". - - Read off the stylesheet rather than a live board because three of these - panels need data the fixtures do not carry, and the cascade is what decides - the outcome either way. + """DRC-4587 F1 and its correction round. A stated absence must never render + larger than the fact it stands in for: "you can tell a label from its + answer, a figure from a gap". + + **The pairs are read from the emitters, not from the stylesheet.** Each row + below is one ternary that picks a value class or an absence class, and the + two branches never co-exist in a render. A sweep of co-existing selectors + cannot see such a pair, and neither can a live board: only one branch is + ever on screen. Round 1 swept selectors, passed, and missed six of these. """ - # value selector -> the absence or caption drawn beside it in the same cell. - PAIRS = ( - (".next-cockpit-recovery strong", ".next-cockpit-recovery .next-project-value--absent"), + # (name, emitter, value path, absence path). Paths are the DOM the emitter + # actually builds; `css_cascade` resolves each branch on its own. + SLOTS = ( ( - ".next-cockpit-recovery .next-project-goal-text", - ".next-cockpit-recovery .next-project-goal-gap", + "recovery published value", + "next-cockpit.js nextCockpitWaitingCommand", + [ + *_REC, + _node("div", "next-cockpit-waiting"), + _node("span", "next-project-value", "next-project-value--known"), + ], + [ + *_REC, + _node("div", "next-cockpit-waiting"), + _node("span", "next-project-value", "next-project-value--absent"), + ], ), - (".next-cockpit-now-state strong", ".next-cockpit-now-state small"), ( - ".next-cockpit-memos [data-next-cockpit-memo-field]>strong", - ".next-cockpit-memos label>small", + "rail wait duration", + "next-delegation.js nextRailWaiting", + [ + *_RAIL, + _node("section", "next-rail-panel"), + _node("article", "next-rail-wait"), + _node("div", "next-rail-wait-heading"), + _node("span", "next-rail-wait-duration"), + ], + [ + *_RAIL, + _node("section", "next-rail-panel"), + _node("article", "next-rail-wait"), + _node("div", "next-rail-wait-heading"), + _node("span", "next-rail-reason"), + ], + ), + ( + "rail token rate", + "next-delegation.js nextRailDelegation", + [ + *_RAIL, + _node("section", "next-delegation"), + _node("div", "next-delegation-metrics"), + _node("span", attrs=("data-next-delegation-rate",)), + ], + [ + *_RAIL, + _node("section", "next-delegation"), + _node("div", "next-delegation-metrics"), + _node("span", "next-rail-reason", attrs=("data-next-delegation-rate-withheld",)), + ], + ), + ( + "rail pace and resets", + "next-delegation.js nextRailCapacityWindow", + [ + *_RAIL, + _node("div", "next-rail-capacity-window"), + _node("div", "next-rail-capacity-caption"), + _node("span"), + ], + [ + *_RAIL, + _node("div", "next-rail-capacity-window"), + _node("div", "next-rail-capacity-caption"), + _node("span", "next-rail-reason"), + ], + ), + ( + "event time", + "project.js projectEventTime", + [ + *_TL, + _node("div", "pc-entry-details"), + _node("div"), + _node("time", attrs=("datetime",)), + ], + [ + *_TL, + _node("div", "pc-entry-details"), + _node("div"), + _node("span", "pc-substrate-reason"), + ], + ), + ( + "published value", + "project.js projectPublishedValue", + [*_TL, _node("div", "pc-entry-details"), _node("div"), _node("span", "pc-source")], + [ + *_TL, + _node("div", "pc-entry-details"), + _node("div"), + _node("span", "pc-substrate-reason"), + ], + ), + ( + "terminal identity window", + "project.js projectTerminalIdentity", + [_node("div", "pc-terminal-identity"), _node("code")], + [_node("div", "pc-terminal-identity"), _node("p")], + ), + ( + "terminal identity session name", + "project.js projectTerminalIdentity", + [_node("div", "pc-terminal-identity"), _node("strong")], + [_node("div", "pc-terminal-identity"), _node("p")], + ), + ( + "recovery assignment", + "next-cockpit.js nextCockpitRecovery", + [*_REC, _node("div"), _node("strong")], + [*_REC, _node("div"), _node("span", "next-project-value--absent")], + ), + ( + "recovery goal", + "next-cockpit.js nextProjectGoal", + [*_REC, _node("p", "next-project-goal-text")], + [*_REC, _node("p", "next-project-goal-gap")], ), - (".pc-terminal-identity code", ".pc-terminal-identity p"), - (".pc-terminal-identity strong", ".pc-terminal-identity p"), - (".next-attention-open strong", ".next-attention-open p"), ) - css: str - tokens: dict[str, float] + # A caption labels the values beside it, so it must stay below them. The + # round-1 fix used a descendant selector and pulled this one level with the + # strip it labels, which is the same collapse of register the other way up. + CAPTION_BELOW_VALUE = ( + ( + "briefing caption", + [*_REC, _node("header"), _node("strong")], + [*_REC, _node("div"), _node("strong")], + ), + ) @classmethod def setUpClass(cls) -> None: - source = ( + cls.tokens, cls.rules = css_cascade.load( pathlib.Path(__file__).resolve().parents[1] / "cargento_runtime" / "web" / "styles.css" - ).read_text(encoding="utf-8") - # Comments first: the block scan below reads everything before a `{` as - # the selector, so a comment above a rule silently detaches it and the - # rule reads as undeclared rather than as wrong. - cls.css = re.sub(r"/\*.*?\*/", "", source, flags=re.DOTALL) - cls.tokens = { - name: float(value) - for name, value in re.findall(r"--(fs-[a-z0-9-]+):([0-9.]+)px", cls.css) - } + ) - def resolved_px(self, selector: str) -> float | None: - """The size the cascade lands on: last declaration for this exact - selector wins, which holds here because every rule in `PAIRS` is a - single class or one descendant step. - """ - found: float | None = None - for block in re.finditer(r"([^{}]+)\{([^{}]*)\}", self.css): - if selector not in [head.strip() for head in block.group(1).split(",")]: - continue - body = block.group(2) - token = re.search(r"font-size:var\(--(fs-[a-z0-9-]+)\)", body) or re.search( - r"font:(?:\d+ )?var\(--(fs-[a-z0-9-]+)\)", body - ) - if token: - found = self.tokens[token.group(1)] - continue - literal = re.search(r"font-size:([0-9.]+)px", body) or re.search( - r"font:(?:\d+ )?([0-9.]+)px", body - ) - if literal: - found = float(literal.group(1)) - return found - - def test_no_value_is_drawn_smaller_than_its_absence_or_caption(self) -> None: - for value, companion in self.PAIRS: - with self.subTest(value=value): - value_px = self.resolved_px(value) - companion_px = self.resolved_px(companion) - self.assertIsNotNone( - companion_px, f"{companion} declares no size to compare against" - ) - self.assertIsNotNone( - value_px, - f"{value} declares no size, so it inherits one smaller than {companion}", - ) - assert value_px is not None and companion_px is not None + tokens: dict[str, float] + rules: list[tuple[str, str, int]] + + def size(self, path: list[dict[str, object]]) -> float: + resolved = css_cascade.resolve(path, self.tokens, self.rules) + self.assertIsNotNone(resolved, f"no size resolves for {path}") + assert resolved is not None + return resolved + + def test_no_slot_draws_its_value_smaller_than_its_absence(self) -> None: + for name, emitter, value_path, absence_path in self.SLOTS: + with self.subTest(slot=name): + value, absence = self.size(value_path), self.size(absence_path) self.assertGreaterEqual( - value_px, - companion_px, - f"{value} is {value_px}px against {companion} at {companion_px}px: " - "the absence outranks the fact it replaces", + value, + absence, + f"{name} ({emitter}): value {value}px against absence {absence}px, " + "so the absence outranks the fact it replaces", + ) + + def test_a_caption_stays_below_the_values_it_labels(self) -> None: + for name, caption_path, value_path in self.CAPTION_BELOW_VALUE: + with self.subTest(caption=name): + caption, value = self.size(caption_path), self.size(value_path) + self.assertLess( + caption, + value, + f"{name}: caption {caption}px against value {value}px, " + "so the label reads level with its own answer", ) diff --git a/cargento/skills/cargento/tests/test_next_flag.py b/cargento/skills/cargento/tests/test_next_flag.py index b4902a6fc..e6561af88 100644 --- a/cargento/skills/cargento/tests/test_next_flag.py +++ b/cargento/skills/cargento/tests/test_next_flag.py @@ -64,9 +64,9 @@ def test_retired_next_query_is_not_a_page_alias(self) -> None: def test_the_canonical_loader_is_the_released_ui_bundle(self) -> None: page = frontend_page.load_page() - self.assertEqual(914_827, len(page)) + self.assertEqual(915_014, len(page)) self.assertEqual( - "9a29a48f99fba7eee3ea0ab0b94c40cf332e94cb4eaff14d58d7c468375776ea", + "3749774c3299c7a7a9a0a5eddd9b0630118116f66e4cb8b1853bd11ea0e727ad", hashlib.sha256(page).hexdigest(), ) diff --git a/cargento/skills/cargento/tests/test_next_page.py b/cargento/skills/cargento/tests/test_next_page.py index af6c5ebed..efaa7c125 100644 --- a/cargento/skills/cargento/tests/test_next_page.py +++ b/cargento/skills/cargento/tests/test_next_page.py @@ -700,16 +700,16 @@ def test_load_page_preserves_its_byte_oracles(self) -> None: self.assertEqual(digest, hashlib.sha256(data).hexdigest()) styles = frontend_page.asset_path("styles.css").read_bytes() - self.assertEqual(111_533, len(styles)) + self.assertEqual(111_720, len(styles)) self.assertEqual( - "0cdd948320e546ec9b589103f95f080c75a700093f6e39dafcdcb4be0fea7799", + "6199c635186bb39da123b727cb4351e586694f7dfd35d017ed43f4c417a5e1fa", hashlib.sha256(styles).hexdigest(), ) assembled = frontend_page.load_page() - self.assertEqual(914_827, len(assembled)) + self.assertEqual(915_014, len(assembled)) self.assertEqual( - "9a29a48f99fba7eee3ea0ab0b94c40cf332e94cb4eaff14d58d7c468375776ea", + "3749774c3299c7a7a9a0a5eddd9b0630118116f66e4cb8b1853bd11ea0e727ad", hashlib.sha256(assembled).hexdigest(), ) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index ebafef2de..e89f2e367 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -112,10 +112,12 @@ regions is an ownership change, not incidental cleanup. Board sentences have a 15px floor (`--fs-sentence`), at weight 500 and line-height 1.55. Labels, identifiers, timestamps, rates and compact controls sit on one tier, `--fs-label` at 11px. Four `:root` steps used to sit below 11px, at 9px, 9.5px, 10px and 10.5px, a 3px band nobody can rank, -and 9px was the smallest step in the file. Of the six tokens at or below 12px, three had no callers -anywhere: `--fs-column`, `--fs-meta` and `--fs-breadcrumb`. (`--fs-breadcrumb` is exactly 12px, so -a reader counting strictly below 12px finds two. `--fs-meta-detail` was never one of them; it had -four callers and was folded into `--fs-label`.) +and 9px was the smallest step in the file. **Counts here read "at or below 12px" inclusively**, so +the band held seven tokens: `--fs-column` 9px, `--fs-label` 9.5px, `--fs-meta` 10px, +`--fs-meta-detail` 10.5px, `--fs-machine` 11px, `--fs-2xs` 11.5px and `--fs-breadcrumb` 12px. Three +of the seven had no callers anywhere and were deleted: `--fs-column`, `--fs-meta` and +`--fs-breadcrumb`. `--fs-meta-detail` had four callers and was folded into `--fs-label`; `--fs-2xs` +has forty-one and stays. The issue said six because it omitted `--fs-2xs`. The floor left 12.5px on the strength of an APCA reading that put `--ink`, the brightest colour in the palette, below the body-text requirement there. **Those figures are audit-only: no APCA @@ -125,8 +127,22 @@ never a contrast defect: `--ink` on `--bg` recomputes to 16.36:1 and the asset t than 4.5:1 is green. DRC-4596 adds the guardrail that would close the gap, and lands after the change the figures justify. The absence explanations the prototype placed at 10px are sentences, so they take the sentence tier, and two of them had to leave an `

`'s `
` to get there. -A value is never drawn smaller than the absence or caption beside it in the same cell, which is -what raising a container without raising its value rules quietly breaks. The stylesheet +A value is never drawn smaller than the absence that replaces it, nor than the caption that labels +it, which is what raising a container without raising its value rules quietly breaks. + +**Finding that class needs a census taken from the emitters, not from the stylesheet.** A value and +its absence are chosen by a ternary, so they never co-exist in one render: no sweep of co-existing +selectors sees the pair, and neither does reading a populated board, because only one branch is +ever on screen. Two passes over this stylesheet missed the same six slots for that reason before a +per-slot reading found them. `AnAbsenceNeverOutranksTheValueItReplacesTest` now lists the ternaries +and resolves both branches separately through `tests/css_cascade.py`; its numbers were checked +against `getComputedStyle` in a browser before they were trusted. Two exemptions in the first pass +were wrong on exactly this point: `.next-cockpit-recovery span` was read as a caption when it also +sizes every `nextProjectValue` span, and `.pc-entry-details time` as a timestamp when +`projectEventTime` swaps it for a reason. A rule is a caption only when nothing chooses it as the +answer, and only the emitter can say so: `.next-cockpit-recovery>header` qualifies because it holds +one fixed string, and `.next-usage-consent` is not in the census at all because it has no absence +branch to be paired with. The stylesheet retains scale tokens and literal sizes. The asset test pins the dark palette and checks text inks above 4.5:1 on the ground, panel and inset surfaces; it does not enforce all font sizes or spacing between contrast steps. @@ -141,18 +157,26 @@ eight carry `overflow-wrap:anywhere`, eight are layout boxes rather than single `.next-cockpit-content` and `.next-cockpit-recovery>div` are the prose containers, where 540px would clamp the cards inside them instead of the sentences. -**The floor is not yet universal, and this is the gap DRC-4602 sizes.** Nineteen further sans -rules pass the same test at 13px to 14.5px: seven on `--fs-sm`, four on `--fs-body`, two on -`--fs-summary`, four literals (one 14.5px, two 13.5px, one 14px), and **two that no single-rule -census can see**, because their size, family and line-height are composed across three rules each. +**The floor is not yet universal, and this is the gap DRC-4602 sizes.** Twenty-one further sans +rules pass the same test, at 12.5px to 14.5px: nineteen a single-rule census finds (seven on +`--fs-sm`, four on `--fs-body`, two on `--fs-summary`, six literals) and **two it cannot see**, +because their size, family and line-height are composed across three rules each. Those two are `.next-operation-fact--unknown strong`, which takes 12.5px and mono from one rule, a flip back to sans from a second and its line-height from a third, and `.next-cockpit-recovery small`, which takes 12.5px from its own rule and inherits sans and the line-height from the cell. -A census that reads one rule at a time reports seventeen and misses both. - -They were left alone: raising them is another nineteen rules of review surface, and two of the -nineteen are not sentences at all (a textarea and the prototype terminal), so the set needs reading -one selector at a time rather than a sweep. +They are also the two that set the bottom of the range. + +The count was reported as nineteen and before that as seventeen, and both were wrong for reasons +worth keeping. Seventeen came from a census whose `font:` shorthand pattern matched the weight +instead of the size, so `font:13px/1.5` read as no size at all and two rules +(`.next-cockpit-work-derived` and the reading-result group) fell out silently. Nineteen then +counted those two back in but treated the composed pair as part of the nineteen rather than beside +it. A census that cannot fail loudly on a shorthand it does not understand will keep producing +plausible totals. + +They were left alone: raising them is another twenty-one rules of review surface, and two of the +twenty-one are not sentences at all (a textarea and the prototype terminal), so the set needs +reading one selector at a time rather than a sweep. Twenty-four declarations carry `.09em`: the whole `.13em` and `.14em` groups, plus eight of the eleven in the `.1em` and `.08em` groups. The other three of those eleven keep their own value From 3ab52025c86c27e3621423414d6ebc8c09f6c3a3 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 19:55:41 +0800 Subject: [PATCH 12/15] docs(design): define the sentence tier over resolved style, not one rule The membership test this branch authored defined the tier over a single rule's declarations, then named two composed cases beneath it as members. Those two fail its letter while passing its intent, so an audit written against the sentence returns clean while elements still render at 12.5px on screen. The definition and the disclosure under it contradicted each other, and the disclosure was right. Membership is now a property of the element's resolved style: sans with a prose line-height, arriving through the cascade, possibly inherited, possibly from different rules. The warning that a per-rule census understates the set stops being an exception bolted onto a rule that denies it and becomes what the rule predicts. The size of the remaining set is no longer stated here. Every figure this branch produced for it came from a per-rule census, so each is a floor rather than a count, and DRC-4602 owns the audit that can produce a real one. Also adds the rule that an element count carries the date and commit it was taken at. The same shape read 163 and then 183 forty minutes later on an unchanged stylesheet, because the board renders whatever sessions exist. A bare number reads as a property of the code when it is a property of an afternoon. The example now carries its own provenance, since a rule whose illustration breaks it teaches the opposite. Docs only: no CSS, so no byte pin moves. Signed-off-by: Jared Scott --- docs/design-next-ui.md | 68 +++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index e89f2e367..04e4d26eb 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -147,36 +147,50 @@ retains scale tokens and literal sizes. The asset test pins the dark palette and above 4.5:1 on the ground, panel and inset surfaces; it does not enforce all font sizes or spacing between contrast steps. -Which rules are sentences is a test, not a judgement call, so a reviewer argues with a list: -**a rule is on the sentence tier when it sets its text in sans and declares its own prose -line-height.** Mono is a string a source published, and a label carries no line-height of its own. -Sixty-five rules on the old `--fs-xs` step qualify, plus the two absence explanations above, and -all sixty-seven resolve to `var(--fs-sentence)`. Forty-nine of them also cap at `--measure` (540px, -about 72 characters at this tier). The eighteen that do not are the ones a cap would clamp wrongly: -eight carry `overflow-wrap:anywhere`, eight are layout boxes rather than single lines, and +What counts as a sentence is a test, not a judgement call, so a reviewer argues with a list: +**an element is on the sentence tier when its resolved style is sans with a prose line-height.** +Resolved, not declared. Both properties come through the cascade, either may be inherited from an +ancestor, and the two may arrive from different rules, so the element is the unit and a single rule +is not. Mono is a string a source published, and a label resolves to no prose line-height at all. + +That distinction is the whole difficulty. **A census that reads one rule at a time will understate +the set**, because it cannot see an element whose family, size and line-height are assembled from +three rules, and it will report clean while such an element still renders below the floor on +screen. Two of those are named below; how many exist, and what to do about them, is DRC-4602's. + +Sixty-five rules on the old `--fs-xs` step carry the whole declaration themselves, plus the two +absence explanations above, and all sixty-seven resolve to `var(--fs-sentence)`. Forty-nine of them +also cap at `--measure` (540px, about 72 characters at this tier). The eighteen that do not are +the ones a cap would clamp wrongly: eight carry `overflow-wrap:anywhere`, eight are layout boxes +rather than single lines, and `.next-cockpit-content` and `.next-cockpit-recovery>div` are the prose containers, where 540px would clamp the cards inside them instead of the sentences. -**The floor is not yet universal, and this is the gap DRC-4602 sizes.** Twenty-one further sans -rules pass the same test, at 12.5px to 14.5px: nineteen a single-rule census finds (seven on -`--fs-sm`, four on `--fs-body`, two on `--fs-summary`, six literals) and **two it cannot see**, -because their size, family and line-height are composed across three rules each. -Those two are `.next-operation-fact--unknown strong`, which takes 12.5px and mono from one rule, a -flip back to sans from a second and its line-height from a third, and `.next-cockpit-recovery -small`, which takes 12.5px from its own rule and inherits sans and the line-height from the cell. -They are also the two that set the bottom of the range. - -The count was reported as nineteen and before that as seventeen, and both were wrong for reasons -worth keeping. Seventeen came from a census whose `font:` shorthand pattern matched the weight -instead of the size, so `font:13px/1.5` read as no size at all and two rules -(`.next-cockpit-work-derived` and the reading-result group) fell out silently. Nineteen then -counted those two back in but treated the composed pair as part of the nineteen rather than beside -it. A census that cannot fail loudly on a shorthand it does not understand will keep producing -plausible totals. - -They were left alone: raising them is another twenty-one rules of review surface, and two of the -twenty-one are not sentences at all (a textarea and the prototype terminal), so the set needs -reading one selector at a time rather than a sweep. +**The floor is not yet universal, and DRC-4602 owns both the audit and the remainder.** Sans text +still resolves below 15px in two shapes. The first is a rule that declares a smaller size outright, +on the `--fs-sm`, `--fs-body` and `--fs-summary` steps and on a handful of literals. The second is +the composed kind the definition above warns about: `.next-operation-fact--unknown strong` takes +its size and mono family from one rule, a flip back to sans from a second and its line-height from +a third, and `.next-cockpit-recovery small` takes its size from its own rule and inherits sans and +the line-height from the cell around it. Neither appears in a census that reads one rule at a time, +and both render at 12.5px, below anything the first shape reaches. + +**The size of that set is not stated here**, because every figure this branch produced for it was +produced by a per-rule census and is therefore a floor rather than a count. One of them was wrong +for a second reason worth keeping: a `font:` shorthand pattern that matched the weight instead of +the size read `font:13px/1.5` as no size at all and dropped two rules silently. A census that +cannot fail loudly on a shorthand it does not understand will keep producing plausible totals. + +**Element counts carry the date and the commit they were taken at, or they do not belong here.** +The board renders whatever sessions exist, so one shape counted 163 elements and then 183 forty +minutes later on an unchanged stylesheet, both readings taken during this branch's review on +2026-09-17 and reported by the reviewer rather than measured here. A bare number reads as a +property of the code when it is a property of an afternoon. The stable unit is the rule, or the +element shape, never its population. + +They were left alone rather than swept: the set is comparable in size to the one this branch +already moved, and some of it is not sentences at all, a textarea and the prototype terminal among +them. It needs reading one element at a time, which is the work DRC-4602 carries. Twenty-four declarations carry `.09em`: the whole `.13em` and `.14em` groups, plus eight of the eleven in the `.1em` and `.08em` groups. The other three of those eleven keep their own value From 295b19ce775a5ac63eda6195d3cb694aba439ad4 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 20:27:54 +0800 Subject: [PATCH 13/15] fix(web): give machine-paired absences their value's tier, not the floor Correction round 3, structural. Three rounds fixed what each was handed and the population kept growing, because the set was never bounded. This closes the class rather than the instances. An absence that replaces a machine string now takes that string's tier: .next-cockpit-work-absent, .next-cockpit-reading-clause-absent and .pc-graph-time. Raising the value instead removes the inversion by breaking the register, and a rate, a timestamp and a duration are compact on purpose. Prose absences in the same panels keep the floor, .next-cockpit-work-limit and .next-cockpit-work-dropped among them, so that rule had to be split out of the declaration it shared. The discriminator is not a new taxonomy. It is the sans/mono register this document already owns: three of the four inversions pair a sans absence with a mono value. And every one of these slots already emits a class separating its machine-paired use from its prose-paired one, so this is CSS only, with no emitter change and no new vocabulary. Fixes the guard's fixture, which built a DOM the app never renders. next-project.js:396 wraps the cockpit in .next-cockpit-content, and without that ancestor an inverted row read clean. The recovery-goal row modelled a

where nextProjectGoal emits a nextProjectValue span, so it never exercised the rule added for it, and the graph-row row carried one of the two classes its emitter sets. Re-running the mutation checks afterwards found a survivor the old fixture had masked: reverting the recovery published value dropped value and absence together, leaving the comparison equal while the value left the tier the document records it on. A promotion test now binds that. Makes the resolver fail loudly. An unsupported selector raised None, which the caller read as "does not match", so a rule left the cascade in silence; it raises now. An always-true @media(min-width:1px) left the guard green, so a min-width block that sets a font size raises rather than being dropped. Records the promotion in docs/design-next-ui.md. The tier sentence claimed timestamps and rates sit at 11px while six machine slots render at 15px; it now names them and says why, and names the three that moved the other way instead. Seven byte pins regenerated. No JS changed, so next-cockpit.js is unchanged. Signed-off-by: Jared Scott --- .../cargento/cargento_runtime/web/styles.css | 10 +- cargento/skills/cargento/tests/css_cascade.py | 24 +++- cargento/skills/cargento/tests/test_focus.py | 2 +- .../cargento/tests/test_next_cockpit.py | 118 +++++++++++++++++- .../skills/cargento/tests/test_next_flag.py | 4 +- .../skills/cargento/tests/test_next_page.py | 8 +- docs/design-next-ui.md | 19 ++- 7 files changed, 168 insertions(+), 17 deletions(-) diff --git a/cargento/skills/cargento/cargento_runtime/web/styles.css b/cargento/skills/cargento/cargento_runtime/web/styles.css index 4dce63842..e90d8dfc7 100644 --- a/cargento/skills/cargento/cargento_runtime/web/styles.css +++ b/cargento/skills/cargento/cargento_runtime/web/styles.css @@ -897,7 +897,12 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-work-mix{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-work-at{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-cockpit-work-source{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} -.next-cockpit-work-absent,.next-cockpit-work-limit,.next-cockpit-work-dropped{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +/* An absence replacing a machine string takes that string's tier, not the + sentence one: raising a rate or a timestamp to 15px removes the inversion + by breaking the register. Prose absences keep the floor -- limit and + dropped below are the board's own sentences and stay there. */ +.next-cockpit-work-absent{margin:0;color:var(--ink3);font:var(--fs-xs)/1.55 var(--mono);max-width:var(--measure)} +.next-cockpit-work-limit,.next-cockpit-work-dropped{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} /* The third treatment, for a model reading: italic Space Grotesk on the secondary ink over a dotted rule, so it reads as neither a published string nor the board stating a fact. No colour under any result, ever, because @@ -944,7 +949,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o /* The board saying the revision's words are not retained, which is the board talking and not a quotation, so sans for the reason the limit line below gives. */ -.next-cockpit-reading-clause-absent{font:500 var(--fs-sentence)/1.55 var(--sans);color:var(--ink3);overflow-wrap:anywhere} +.next-cockpit-reading-clause-absent{font:var(--fs-xs)/1.5 var(--mono);color:var(--ink3);overflow-wrap:anywhere} .next-cockpit-reading-result,.next-cockpit-reading-detail,.next-session-departure-reading{font:italic 13.5px/1.55 var(--sans);color:var(--ink2);overflow-wrap:anywhere} .next-cockpit-reading-evidence,.next-session-departure-base{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);overflow-wrap:anywhere} /* The board talking, not a string a source published, so sans. The @@ -1162,6 +1167,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-graph-row{display:grid;grid-template-columns:74px calc(var(--lane-count) * 24px) minmax(0,1fr);align-items:start;min-height:48px} .pc-graph-row>time,.pc-graph-time{padding:8px 6px 0 0;color:var(--ink3);text-align:right;overflow-wrap:anywhere} .pc-graph-row>time{font:var(--fs-label)/1.5 var(--mono)} +.pc-graph-time{font:var(--fs-label)/1.5 var(--mono)} .pc-graph-filter{display:flex;justify-content:flex-end;gap:4px;margin-bottom:8px;flex-wrap:wrap} .pc-graph-filter button{padding:4px 8px;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--ink3);font:var(--fs-machine) var(--mono);cursor:pointer} .pc-graph-filter button.selected{border-bottom-color:var(--accent);color:var(--ink)} diff --git a/cargento/skills/cargento/tests/css_cascade.py b/cargento/skills/cargento/tests/css_cascade.py index e132b2cbb..4c8726746 100644 --- a/cargento/skills/cargento/tests/css_cascade.py +++ b/cargento/skills/cargento/tests/css_cascade.py @@ -32,6 +32,16 @@ def _strip_media(css: str) -> str: + """Drop `@media` blocks, and refuse the case where that would hide a rule. + + Every media font rule in this stylesheet is `max-width`, so dropping them + leaves the default viewport intact. A `min-width` block could raise or lower + a size at a width people actually use, and silently ignoring it would let an + always-true `@media(min-width:1px)` escape the guard, so it raises instead. + """ + for block in re.finditer(r"@media\s*\(([^)]*)\)", css): + if "min-width" in block.group(1) and "font" in css[block.end() : block.end() + 400]: + raise UnsupportedSelectorError(f"@media ({block.group(1)}) sets a font size") out: list[str] = [] index = 0 while index < len(css): @@ -99,11 +109,21 @@ def _compound(text: str) -> Compound: } -def _steps(selector: str) -> list[tuple[str, Compound]] | None: +class UnsupportedSelectorError(Exception): + """A selector form this resolver cannot express. + + Raised rather than returned, because a `None` here reads to the caller as + "did not match" and the rule then leaves the cascade in silence. A resolver + that quietly forgets rules produces plausible numbers, which is the failure + this whole guard exists to stop. + """ + + +def _steps(selector: str) -> list[tuple[str, Compound]]: """Selector as [(combinator, compound)], the first combinator ignored.""" parts = [p for p in re.split(r"\s*(>|\+|~)\s*|\s+", selector.strip()) if p] if any(part in ("+", "~") for part in parts): - return None + raise UnsupportedSelectorError(selector) steps: list[tuple[str, Compound]] = [] combinator = " " for part in parts: diff --git a/cargento/skills/cargento/tests/test_focus.py b/cargento/skills/cargento/tests/test_focus.py index d3c3c54b4..22077eeed 100644 --- a/cargento/skills/cargento/tests/test_focus.py +++ b/cargento/skills/cargento/tests/test_focus.py @@ -1021,7 +1021,7 @@ def test_the_pinned_assembly_is_untouched(self) -> None: # would fail on the reader rather than on an injected token. self.assertNotIn(b' dict[str, obj return {"tag": tag, "classes": set(classes), "attrs": set(attrs)} -_REC = [_node("section", "next-cockpit-recovery")] +# `next-project.js:396` wraps the whole cockpit in `.next-cockpit-content`, and +# leaving it out is not cosmetic: without that ancestor +# `.next-cockpit-content .next-cockpit-evidence-missing` (0,2,0) loses to +# `.next-cockpit-recovery small` (0,1,1) and an inverted row reads clean. +_CONTENT = [_node("div", "next-cockpit-content")] +_REC = [*_CONTENT, _node("section", "next-cockpit-recovery")] _RAIL = [_node("div", "next-project-detail-rail")] _TL = [_node("div", "pc-semantic-timeline")] @@ -5712,9 +5717,63 @@ class AnAbsenceNeverOutranksTheValueItReplacesTest(unittest.TestCase): ), ( "recovery goal", - "next-cockpit.js nextProjectGoal", - [*_REC, _node("p", "next-project-goal-text")], - [*_REC, _node("p", "next-project-goal-gap")], + "next-project.js nextProjectGoal", + [ + *_REC, + _node("section", "next-project-goal"), + _node( + "span", + "next-project-value", + "next-project-value--known", + "next-project-goal-text", + ), + ], + [ + *_REC, + _node("section", "next-project-goal"), + _node( + "span", + "next-project-value", + "next-project-value--absent", + "next-project-goal-text", + ), + ], + ), + ( + "reading clause", + "next-cockpit.js nextCockpitReading", + [*_CONTENT, _node("span", "next-cockpit-reading-clause")], + [*_CONTENT, _node("span", "next-cockpit-reading-clause-absent")], + ), + ( + "work summary", + "next-cockpit.js nextCockpitWork", + [*_CONTENT, _node("span", "next-cockpit-work-summary")], + [*_CONTENT, _node("p", "next-cockpit-work-absent")], + ), + ( + # Both classes, because the emitter sets both and the second is + # what distinguishes this machine-paired use of the reason class + # from its prose-paired one. + "graph row time", + "project.js projectGraphRow", + [_node("article", "pc-graph-row"), _node("time")], + [ + _node("article", "pc-graph-row"), + _node("span", "pc-substrate-reason", "pc-graph-time"), + ], + ), + ) + + # Prose absences are the other half of the same rule: they keep the floor. + # `.next-cockpit-recovery small` sizes the strip's storage cue at 12.5px and + # must not drag an absence sentence down with it, which is only decidable + # with the `.next-cockpit-content` wrapper in the path. + PROSE_ABSENCES = ( + ( + "assignment evidence absent", + "next-cockpit.js nextCockpitRecovery", + [*_REC, _node("div"), _node("small", "next-cockpit-evidence-missing")], ), ) @@ -5755,6 +5814,57 @@ def test_no_slot_draws_its_value_smaller_than_its_absence(self) -> None: "so the absence outranks the fact it replaces", ) + # The six machine slots `design-next-ui.md` records as promoted. Comparing a + # value against its absence does not bind these: reverting the promotion + # drops both sides together and the comparison stays equal while the value + # leaves the tier the document says it is on. Measured, not assumed: that + # mutation survived until this list existed. + PROMOTED = ( + ( + "recovery published value", + [ + *_REC, + _node("div", "next-cockpit-waiting"), + _node("span", "next-project-value", "next-project-value--known"), + ], + ), + ("rail wait duration", [*_RAIL, _node("span", "next-rail-wait-duration")]), + ( + "rail token rate", + [ + *_RAIL, + _node("div", "next-delegation-metrics"), + _node("span", attrs=("data-next-delegation-rate",)), + ], + ), + ( + "rail pace and resets", + [*_RAIL, _node("div", "next-rail-capacity-caption"), _node("span")], + ), + ("entry-details timestamp", [*_TL, _node("div", "pc-entry-details"), _node("time")]), + ("published value", [*_TL, _node("div", "pc-entry-details"), _node("span", "pc-source")]), + ) + + def test_every_promoted_machine_slot_is_on_the_sentence_tier(self) -> None: + sentence = self.tokens["fs-sentence"] + for name, path in self.PROMOTED: + with self.subTest(slot=name): + self.assertEqual( + sentence, + self.size(path), + f"{name} left the sentence tier, which design-next-ui.md records it on", + ) + + def test_a_prose_absence_keeps_the_sentence_floor(self) -> None: + sentence = self.tokens["fs-sentence"] + for name, emitter, path in self.PROSE_ABSENCES: + with self.subTest(absence=name): + self.assertEqual( + sentence, + self.size(path), + f"{name} ({emitter}) does not resolve to the sentence tier", + ) + def test_a_caption_stays_below_the_values_it_labels(self) -> None: for name, caption_path, value_path in self.CAPTION_BELOW_VALUE: with self.subTest(caption=name): diff --git a/cargento/skills/cargento/tests/test_next_flag.py b/cargento/skills/cargento/tests/test_next_flag.py index e6561af88..33640d160 100644 --- a/cargento/skills/cargento/tests/test_next_flag.py +++ b/cargento/skills/cargento/tests/test_next_flag.py @@ -64,9 +64,9 @@ def test_retired_next_query_is_not_a_page_alias(self) -> None: def test_the_canonical_loader_is_the_released_ui_bundle(self) -> None: page = frontend_page.load_page() - self.assertEqual(915_014, len(page)) + self.assertEqual(915_434, len(page)) self.assertEqual( - "3749774c3299c7a7a9a0a5eddd9b0630118116f66e4cb8b1853bd11ea0e727ad", + "b48cdd7ed6390894f85b3bad0eb8662c62592a58e532ac62eec0fb523622afb0", hashlib.sha256(page).hexdigest(), ) diff --git a/cargento/skills/cargento/tests/test_next_page.py b/cargento/skills/cargento/tests/test_next_page.py index efaa7c125..092a0cec4 100644 --- a/cargento/skills/cargento/tests/test_next_page.py +++ b/cargento/skills/cargento/tests/test_next_page.py @@ -700,16 +700,16 @@ def test_load_page_preserves_its_byte_oracles(self) -> None: self.assertEqual(digest, hashlib.sha256(data).hexdigest()) styles = frontend_page.asset_path("styles.css").read_bytes() - self.assertEqual(111_720, len(styles)) + self.assertEqual(112_140, len(styles)) self.assertEqual( - "6199c635186bb39da123b727cb4351e586694f7dfd35d017ed43f4c417a5e1fa", + "77dd73d39d3dcda3a8937e0c9b1e0f06336e68e6a0a8b1c0f19ef7b6de3bede9", hashlib.sha256(styles).hexdigest(), ) assembled = frontend_page.load_page() - self.assertEqual(915_014, len(assembled)) + self.assertEqual(915_434, len(assembled)) self.assertEqual( - "3749774c3299c7a7a9a0a5eddd9b0630118116f66e4cb8b1853bd11ea0e727ad", + "b48cdd7ed6390894f85b3bad0eb8662c62592a58e532ac62eec0fb523622afb0", hashlib.sha256(assembled).hexdigest(), ) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index 04e4d26eb..42d8a5f61 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -109,8 +109,23 @@ Assign one owner to each region during parallel work. Keep media queries at the region; a shared responsive block would make every view edit the same tail. Moving a rule between regions is an ownership change, not incidental cleanup. -Board sentences have a 15px floor (`--fs-sentence`), at weight 500 and line-height 1.55. Labels, -identifiers, timestamps, rates and compact controls sit on one tier, `--fs-label` at 11px. Four +Board sentences have a 15px floor (`--fs-sentence`), at weight 500 and line-height 1.55. Labels +and compact controls sit on one tier, `--fs-label` at 11px. **Six machine slots are promoted off +that tier** because the absence that replaces each one is a sentence, and an absence must never +read larger than the fact it stands in for: `.next-rail-wait-duration`, the token rate in +`.next-delegation-metrics`, the pace and resets in `.next-rail-capacity-caption`, the timestamps in +`.pc-entry-details time` and `.pc-event-evidence time`, `.pc-semantic-timeline .pc-source`, and the +published values in `.next-cockpit-recovery .next-project-value`. So a timestamp or a rate is on +the label tier only where nothing replaces it with a sentence. + +**Where the machine string has a machine-shaped absence, the absence moves instead.** +`.next-cockpit-work-absent`, `.next-cockpit-reading-clause-absent` and `.pc-graph-time` sit with +the values they replace rather than at the sentence floor, because a rate, a timestamp and a +duration are compact on purpose and raising them removes the inversion by breaking the register. +That is the cheaper half of the same rule and it needs no emitter change, because each of those +slots already emits a class that separates its machine-paired use from its prose-paired one. +Prose absences in the same panels, `.next-cockpit-work-limit` and `.next-cockpit-work-dropped` +among them, keep the floor. Four `:root` steps used to sit below 11px, at 9px, 9.5px, 10px and 10.5px, a 3px band nobody can rank, and 9px was the smallest step in the file. **Counts here read "at or below 12px" inclusively**, so the band held seven tokens: `--fs-column` 9px, `--fs-label` 9.5px, `--fs-meta` 10px, From 0f8a3a75524abd12a69fd49139ce9a2bce721ab9 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 20:30:44 +0800 Subject: [PATCH 14/15] docs(design): drop the remainder count that survived as a comparison "Comparable in size to the one this branch already moved" is a count. The set it compares against is stated three paragraphs earlier as sixty-seven, so the sentence asserts a magnitude two to three times the highest figure any census here produced, and no census produced it at all. Retracting the numbers and keeping this was the same error pointing the confident way: a floor cannot be reported as a count in either direction. The paragraph now says the shape and refuses the size in any form, comparison included. The textarea-and-terminal clause stays, because it is an example rather than a tally. Also narrows the provenance clause on the 163-then-183 readings. It claimed an unchanged stylesheet; 4fb5ee6d changed styles.css by thirteen lines. None of them matches operation-fact, so the rules resolving the counted shape were the same across both readings, which is the true and narrower statement. A paragraph whose whole job is to stop a number being read as a property of the code must not overstate by a step itself. Docs only: no CSS, so no byte pin moves. Signed-off-by: Jared Scott --- docs/design-next-ui.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index 42d8a5f61..bf17498df 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -198,14 +198,20 @@ cannot fail loudly on a shorthand it does not understand will keep producing pla **Element counts carry the date and the commit they were taken at, or they do not belong here.** The board renders whatever sessions exist, so one shape counted 163 elements and then 183 forty -minutes later on an unchanged stylesheet, both readings taken during this branch's review on -2026-09-17 and reported by the reviewer rather than measured here. A bare number reads as a +minutes later, at `3cc7ef49` and `4fb5ee6d` on 2026-09-17, both reported by the reviewer rather +than measured here. The stylesheet was not identical across that pair, `4fb5ee6d` changed it by +thirteen lines, but none of those lines matches `operation-fact`, so the rules resolving the shape +that was counted were the same for both readings. The narrower claim is the true one. A bare number reads as a property of the code when it is a property of an afternoon. The stable unit is the rule, or the element shape, never its population. -They were left alone rather than swept: the set is comparable in size to the one this branch -already moved, and some of it is not sentences at all, a textarea and the prototype terminal among -them. It needs reading one element at a time, which is the work DRC-4602 carries. +They were left alone rather than swept, and **how many there are is not stated here in any form**, +including by comparison with the tier this branch did move. Every figure this branch produced was a +per-rule floor, and a floor cannot be reported as a count in either direction: asserting a +magnitude above the highest of them would be the same error pointing the confident way. What can be +said is the shape. Some of the set is not sentences at all, a textarea and the prototype terminal +among them, so it needs reading one element at a time. That is the work DRC-4602 carries, and the +size is its to measure. Twenty-four declarations carry `.09em`: the whole `.13em` and `.14em` groups, plus eight of the eleven in the `.1em` and `.08em` groups. The other three of those eleven keep their own value From 39c81df12f2d4d841574d5f0944838826eea0155 Mon Sep 17 00:00:00 2001 From: Jared Scott Date: Thu, 17 Sep 2026 20:53:31 +0800 Subject: [PATCH 15/15] revert(web): return every absence rule but two to its value on main Correction round 4. Four review cycles each found more of one class, and the class contains instances that predate this branch. A defect class older than the pull request is not the pull request's to close, so this scopes back to the part that cannot be wrong. The rule: no absence rule is raised unless the value it is paired with already resolves at or above the sentence tier. That cannot create an inversion by construction, which is the property none of the last three rounds had, and it needs no bound, no emitter census and no reading of 31 call sites. Twenty absence rules and the value raises that existed only to answer the inversions they created return to their value on main. Four rules this branch invented for that purpose are removed outright: .pc-graph-time, .pc-terminal-identity strong, .pc-semantic-timeline .pc-source, and .next-cockpit-recovery .next-project-value. Two selector groups had to be split because they mixed absences with prose that stays raised, and two that this branch had split are recombined as they were. Two absences stay on the sentence tier, both named by string in the issue's own criterion, and each is safe for a reason that does not depend on finding every pair. "two axes, read separately" is drawn beside values already on that tier. "No revision saved yet" shares one class with the revision line it alternates with, so both branches resolve identically whatever the tier is; it is a polymorphic slot rather than an absence in a ternary, which is a stronger guarantee than a comparison. The guard shrinks to those two and asserts nothing wider. The test it replaces checked an absence's size without its value and so asserted an inversion as correct, which is the third guard on this branch to pass over the defect it was named for: one compared a value against its absence and could not see both move together, one built a DOM the application never renders, and this one read only one side. An absence is now only ever read beside the value it replaces. The documentation follows the smaller claim. The six promotions, the three demotions and the absence-register prose are gone rather than reworded, because a sentence describing work that moved to DRC-4602 is the false-record failure this branch has already had to repair twice. Runtime is negative: styles.css loses 14 lines net. Seven byte pins regenerated; no JS changed, so next-cockpit.js is unchanged. Signed-off-by: Jared Scott --- .../cargento/cargento_runtime/web/styles.css | 66 ++-- cargento/skills/cargento/tests/test_focus.py | 2 +- .../cargento/tests/test_next_cockpit.py | 324 ++++-------------- .../skills/cargento/tests/test_next_flag.py | 4 +- .../skills/cargento/tests/test_next_page.py | 8 +- docs/design-next-ui.md | 45 +-- 6 files changed, 108 insertions(+), 341 deletions(-) diff --git a/cargento/skills/cargento/cargento_runtime/web/styles.css b/cargento/skills/cargento/cargento_runtime/web/styles.css index e90d8dfc7..f86e3dc61 100644 --- a/cargento/skills/cargento/cargento_runtime/web/styles.css +++ b/cargento/skills/cargento/cargento_runtime/web/styles.css @@ -188,7 +188,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-project-goal-source{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-project-goal-text{max-width:640px;font-size:14.5px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere} .next-project-goal-text.next-project-value--known{color:var(--ink)} -.next-project-goal-gap{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} +.next-project-goal-gap{margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} .next-project-goal-row{display:flex;flex-direction:column;gap:5px;padding-left:9px;border-left:1px solid var(--line2)} .next-project-goal-row+.next-project-goal-row{margin-top:2px} .next-project-goal-tag{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700;letter-spacing:.09em} @@ -298,7 +298,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-rail-meta{margin-left:auto;color:var(--ink3);font:var(--fs-label) var(--mono);text-align:right} .next-rail-meta--sentence{font:var(--fs-xs) var(--sans)} .next-rail-meta--amber{color:var(--amber)} -.next-project-detail-rail .next-rail-reason{margin:0;color:var(--ink3);font-family:var(--sans);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} +.next-project-detail-rail .next-rail-reason{margin:0;color:var(--ink3);font-family:var(--sans);font-size:var(--fs-xs);line-height:1.5} .next-delegation-figure{display:flex;gap:9px;align-items:baseline} .next-delegation-figure>strong{color:var(--accent);font:700 32px/1 var(--mono);letter-spacing:-.02em} .next-delegation-caption{margin:0;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} @@ -306,16 +306,16 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-delegation progress::-webkit-progress-bar{background:var(--line)} .next-delegation progress::-webkit-progress-value{background:var(--accent-dim)} .next-delegation progress::-moz-progress-bar{background:var(--accent-dim)} -.next-delegation-metrics{display:flex;flex-wrap:wrap;gap:4px 9px;color:var(--ink2);font:var(--fs-sentence)/1.5 var(--mono)} +.next-delegation-metrics{display:flex;flex-wrap:wrap;gap:4px 9px;color:var(--ink2);font:10.5px/1.5 var(--mono)} .next-delegation-withheld{display:flex;flex-direction:column;gap:6px;color:var(--ink2)} .next-delegation-withheld strong{font:700 16px var(--mono)} -.next-delegation-withheld small{color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} +.next-delegation-withheld small{color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} .next-delegation-trend{color:var(--ink3);font:var(--fs-xs) var(--mono)} .next-rail-wait{display:flex;flex-direction:column;gap:8px;padding:12px 13px;border:1px solid var(--line);border-left:2px solid var(--amber);border-radius:9px;background:var(--panel)} .next-rail-wait-heading{display:flex;align-items:baseline;gap:9px} .next-rail-wait-heading a{min-width:0;color:var(--ink);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;overflow-wrap:anywhere;text-decoration:none} .next-rail-wait-heading a:hover{text-decoration:underline} -.next-rail-wait-duration{margin-left:auto;flex-shrink:0;color:var(--amber);font:700 var(--fs-sentence) var(--mono)} +.next-rail-wait-duration{margin-left:auto;flex-shrink:0;color:var(--amber);font:700 11px var(--mono)} .next-rail-question{margin:0;color:var(--ink2);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;overflow-wrap:anywhere} .next-rail-wait-controls{display:flex;flex-wrap:wrap;gap:7px;padding-top:2px} .next-rail-wait-controls button{margin-top:0;padding:5px 10px;border-radius:6px;font-size:var(--fs-label)} @@ -331,7 +331,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-rail-capacity-fill--amber{background:var(--amber)} .next-rail-capacity-fill--unknown{background:var(--ink3)} .next-rail-capacity-tick{position:absolute;top:-2px;bottom:-2px;width:1.5px;background:var(--ink2)} -.next-rail-capacity-caption{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;color:var(--ink3);font:var(--fs-sentence) var(--mono)} +.next-rail-capacity-caption{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;color:var(--ink3);font:10px var(--mono)} .next-project-detail-rail .next-usage-consent{margin:0;padding:12px;font-size:var(--fs-xs)} .next-project-detail-rail .next-usage-consent-note{font-family:var(--sans);font-size:var(--fs-xs)} .next-project-detail-rail .next-usage-switch{margin:0;font-size:var(--fs-xs)} @@ -353,7 +353,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-guardrail-row[aria-checked="false"]{color:var(--ink3)} .next-guardrail-glyph{margin-top:2px;color:var(--ink3);font:var(--fs-label) var(--mono)} .next-guardrail-copy strong,.next-guardrail-copy small{display:block;overflow-wrap:anywhere;font-size:var(--fs-sentence);font-weight:500;line-height:1.55} -.next-guardrail-copy small,.next-guardrail-empty{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} +.next-guardrail-copy small,.next-guardrail-empty{margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} .next-guardrail-add-input{display:flex;align-items:center;gap:9px} .next-guardrail-add-input button{flex-shrink:0;color:var(--ink)} .next-project-detail-rail :is(a,button,input):focus-visible{outline:2px solid var(--accent);outline-offset:3px} @@ -598,7 +598,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-attention-open{margin:0;padding:0;list-style:none} .next-attention-open li{display:grid;grid-template-columns:34px 210px minmax(0,1fr);gap:16px;align-items:baseline;padding:9px 18px;border-bottom:1px solid var(--line)} .next-attention-open li>span{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);font-weight:700} -.next-attention-open strong{color:var(--ink2);font-size:var(--fs-sentence);font-weight:400} +.next-attention-open strong{color:var(--ink2);font-size:var(--fs-body);font-weight:400} .next-attention-open p{margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55;max-width:var(--measure)} @media(min-width:900px){ .next-attention-item--legacy{grid-template-columns:minmax(150px,.8fr) minmax(180px,1fr) minmax(180px,1.2fr) minmax(140px,.8fr) minmax(130px,.7fr)} @@ -725,14 +725,14 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-now-state>div{min-width:0;padding:10px 12px;border-right:1px solid var(--line)} .next-cockpit-now-state>div:last-child{border-right:0} .next-cockpit-now-state strong,.next-cockpit-now-state small{display:block;margin-top:4px;overflow-wrap:anywhere} -.next-cockpit-now-state strong{font-size:var(--fs-sentence);font-weight:600} +.next-cockpit-now-state strong{font-size:var(--fs-xs);font-weight:600} .next-cockpit-now-state small{color:var(--ink3);font-size:var(--fs-2xs)} .next-cockpit-panel[data-next-cockpit-panel="now"]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start} .next-cockpit-panel[data-next-cockpit-panel="now"]>.next-project-detail-status,.next-cockpit-panel[data-next-cockpit-panel="now"]>.next-cockpit-plan-details{grid-column:1/-1} .next-cockpit-panel[data-next-cockpit-panel="console"]>.next-project-detail-rail{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;width:auto;min-width:0;border-left:0} .next-cockpit-waiting{margin-bottom:12px} .next-cockpit-waiting a{color:var(--ink);text-decoration-color:var(--line2);text-underline-offset:4px} -.next-cockpit-recovery .next-project-value--absent{font:500 var(--fs-sentence)/1.55 var(--sans);letter-spacing:0;max-width:var(--measure)} +.next-cockpit-recovery .next-project-value--absent{font:var(--fs-xs)/1.55 var(--sans);letter-spacing:0} .next-cockpit-active-delegation,.next-cockpit-needs{min-width:0;padding:18px 12px;border-bottom:1px solid var(--line)} .next-cockpit-needs{border-right:1px solid var(--line)} .next-cockpit-active-delegation h2,.next-cockpit-needs h2{margin:0 0 10px;font-size:var(--fs-md);font-weight:600} @@ -766,7 +766,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-memos>div>[data-next-cockpit-memo-field]{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:baseline;gap:8px;padding:10px 12px} .next-cockpit-memos>div>[data-next-cockpit-memo-field]+[data-next-cockpit-memo-field]{border-left:1px solid var(--line)} .next-cockpit-memos [data-next-cockpit-memo-field]>span,.next-cockpit-memos label>small{color:var(--ink3);font:var(--fs-2xs) var(--mono)} -.next-cockpit-memos [data-next-cockpit-memo-field]>strong{overflow:hidden;color:var(--ink2);font-size:var(--fs-sentence);font-weight:450;text-overflow:ellipsis;white-space:nowrap} +.next-cockpit-memos [data-next-cockpit-memo-field]>strong{overflow:hidden;color:var(--ink2);font-size:var(--fs-sm);font-weight:450;text-overflow:ellipsis;white-space:nowrap} .next-cockpit-memos [data-next-cockpit-action^="memo-"]{border:0;background:none;color:var(--ink2);font:var(--fs-2xs) var(--mono);cursor:pointer} .next-cockpit-memos textarea{grid-column:1/-1;display:block;width:100%;min-height:64px;resize:vertical;padding:9px;border:1px solid var(--line);border-radius:0;background:var(--panel);color:var(--ink);font:inherit;line-height:1.45} .next-cockpit-memos label>small{grid-column:1/3} @@ -835,7 +835,6 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-recovery-memos textarea{grid-column:1/-1;width:100%;min-height:54px;resize:vertical;padding:7px;border:1px solid var(--line2);background:var(--panel);color:var(--ink);font:inherit} .next-cockpit-recovery span,.next-cockpit-recovery strong,.next-cockpit-recovery small{display:block} .next-cockpit-recovery span{margin-bottom:5px;color:var(--ink3);font-size:var(--fs-2xs);letter-spacing:.09em} -.next-cockpit-recovery .next-project-value{font-size:var(--fs-sentence);letter-spacing:0} .next-cockpit-recovery strong{font-weight:500;overflow-wrap:anywhere} .next-cockpit-recovery small{margin-top:4px;color:var(--ink3);overflow-wrap:anywhere} .next-cockpit-recovery details{margin-top:5px;color:var(--ink3)} @@ -873,7 +872,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-held-count{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-cockpit-held-field button{border:0;background:none;color:var(--ink2);font:var(--fs-xs) var(--mono);cursor:pointer} .next-cockpit-held-field button[hidden]{display:none} -.next-cockpit-held-absent{grid-column:1/-1;margin:0;color:var(--ink3);font-size:var(--fs-sentence);font-weight:500;line-height:1.55} +.next-cockpit-held-absent{grid-column:1/-1;margin:0;color:var(--ink3);font-size:var(--fs-xs);line-height:1.5} .next-cockpit-held-cue{grid-column:1/-1;color:var(--ink3);font-size:var(--fs-xs)} /* Section scope, outside the field grid: the act is per annotation and the grid has exactly three columns for the counter and the two field controls. @@ -897,12 +896,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-work-mix{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-work-at{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} .next-cockpit-work-source{grid-column:1/-1;color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label)} -/* An absence replacing a machine string takes that string's tier, not the - sentence one: raising a rate or a timestamp to 15px removes the inversion - by breaking the register. Prose absences keep the floor -- limit and - dropped below are the board's own sentences and stay there. */ -.next-cockpit-work-absent{margin:0;color:var(--ink3);font:var(--fs-xs)/1.55 var(--mono);max-width:var(--measure)} -.next-cockpit-work-limit,.next-cockpit-work-dropped{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-work-absent,.next-cockpit-work-limit,.next-cockpit-work-dropped{margin:0;color:var(--ink3);font:var(--fs-xs)/1.5 var(--sans)} /* The third treatment, for a model reading: italic Space Grotesk on the secondary ink over a dotted rule, so it reads as neither a published string nor the board stating a fact. No colour under any result, ever, because @@ -949,14 +943,14 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o /* The board saying the revision's words are not retained, which is the board talking and not a quotation, so sans for the reason the limit line below gives. */ -.next-cockpit-reading-clause-absent{font:var(--fs-xs)/1.5 var(--mono);color:var(--ink3);overflow-wrap:anywhere} +.next-cockpit-reading-clause-absent{font:var(--fs-xs)/1.5 var(--sans);color:var(--ink3);overflow-wrap:anywhere} .next-cockpit-reading-result,.next-cockpit-reading-detail,.next-session-departure-reading{font:italic 13.5px/1.55 var(--sans);color:var(--ink2);overflow-wrap:anywhere} .next-cockpit-reading-evidence,.next-session-departure-base{color:var(--ink3);font-family:var(--mono);font-size:var(--fs-label);overflow-wrap:anywhere} /* The board talking, not a string a source published, so sans. The evidence line above it names a type and a source and stays mono. */ .next-cockpit-reading-limit{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} .next-cockpit-reading-why{margin:0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} -.next-cockpit-reading-stale,.next-session-departure-stale{margin:0;color:var(--amber);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-reading-stale,.next-session-departure-stale{margin:0;color:var(--amber);font:var(--fs-xs)/1.5 var(--sans)} .next-cockpit-reading button{align-self:flex-start;padding:5px 10px;border:1px solid var(--line2);background:var(--panel);color:var(--ink);font:var(--fs-xs) var(--sans);cursor:pointer} .next-cockpit-reading button[disabled]{color:var(--ink3);cursor:default} /* HOW IT LANDED. Two cards side by side and never one verdict: what ended and @@ -1086,12 +1080,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-recovery [data-next-cockpit-task-known]>strong,.next-cockpit-recovery .next-cockpit-recovery-evidence>strong{color:var(--ink)} .next-cockpit-recovery>div{font:500 var(--fs-sentence)/1.55 var(--sans)} .next-cockpit-recovery span{font-family:var(--mono)} -/* A stated absence must never read larger than the fact it stands in for. - Lowering the absence back to 12.5px is the other way out and is refused: - it reinstates the sub-floor sentence. The value keeps mono and --ink, so - it still outranks its absence on family and tone. `small` is the storage - cue, not a value, and stays down. */ -.next-cockpit-recovery div strong{font-size:var(--fs-sentence)} +.next-cockpit-recovery strong,.next-cockpit-recovery small{font-size:var(--fs-xs)} .next-cockpit-recovery small{font-size:var(--fs-xs)} .next-cockpit-recovery-evidence>strong,.next-cockpit-execution-root>strong,.next-cockpit-child-row>strong, .next-cockpit-recovery [data-next-cockpit-memo-field]>strong,.next-cockpit-recovery textarea, @@ -1101,17 +1090,17 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .next-cockpit-scope-tree small[data-next-withheld],.next-cockpit-scope-options small[data-next-withheld]{font-family:var(--sans);color:var(--ink3)} .next-cockpit-authority>strong,.next-cockpit-authority>small{font-family:var(--sans)} .next-cockpit-authority>span{font:500 var(--fs-sentence)/1.55 var(--sans);letter-spacing:0;max-width:var(--measure)} -.next-cockpit-empty,.next-cockpit-decision-summary,.next-cockpit-viewing-session, +.next-cockpit-decision-summary,.next-cockpit-viewing-session, .next-cockpit-now-state small,.next-project-workflow-definition>small, -.next-cockpit-system-details ul,.next-cockpit-memos label>small, -.next-cockpit-evidence-missing{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} -.next-cockpit-content .next-cockpit-evidence-missing{margin:6px 0;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-system-details ul,.next-cockpit-memos label>small{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-empty,.next-cockpit-evidence-missing{font:var(--fs-xs)/1.55 var(--sans)} +.next-cockpit-content .next-cockpit-evidence-missing{margin:6px 0;color:var(--ink3);font:var(--fs-xs)/1.55 var(--sans)} .next-cockpit-content .next-course-evidence{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} .next-cockpit-content .next-course-evidence .next-cockpit-source{display:inline;font:inherit;font-family:var(--mono)} .next-cockpit-recovery .next-project-goal{margin-top:12px;padding:10px 0;border:0;border-top:1px solid var(--line);border-radius:0;background:transparent} -.next-cockpit-recovery .next-project-goal-text{font:var(--fs-sentence)/1.55 var(--mono)} +.next-cockpit-recovery .next-project-goal-text{font:var(--fs-xs)/1.55 var(--mono)} .next-cockpit-recovery .next-project-goal-text.next-project-value--absent, -.next-cockpit-recovery .next-project-goal-gap{font:500 var(--fs-sentence)/1.55 var(--sans);max-width:var(--measure)} +.next-cockpit-recovery .next-project-goal-gap{font:var(--fs-xs)/1.55 var(--sans)} .next-cockpit-recovery .next-project-goal-source{font-size:var(--fs-2xs)} .next-cockpit-recovery details>summary,.next-course-evidence>summary, .next-cockpit-plan-details>summary,.next-cockpit-console-status>summary{align-content:center;padding:4px 0;color:var(--ink2);font:500 var(--fs-sentence)/1.55 var(--sans);text-decoration:underline;text-decoration-color:var(--line2);text-underline-offset:4px;max-width:var(--measure)} @@ -1152,7 +1141,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-terminal-open,.pc-terminal-bar button{padding:7px 10px;border:1px solid var(--line2);border-radius:4px;background:transparent;color:var(--ink2);font:var(--fs-machine) var(--mono);cursor:pointer} .pc-terminal-open:hover,.pc-terminal-bar button:hover{background:var(--panel);border-color:var(--ink3);color:var(--ink)} .pc-terminal-open:focus-visible,.pc-terminal-bar button:focus-visible,.pc-semantic-timeline button:focus-visible,.pc-semantic-timeline summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px} -.pc-substrate-empty,.pc-substrate-reason,.pc-terminal-identity p{color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} +.pc-substrate-empty,.pc-substrate-reason,.pc-terminal-identity p{color:var(--ink3);font:var(--fs-xs)/1.55 var(--sans);overflow-wrap:anywhere} .pc-substrate-empty{margin:8px 0} .pc-terminal-absence{padding:4px 0 10px;max-width:70ch} .pc-terminal-absence .pc-substrate-empty:first-child{color:var(--ink2)} @@ -1167,7 +1156,6 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-graph-row{display:grid;grid-template-columns:74px calc(var(--lane-count) * 24px) minmax(0,1fr);align-items:start;min-height:48px} .pc-graph-row>time,.pc-graph-time{padding:8px 6px 0 0;color:var(--ink3);text-align:right;overflow-wrap:anywhere} .pc-graph-row>time{font:var(--fs-label)/1.5 var(--mono)} -.pc-graph-time{font:var(--fs-label)/1.5 var(--mono)} .pc-graph-filter{display:flex;justify-content:flex-end;gap:4px;margin-bottom:8px;flex-wrap:wrap} .pc-graph-filter button{padding:4px 8px;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--ink3);font:var(--fs-machine) var(--mono);cursor:pointer} .pc-graph-filter button.selected{border-bottom-color:var(--accent);color:var(--ink)} @@ -1183,14 +1171,14 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-trail-summary,.pc-trail-summary>span{display:block} .pc-entry-details{display:grid;gap:7px;margin-top:9px;padding:10px 12px;border-left:2px solid var(--line2);background:var(--sunk);color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} .pc-entry-details b{color:var(--ink2);font:var(--fs-machine) var(--mono)} -.pc-entry-details time,.pc-event-evidence time{font:var(--fs-sentence) var(--mono)} +.pc-entry-details time,.pc-event-evidence time{font:var(--fs-machine) var(--mono)} .pc-entry-source-list{display:grid;gap:6px;margin-top:6px} .pc-trail-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px} .pc-trail-top strong{color:var(--ink);font-size:var(--fs-body)} .pc-trail-top .pc-lane-title{color:var(--ink);font:var(--fs-body)/1.4 var(--mono);overflow-wrap:anywhere} .pc-trail-top span{color:var(--ink3);font:var(--fs-machine)/1.5 var(--mono);text-align:right} .pc-trail-result{margin-top:4px;color:var(--ink2);font:var(--fs-body)/1.5 var(--sans);overflow-wrap:anywhere} -.pc-trail-quiet,.pc-trail-history,.pc-event-evidence{margin-top:5px;color:var(--ink3);font:500 var(--fs-sentence)/1.55 var(--sans);overflow-wrap:anywhere} +.pc-trail-quiet,.pc-trail-history,.pc-event-evidence{margin-top:5px;color:var(--ink3);font:var(--fs-xs)/1.55 var(--sans);overflow-wrap:anywhere} .pc-terminal-open{margin:8px 0 12px} .pc-terminal{display:flex;min-width:0;flex-direction:column;border:1px solid var(--line2);border-radius:6px;background:var(--sunk);color:var(--ink);overflow:hidden} .pc-terminal-bar{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--line);font:var(--fs-machine)/1.5 var(--mono);flex-wrap:wrap} @@ -1198,9 +1186,7 @@ body{font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:o .pc-terminal-bar>span{margin-left:auto;color:var(--ink3)} .pc-terminal-identity{flex-basis:100%;min-width:0} .pc-terminal-identity p{margin:3px 0} -.pc-terminal-identity strong{font-size:var(--fs-sentence)} -.pc-semantic-timeline .pc-source{font-size:var(--fs-sentence)} -.pc-terminal-identity code{margin-right:10px;font-size:var(--fs-sentence)} +.pc-terminal-identity code{margin-right:10px} .pc-terminal-viewport{height:340px;overflow:auto;overscroll-behavior:contain} .pc-terminal-screen{min-width:100%;min-height:340px;padding:6px;box-sizing:content-box;font:500 var(--fs-sentence)/1.55 var(--sans)} .pc-terminal-screen>.xterm{width:100%;height:100%} diff --git a/cargento/skills/cargento/tests/test_focus.py b/cargento/skills/cargento/tests/test_focus.py index 22077eeed..8a5a8177f 100644 --- a/cargento/skills/cargento/tests/test_focus.py +++ b/cargento/skills/cargento/tests/test_focus.py @@ -1021,7 +1021,7 @@ def test_the_pinned_assembly_is_untouched(self) -> None: # would fail on the reader rather than on an injected token. self.assertNotIn(b' dict[str, obj return {"tag": tag, "classes": set(classes), "attrs": set(attrs)} -# `next-project.js:396` wraps the whole cockpit in `.next-cockpit-content`, and -# leaving it out is not cosmetic: without that ancestor -# `.next-cockpit-content .next-cockpit-evidence-missing` (0,2,0) loses to -# `.next-cockpit-recovery small` (0,1,1) and an inverted row reads clean. +# `next-project.js:396` wraps the cockpit in `.next-cockpit-content`. Leaving it +# out is not cosmetic: a rule qualified by it outranks one that is not, so a +# fixture without it resolves a DOM the application never builds. _CONTENT = [_node("div", "next-cockpit-content")] -_REC = [*_CONTENT, _node("section", "next-cockpit-recovery")] -_RAIL = [_node("div", "next-project-detail-rail")] -_TL = [_node("div", "pc-semantic-timeline")] class AnAbsenceNeverOutranksTheValueItReplacesTest(unittest.TestCase): - """DRC-4587 F1 and its correction round. A stated absence must never render - larger than the fact it stands in for: "you can tell a label from its - answer, a figure from a gap". - - **The pairs are read from the emitters, not from the stylesheet.** Each row - below is one ternary that picks a value class or an absence class, and the - two branches never co-exist in a render. A sweep of co-existing selectors - cannot see such a pair, and neither can a live board: only one branch is - ever on screen. Round 1 swept selectors, passed, and missed six of these. + """DRC-4587. A stated absence must never render larger than the fact it + stands in for: "you can tell a label from its answer, a figure from a gap". + + **This asserts only what the pull request claims.** It raises two absence + rules, both named by string in the issue's own criterion, and each is listed + below with the value it is drawn against. Enumerating them exhaustively is + the whole claim; the wider class of sub-floor absences is DRC-4602's, and + nothing here asserts anything about it. + + Three earlier versions of this guard passed over the defect they were named + for, and the shape of each failure is why this one is written as it is. One + compared a value against its absence and could not see both move together. + One built a DOM the application never renders. One checked an absence's size + without its value, and so asserted an inversion as correct. **An absence is + only ever read beside the value it replaces.** """ - # (name, emitter, value path, absence path). Paths are the DOM the emitter - # actually builds; `css_cascade` resolves each branch on its own. - SLOTS = ( + # (name, emitter, absence path, the value it is drawn against). + RAISED_ABSENCES = ( ( - "recovery published value", - "next-cockpit.js nextCockpitWaitingCommand", + "two axes, read separately", + "next-cockpit.js nextCockpitLanded", [ - *_REC, - _node("div", "next-cockpit-waiting"), - _node("span", "next-project-value", "next-project-value--known"), + *_CONTENT, + _node("section", "next-cockpit-landed"), + _node("span", "next-cockpit-landed-axes"), ], [ - *_REC, - _node("div", "next-cockpit-waiting"), - _node("span", "next-project-value", "next-project-value--absent"), - ], - ), - ( - "rail wait duration", - "next-delegation.js nextRailWaiting", - [ - *_RAIL, - _node("section", "next-rail-panel"), - _node("article", "next-rail-wait"), - _node("div", "next-rail-wait-heading"), - _node("span", "next-rail-wait-duration"), - ], - [ - *_RAIL, - _node("section", "next-rail-panel"), - _node("article", "next-rail-wait"), - _node("div", "next-rail-wait-heading"), - _node("span", "next-rail-reason"), - ], - ), - ( - "rail token rate", - "next-delegation.js nextRailDelegation", - [ - *_RAIL, - _node("section", "next-delegation"), - _node("div", "next-delegation-metrics"), - _node("span", attrs=("data-next-delegation-rate",)), - ], - [ - *_RAIL, - _node("section", "next-delegation"), - _node("div", "next-delegation-metrics"), - _node("span", "next-rail-reason", attrs=("data-next-delegation-rate-withheld",)), - ], - ), - ( - "rail pace and resets", - "next-delegation.js nextRailCapacityWindow", - [ - *_RAIL, - _node("div", "next-rail-capacity-window"), - _node("div", "next-rail-capacity-caption"), - _node("span"), - ], - [ - *_RAIL, - _node("div", "next-rail-capacity-window"), - _node("div", "next-rail-capacity-caption"), - _node("span", "next-rail-reason"), - ], - ), - ( - "event time", - "project.js projectEventTime", - [ - *_TL, - _node("div", "pc-entry-details"), - _node("div"), - _node("time", attrs=("datetime",)), - ], - [ - *_TL, - _node("div", "pc-entry-details"), - _node("div"), - _node("span", "pc-substrate-reason"), - ], - ), - ( - "published value", - "project.js projectPublishedValue", - [*_TL, _node("div", "pc-entry-details"), _node("div"), _node("span", "pc-source")], - [ - *_TL, - _node("div", "pc-entry-details"), - _node("div"), - _node("span", "pc-substrate-reason"), - ], - ), - ( - "terminal identity window", - "project.js projectTerminalIdentity", - [_node("div", "pc-terminal-identity"), _node("code")], - [_node("div", "pc-terminal-identity"), _node("p")], - ), - ( - "terminal identity session name", - "project.js projectTerminalIdentity", - [_node("div", "pc-terminal-identity"), _node("strong")], - [_node("div", "pc-terminal-identity"), _node("p")], - ), - ( - "recovery assignment", - "next-cockpit.js nextCockpitRecovery", - [*_REC, _node("div"), _node("strong")], - [*_REC, _node("div"), _node("span", "next-project-value--absent")], - ), - ( - "recovery goal", - "next-project.js nextProjectGoal", - [ - *_REC, - _node("section", "next-project-goal"), - _node( - "span", - "next-project-value", - "next-project-value--known", - "next-project-goal-text", - ), - ], - [ - *_REC, - _node("section", "next-project-goal"), - _node( - "span", - "next-project-value", - "next-project-value--absent", - "next-project-goal-text", - ), - ], - ), - ( - "reading clause", - "next-cockpit.js nextCockpitReading", - [*_CONTENT, _node("span", "next-cockpit-reading-clause")], - [*_CONTENT, _node("span", "next-cockpit-reading-clause-absent")], - ), - ( - "work summary", - "next-cockpit.js nextCockpitWork", - [*_CONTENT, _node("span", "next-cockpit-work-summary")], - [*_CONTENT, _node("p", "next-cockpit-work-absent")], - ), - ( - # Both classes, because the emitter sets both and the second is - # what distinguishes this machine-paired use of the reason class - # from its prose-paired one. - "graph row time", - "project.js projectGraphRow", - [_node("article", "pc-graph-row"), _node("time")], - [ - _node("article", "pc-graph-row"), - _node("span", "pc-substrate-reason", "pc-graph-time"), + *_CONTENT, + _node("section", "next-cockpit-landed"), + _node("div", "next-cockpit-landed-cards"), + _node("div", "next-cockpit-landed-card"), + _node("span", "next-cockpit-landed-value"), ], ), ) - # Prose absences are the other half of the same rule: they keep the floor. - # `.next-cockpit-recovery small` sizes the strip's storage cue at 12.5px and - # must not drag an absence sentence down with it, which is only decidable - # with the `.next-cockpit-content` wrapper in the path. - PROSE_ABSENCES = ( - ( - "assignment evidence absent", - "next-cockpit.js nextCockpitRecovery", - [*_REC, _node("div"), _node("small", "next-cockpit-evidence-missing")], - ), - ) - - # A caption labels the values beside it, so it must stay below them. The - # round-1 fix used a descendant selector and pulled this one level with the - # strip it labels, which is the same collapse of register the other way up. - CAPTION_BELOW_VALUE = ( - ( - "briefing caption", - [*_REC, _node("header"), _node("strong")], - [*_REC, _node("div"), _node("strong")], - ), - ) + css: str + tokens: dict[str, float] + rules: list[tuple[str, str, int]] @classmethod def setUpClass(cls) -> None: - cls.tokens, cls.rules = css_cascade.load( - pathlib.Path(__file__).resolve().parents[1] / "cargento_runtime" / "web" / "styles.css" - ) + web = pathlib.Path(__file__).resolve().parents[1] / "cargento_runtime" / "web" + cls.tokens, cls.rules = css_cascade.load(web / "styles.css") + cls.cockpit_js = (web / "next-cockpit.js").read_text(encoding="utf-8") - tokens: dict[str, float] - rules: list[tuple[str, str, int]] + cockpit_js: str def size(self, path: list[dict[str, object]]) -> float: resolved = css_cascade.resolve(path, self.tokens, self.rules) @@ -5803,78 +5643,38 @@ def size(self, path: list[dict[str, object]]) -> float: assert resolved is not None return resolved - def test_no_slot_draws_its_value_smaller_than_its_absence(self) -> None: - for name, emitter, value_path, absence_path in self.SLOTS: - with self.subTest(slot=name): - value, absence = self.size(value_path), self.size(absence_path) + def test_every_absence_this_change_raises_is_read_beside_its_value(self) -> None: + for name, emitter, absence_path, value_path in self.RAISED_ABSENCES: + with self.subTest(absence=name): + absence, value = self.size(absence_path), self.size(value_path) self.assertGreaterEqual( value, absence, - f"{name} ({emitter}): value {value}px against absence {absence}px, " + f'"{name}" ({emitter}): absence {absence}px against value {value}px, ' "so the absence outranks the fact it replaces", ) - # The six machine slots `design-next-ui.md` records as promoted. Comparing a - # value against its absence does not bind these: reverting the promotion - # drops both sides together and the comparison stays equal while the value - # leaves the tier the document says it is on. Measured, not assumed: that - # mutation survived until this list existed. - PROMOTED = ( - ( - "recovery published value", - [ - *_REC, - _node("div", "next-cockpit-waiting"), - _node("span", "next-project-value", "next-project-value--known"), - ], - ), - ("rail wait duration", [*_RAIL, _node("span", "next-rail-wait-duration")]), - ( - "rail token rate", - [ - *_RAIL, - _node("div", "next-delegation-metrics"), - _node("span", attrs=("data-next-delegation-rate",)), - ], - ), - ( - "rail pace and resets", - [*_RAIL, _node("div", "next-rail-capacity-caption"), _node("span")], - ), - ("entry-details timestamp", [*_TL, _node("div", "pc-entry-details"), _node("time")]), - ("published value", [*_TL, _node("div", "pc-entry-details"), _node("span", "pc-source")]), - ) - - def test_every_promoted_machine_slot_is_on_the_sentence_tier(self) -> None: - sentence = self.tokens["fs-sentence"] - for name, path in self.PROMOTED: - with self.subTest(slot=name): - self.assertEqual( - sentence, - self.size(path), - f"{name} left the sentence tier, which design-next-ui.md records it on", - ) - - def test_a_prose_absence_keeps_the_sentence_floor(self) -> None: - sentence = self.tokens["fs-sentence"] - for name, emitter, path in self.PROSE_ABSENCES: - with self.subTest(absence=name): - self.assertEqual( - sentence, - self.size(path), - f"{name} ({emitter}) does not resolve to the sentence tier", - ) + def test_the_revision_slot_cannot_invert_because_one_class_carries_both(self) -> None: + """The other raised absence has no value to be read beside. - def test_a_caption_stays_below_the_values_it_labels(self) -> None: - for name, caption_path, value_path in self.CAPTION_BELOW_VALUE: - with self.subTest(caption=name): - caption, value = self.size(caption_path), self.size(value_path) - self.assertLess( - caption, - value, - f"{name}: caption {caption}px against value {value}px, " - "so the label reads level with its own answer", - ) + `nextCockpitHeldTo` fills one span from a chain: a discard stamp, then a + revision line, then "No revision saved yet". Value and absence are the + same element with the same class, so they resolve identically whatever + the tier is. That is a stronger guarantee than a comparison, and it + holds only while the chain stays in one assignment, which is what this + asserts. + """ + chain = ( + "nextAnnotationDiscardStamp(annotation) ||\n" + ' nextProjectRevisionLine(annotation) || "No revision saved yet"' + ) + self.assertIn(chain, self.cockpit_js) + emitted = re.findall(r'class="next-cockpit-held-revision"', self.cockpit_js) + self.assertEqual( + 1, + len(emitted), + "the revision slot is emitted more than once, so the chain may have split", + ) @unittest.skipUnless(shutil.which("node"), "node not available") diff --git a/cargento/skills/cargento/tests/test_next_flag.py b/cargento/skills/cargento/tests/test_next_flag.py index 33640d160..3ffc6767b 100644 --- a/cargento/skills/cargento/tests/test_next_flag.py +++ b/cargento/skills/cargento/tests/test_next_flag.py @@ -64,9 +64,9 @@ def test_retired_next_query_is_not_a_page_alias(self) -> None: def test_the_canonical_loader_is_the_released_ui_bundle(self) -> None: page = frontend_page.load_page() - self.assertEqual(915_434, len(page)) + self.assertEqual(913_998, len(page)) self.assertEqual( - "b48cdd7ed6390894f85b3bad0eb8662c62592a58e532ac62eec0fb523622afb0", + "fabba3bc613af4f76e9c7d327d3a42f4a4e49acf4da73d2e5aa8b3e0f4d19866", hashlib.sha256(page).hexdigest(), ) diff --git a/cargento/skills/cargento/tests/test_next_page.py b/cargento/skills/cargento/tests/test_next_page.py index 092a0cec4..b1f95fad8 100644 --- a/cargento/skills/cargento/tests/test_next_page.py +++ b/cargento/skills/cargento/tests/test_next_page.py @@ -700,16 +700,16 @@ def test_load_page_preserves_its_byte_oracles(self) -> None: self.assertEqual(digest, hashlib.sha256(data).hexdigest()) styles = frontend_page.asset_path("styles.css").read_bytes() - self.assertEqual(112_140, len(styles)) + self.assertEqual(110_704, len(styles)) self.assertEqual( - "77dd73d39d3dcda3a8937e0c9b1e0f06336e68e6a0a8b1c0f19ef7b6de3bede9", + "02aa394649d128bef1a44d728e061d643a0535a6060650022931738add0dc194", hashlib.sha256(styles).hexdigest(), ) assembled = frontend_page.load_page() - self.assertEqual(915_434, len(assembled)) + self.assertEqual(913_998, len(assembled)) self.assertEqual( - "b48cdd7ed6390894f85b3bad0eb8662c62592a58e532ac62eec0fb523622afb0", + "fabba3bc613af4f76e9c7d327d3a42f4a4e49acf4da73d2e5aa8b3e0f4d19866", hashlib.sha256(assembled).hexdigest(), ) diff --git a/docs/design-next-ui.md b/docs/design-next-ui.md index bf17498df..938f978fc 100644 --- a/docs/design-next-ui.md +++ b/docs/design-next-ui.md @@ -110,22 +110,7 @@ region; a shared responsive block would make every view edit the same tail. Movi regions is an ownership change, not incidental cleanup. Board sentences have a 15px floor (`--fs-sentence`), at weight 500 and line-height 1.55. Labels -and compact controls sit on one tier, `--fs-label` at 11px. **Six machine slots are promoted off -that tier** because the absence that replaces each one is a sentence, and an absence must never -read larger than the fact it stands in for: `.next-rail-wait-duration`, the token rate in -`.next-delegation-metrics`, the pace and resets in `.next-rail-capacity-caption`, the timestamps in -`.pc-entry-details time` and `.pc-event-evidence time`, `.pc-semantic-timeline .pc-source`, and the -published values in `.next-cockpit-recovery .next-project-value`. So a timestamp or a rate is on -the label tier only where nothing replaces it with a sentence. - -**Where the machine string has a machine-shaped absence, the absence moves instead.** -`.next-cockpit-work-absent`, `.next-cockpit-reading-clause-absent` and `.pc-graph-time` sit with -the values they replace rather than at the sentence floor, because a rate, a timestamp and a -duration are compact on purpose and raising them removes the inversion by breaking the register. -That is the cheaper half of the same rule and it needs no emitter change, because each of those -slots already emits a class that separates its machine-paired use from its prose-paired one. -Prose absences in the same panels, `.next-cockpit-work-limit` and `.next-cockpit-work-dropped` -among them, keep the floor. Four +identifiers, timestamps, rates and compact controls sit on one tier, `--fs-label` at 11px. Four `:root` steps used to sit below 11px, at 9px, 9.5px, 10px and 10.5px, a 3px band nobody can rank, and 9px was the smallest step in the file. **Counts here read "at or below 12px" inclusively**, so the band held seven tokens: `--fs-column` 9px, `--fs-label` 9.5px, `--fs-meta` 10px, @@ -142,22 +127,18 @@ never a contrast defect: `--ink` on `--bg` recomputes to 16.36:1 and the asset t than 4.5:1 is green. DRC-4596 adds the guardrail that would close the gap, and lands after the change the figures justify. The absence explanations the prototype placed at 10px are sentences, so they take the sentence tier, and two of them had to leave an `

`'s `
` to get there. -A value is never drawn smaller than the absence that replaces it, nor than the caption that labels -it, which is what raising a container without raising its value rules quietly breaks. - -**Finding that class needs a census taken from the emitters, not from the stylesheet.** A value and -its absence are chosen by a ternary, so they never co-exist in one render: no sweep of co-existing -selectors sees the pair, and neither does reading a populated board, because only one branch is -ever on screen. Two passes over this stylesheet missed the same six slots for that reason before a -per-slot reading found them. `AnAbsenceNeverOutranksTheValueItReplacesTest` now lists the ternaries -and resolves both branches separately through `tests/css_cascade.py`; its numbers were checked -against `getComputedStyle` in a browser before they were trusted. Two exemptions in the first pass -were wrong on exactly this point: `.next-cockpit-recovery span` was read as a caption when it also -sizes every `nextProjectValue` span, and `.pc-entry-details time` as a timestamp when -`projectEventTime` swaps it for a reason. A rule is a caption only when nothing chooses it as the -answer, and only the emitter can say so: `.next-cockpit-recovery>header` qualifies because it holds -one fixed string, and `.next-usage-consent` is not in the census at all because it has no absence -branch to be paired with. The stylesheet +**Raising an absence is not safe on its own.** An absence and the value it replaces are chosen by a +ternary, so they never co-exist in one render: no sweep of co-existing selectors sees the pair, and +neither does reading a populated board, because only one branch is ever on screen. Raise the +absence without its value and the gap reads larger than the fact it stands in for. So only two +absences are on the sentence tier here, the two named above, and each is safe for a reason that +does not depend on finding every pair: "two axes, read separately" is drawn beside values already +on that tier, and "No revision saved yet" shares one class with the revision line it alternates +with, so both branches resolve identically whatever the tier is. +`AnAbsenceNeverOutranksTheValueItReplacesTest` asserts exactly those two and nothing wider. +**Every other absence keeps the size it had**, and DRC-4602 owns the audit that can raise them +safely, because doing so needs a census of the emitters that this stylesheet cannot supply. The +stylesheet retains scale tokens and literal sizes. The asset test pins the dark palette and checks text inks above 4.5:1 on the ground, panel and inset surfaces; it does not enforce all font sizes or spacing between contrast steps.