diff --git a/TESTING.md b/TESTING.md
index d3a3ad1..560d167 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -361,8 +361,8 @@ of the approved concept's `docs/concepts/workspace/style.css`. Never edit the co
it is only rewritten from the concept with `npm run --prefix tools/vendor sync:concept`.
`npm run --prefix tools/vendor check` runs `sync-concept-css.mjs --check`, which fails
on a missing copy or names the first differing byte. `workspace.html` loads, in order,
-the toolkit controls, Viewer.js, `concept.css`, the page stylesheets that their own
-PRs retire, and the production overlay `workspace.css` last.
+the toolkit controls, Viewer.js, `concept.css` and the production overlay `workspace.css`
+last; every page stylesheet that waited for its page PR is retired.
`tests/vendor/concept-css.test.mjs` asserts that order, the drift check, and that no
other page or canvas script loads workspace CSS.
@@ -373,19 +373,28 @@ D-number must be one of this spec's (#249): a comment that cites any other issue
as `D5 (#245)`, is that issue's ruling and is not a marker, so write `#245 decision 5`
for those. The lint test fails if `workspace.css` is missing. The match is by exact
selector, so a narrower selector over a concept rule (for example
-`.ca-tools dialog.prompt-sender`) passes the lint. Such rules, and the page-stylesheet
-leftovers such as `library.css` `.row-actions`, name the PR that retires them, by
-convention.
+`.ca-tools dialog.prompt-sender`) passes the lint; by convention such rules carry a
+marker too.
-The lint also runs over every page stylesheet that loads before the overlay and is
-still kept (`library.css`; test `kept page stylesheets mark their concept-selector
-overrides`). PR D deleted `images.css` and `inspector.css` (test `the retired shell,
-vocabulary, editor, Images and inspector stylesheets stay deleted`); their few
-production-only rules with a live consumer (inspector wrapping and the inert download,
-the narrow Images container, the Library and recorded-run prompt blocks, the Archive
-negative's `section.prompt-block`, the prompt sender) moved to the overlay under
-`#249 D3`, and `the overlay keeps no retired Images page or inspector rules` keeps the
-replaced card, filter, toolbar, preview, facts and inspector rules out.
+PR D deleted `images.css` and `inspector.css`, and PR F-b deleted `library.css`, the
+last page stylesheet (test `the retired shell, vocabulary, editor, Images, inspector and
+library stylesheets stay deleted`). `the overlay keeps no retired Images page or
+inspector rules` keeps the replaced card, filter, toolbar, preview, facts and inspector
+rules out, and `the overlay keeps no rule waiting for a page rebuild` keeps out the
+interim guards and the history page's private role colors. Every rule that stayed has
+a live consumer and a marker: the Library rows windowed (D3) and the shelf's states
+(D40), the history panel's wrap, archived-lane and coarse-target rules (D45), and under
+**D48** the production surfaces the concept never shows, drawn in the concept's tokens
+(`--green`, `--red`, `--ca-*`): recorded executions and the Recorded run dialog (#209,
+D26), `pre.prompt-text` for stored text in the frozen field provenance and the Archive
+negative confirmation, whose `section.prompt-block` (#235) keeps its rules too, and the
+acknowledged prompt sender (spec §2.2). Rules that repeated the concept
+(the `.library-grid` tracks and breakpoints, the history `.row-actions` flex row and
+button size, `summary` cursors, `h2` margins, the sender and Recorded run dialogs'
+colors and border) or had no effect (`min-inline-size` on block children, a
+`.recorded-run-dialog pre` limit, a 700px padding that a later rule overrode) were
+deleted; the workspace browser specs on Chromium and the concept specs on touch prove
+the pages unchanged.
**Comparison method.** Production must equal the concept except for the recorded
deviations in the #249 design (spec §8, D-numbers). It is never compared against
@@ -853,6 +862,11 @@ Recorded deviations on this page:
The Images page keeps its caption. With no saved variant the page shows every image
of the positive under "All images for this positive, including unassigned images. …"
and no empty copy (a state the concept never reaches).
+- **D48** (#249 PR F-b): production surfaces the concept never shows keep their own
+ overlay rules, drawn in the concept's tokens: recorded executions and the Recorded run
+ dialog (#209, D26), the Archive negative confirmation (#235) and the acknowledged
+ prompt sender (spec §2.2). With `library.css` retired, these rules live in
+ `workspace.css` (see "Concept shell and comparison method").
The run dot reads `GET /prompt_manager/library/{positive_id}/history/pairs/{pair_id}/newest`
(see "Chronological Library history"), and each lane's "N runs" is the pair's
@@ -1320,8 +1334,10 @@ Initial production acceptance requires maintainer inspection of paired PNGs and
geometry; never update snapshots automatically or mask content to bless a gap.
Data-dependent text/images/counts and the clearly labeled saved/frozen/current
distinctions are documented in the comparison helper. The Images card tag
-button (#249 D9) and shared role-block
-spacing/padding/glyph polish remain outside #209. The
+button (#249 D9) remains outside #209; every prompt block now renders the concept's
+`section.prompt-section` (D22), so the shared role-block spacing, padding and glyph
+gap is closed, and only the production-only surfaces above keep their own rules
+(D48). The
responsive production Viewer keeps its toolbar in the media panel and prompt details readable rather than reproducing the concept's
mobile toolbar collision. These are explicit remaining visual differences, not
a whole-application or full-gallery/Viewer pixel-parity claim.
diff --git a/tests/browser/history-concept.spec.mjs b/tests/browser/history-concept.spec.mjs
index ebd6d4f..198da72 100644
--- a/tests/browser/history-concept.spec.mjs
+++ b/tests/browser/history-concept.spec.mjs
@@ -99,7 +99,7 @@ async function openProductionHistory(page,theme,motion,positiveId) {
await selectVariant(page,SELECTED);
}
-// D45: the recorded production rules that lay the panel out (library.css): the panel heading and the legend row wrap,
+// D45: the recorded production rules that lay the panel out (workspace.css): the panel heading and the legend row wrap,
// All images wraps its label, and a lane's text wraps before a shown repeat-run dot. Applied to the concept so the browser
// lays out what follows; at 1600px nothing wraps.
const D45_LAYOUT=`.timeline-panel > .panel-heading, .timeline-lower { flex-wrap: wrap; }
diff --git a/tests/browser/workspace-history.spec.mjs b/tests/browser/workspace-history.spec.mjs
index 875ea7e..c28e591 100644
--- a/tests/browser/workspace-history.spec.mjs
+++ b/tests/browser/workspace-history.spec.mjs
@@ -381,7 +381,7 @@ for(const [width,height] of [[1600,1100],[390,844]])for(const theme of ['dark','
await recorded(page).getByRole('button',{name:'View selected run',exact:true}).click();await expect(dialog(page)).toContainText(oldRun);await expect(dialog(page).getByRole('button',{name:'Effective',exact:true})).toHaveAttribute('aria-pressed','true');
await reviewCapture(page,testInfo,`outside-selected-dialog-${width}-${theme}`,dialog(page));
const hashes={};
- for(const path of ['docs/concepts/workspace/index.html','docs/concepts/workspace/app.js','docs/concepts/workspace/style.css','database/history_reads.py','py/api/history.py','web/js/workspace/history-state.mjs','web/js/workspace/history-media.mjs','web/js/workspace/history.mjs','web/js/workspace/recorded-run.mjs','web/js/workspace/library.css'])hashes[path]=createHash('sha256').update(await readFile(new URL(`../../${path}`,import.meta.url))).digest('hex');
+ for(const path of ['docs/concepts/workspace/index.html','docs/concepts/workspace/app.js','docs/concepts/workspace/style.css','database/history_reads.py','py/api/history.py','web/js/workspace/history-state.mjs','web/js/workspace/history-media.mjs','web/js/workspace/history.mjs','web/js/workspace/recorded-run.mjs','web/js/workspace/workspace.css'])hashes[path]=createHash('sha256').update(await readFile(new URL(`../../${path}`,import.meta.url))).digest('hex');
expect(hashes['docs/concepts/workspace/index.html']).toBe('d84712c7284bb232919a1ed9b154575c8ac574f63b2404a4ac4bc06cbf323c03');expect(hashes['docs/concepts/workspace/app.js']).toBe('e33e7652da12a68cd3fe31e94d77d680aa439b6434ceee94f21cc4ce8883263e');expect(hashes['docs/concepts/workspace/style.css']).toBe('2c479ca1a2ddbbd14a550eae27dd731c326ccdb91fe9ddc87e2b81cd5298fce8');
const manifest=testInfo.outputPath('final-fix-visual-source-and-scroll.json');await writeFile(manifest,JSON.stringify({width,height,theme,oldRun,before,after,hashes},null,2));
await testInfo.attach('final-fix-visual-source-and-scroll.json',{path:manifest,contentType:'application/json'});
diff --git a/tests/browser/workspace-library-editor.spec.mjs b/tests/browser/workspace-library-editor.spec.mjs
index 3101c4a..7560fbf 100644
--- a/tests/browser/workspace-library-editor.spec.mjs
+++ b/tests/browser/workspace-library-editor.spec.mjs
@@ -899,7 +899,7 @@ test('long and error states retain footer focus and reflow',async({page})=>{
// Task A2: the editor dialogs keep matching the concept on the concept base stylesheet, with library-editor.css retired.
const CONCEPT_BASE=['js/shared/toolkit/controls.css','lib/viewerjs/viewer.min.css','js/workspace/concept.css',
- 'js/workspace/library.css','js/workspace/workspace.css'];
+ 'js/workspace/workspace.css'];
for(const [width,height,theme] of [[1600,1100,'dark'],[390,844,'light']])test(`concept base keeps the tags screen and editor dialogs equal to the concept ${width} ${theme}`,async({page})=>{
await page.setViewportSize({width,height});const f=page.fixture;const references={};
for(const mode of Object.keys(conceptModes)) {
diff --git a/tests/browser/workspace-vocabulary.spec.mjs b/tests/browser/workspace-vocabulary.spec.mjs
index 3fb8031..f29b2af 100644
--- a/tests/browser/workspace-vocabulary.spec.mjs
+++ b/tests/browser/workspace-vocabulary.spec.mjs
@@ -825,7 +825,7 @@ test('mutation check: the comparison reports a changed surface padding and a cha
// Task A2: the workspace loads the verbatim concept stylesheet as its base and one overlay last; the screen and its
// dialogs must stay equal to the concept with the retired vocabulary and editor sheets gone.
const CONCEPT_BASE=['js/shared/toolkit/controls.css','lib/viewerjs/viewer.min.css','js/workspace/concept.css',
- 'js/workspace/library.css','js/workspace/workspace.css'];
+ 'js/workspace/workspace.css'];
const loadedSheets=page=>page.evaluate(()=>[...document.styleSheets].filter(sheet=>sheet.href).map(sheet=>new URL(sheet.href).pathname.replace(/^\/prompt_manager\//,'')));
for(const [width,height,theme] of [[1600,1100,'dark'],[390,844,'light']])test(`concept base keeps the tags screen and editor dialogs equal to the concept ${width} ${theme}`,async({page})=>{
await page.setViewportSize({width,height});
diff --git a/tests/vendor/concept-css.test.mjs b/tests/vendor/concept-css.test.mjs
index 13fecaf..6cedb95 100644
--- a/tests/vendor/concept-css.test.mjs
+++ b/tests/vendor/concept-css.test.mjs
@@ -73,11 +73,13 @@ test('overlay rules on concept selectors carry a decision marker', () => {
assert.deepEqual(violations, []);
});
-// Each kept page stylesheet is linted: a page rule on a concept selector is an override like the overlay's.
-test('kept page stylesheets mark their concept-selector overrides', () => {
- for (const name of ['library.css']) {
- const { violations } = lintOverlay(readFileSync(concept, 'utf8'), readFileSync(join(root, 'web/js/workspace', name), 'utf8'));
- assert.deepEqual(violations, [], name);
+// #249 PR F-b Task 3: every page is rebuilt, so no rule waits for a page PR any more and the history page's
+// production-only rules live in the overlay in the concept's tokens (D48).
+test('the overlay keeps no rule waiting for a page rebuild', () => {
+ const css = readFileSync(join(root, 'web/js/workspace/workspace.css'), 'utf8');
+ for (const retired of ['until PR', 'PR D */', 'PR F */', 'PR E/F', 'until their pages rebuild', 'Interim page guards',
+ '.library-chip', '--library-positive', '--library-negative', '--prompt-role']) {
+ assert.equal(css.includes(retired), false, `${retired} is retired`);
}
});
@@ -100,22 +102,21 @@ test('only workspace.html loads workspace CSS', () => {
assert.ok(offenders.every(path => path === 'web/workspace.html'), `unexpected consumers: ${offenders.join(', ')}`);
});
-// Task A2 order: the concept's own order (toolkit, Viewer.js, concept) with the page stylesheets that their own PRs
-// retire, then the production overlay last.
-test('workspace.html loads controls, viewer, concept, page CSS and the overlay in order', () => {
+// Task A2 order: the concept's own order (toolkit, Viewer.js, concept), then the production overlay last; the page
+// stylesheets that waited for their PRs are all retired (PR F-b deleted the last, library.css).
+test('workspace.html loads controls, viewer, concept and the overlay in order', () => {
const html = readFileSync(join(root, 'web/workspace.html'), 'utf8');
const hrefs = [...html.matchAll(/]+rel="stylesheet"[^>]+href="([^"]+)"/g)].map(match => match[1]);
assert.deepEqual(hrefs, [
'js/shared/toolkit/controls.css',
'lib/viewerjs/viewer.min.css',
'js/workspace/concept.css',
- 'js/workspace/library.css',
'js/workspace/workspace.css',
]);
});
-test('the retired shell, vocabulary, editor, Images and inspector stylesheets stay deleted', () => {
- for (const name of ['style.css', 'vocabulary.css', 'library-editor.css', 'images.css', 'inspector.css']) {
+test('the retired shell, vocabulary, editor, Images, inspector and library stylesheets stay deleted', () => {
+ for (const name of ['style.css', 'vocabulary.css', 'library-editor.css', 'images.css', 'inspector.css', 'library.css']) {
assert.equal(existsSync(join(root, 'web/js/workspace', name)), false, `web/js/workspace/${name} must not come back`);
}
});
diff --git a/web/js/workspace/library.css b/web/js/workspace/library.css
deleted file mode 100644
index 730e474..0000000
--- a/web/js/workspace/library.css
+++ /dev/null
@@ -1,97 +0,0 @@
-main.library-main { --library-positive: #73dda9; }
-.library-main :where(.library-shelf, .library-history, .library-linked-images) { min-inline-size: 0; }
-.library-main .library-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; overflow-anchor: none; }
-.library-main .library-row { grid-column: 1 / -1; display: grid; grid-template-columns: inherit; gap: 18px; min-inline-size: 0; }
-.library-main .library-row-spacer { grid-column: 1 / -1; pointer-events: none; }
-.library-main .library-continuation { padding-block: 22px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
-.library-main .library-status { flex: 1 1 240px; color: var(--ca-muted); }
-.library-main .library-surface { padding: 22px; background: var(--ca-surface); border: 1px solid var(--ca-line); border-radius: 7px; margin-block: 18px; }
-@media (min-width: 1700px) { .library-main .library-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
-@media (max-width: 700px) { .library-main .library-grid { grid-template-columns: minmax(0, 1fr); } .library-main .library-surface { padding: 16px; } }
-
-[data-ca-theme="light"] main.library-main { --library-positive: #176b46; }
-
-/* Role colors and branch geometry follow the approved history surface. */
-main.library-main { --library-negative: #ff8c98; }
-[data-ca-theme="light"] main.library-main { --library-negative: #b71931; }
-.library-main :where(.library-surface, .library-recorded-container, .recorded-history) { min-inline-size: 0; }
-.history-positive-lane::before { content: "+"; position: absolute; inset-block-start: -13px; inset-inline-start: 0; border-radius: 50%; inline-size: 25px; block-size: 25px; background: var(--library-positive); color: #172b22; text-align: center; font-size: 14px; line-height: 21px; }
-.history-positive-lane > span { position: absolute; inset-block-start: -25px; inset-inline-start: 38px; font-size: 10px; color: var(--library-positive); }
-.history-context-branch::before { content: ""; position: absolute; inset-block-start: -25px; inset-inline-start: -30px; inline-size: 30px; block-size: 25px; border-inline-start: 2px solid var(--library-negative); border-block-end: 2px solid var(--library-negative); border-end-start-radius: 22px; }
-.history-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-block: 14px; }
-/* D45: the panel's paging buttons wrap like the recorded toolbar's. */
-.ca-tools .library-main :where(.timeline-panel, .history-toolbar) > button { max-inline-size: 100%; white-space: normal; }
-.recorded-history > .muted, .history-count, .history-scope, .history-status, .history-legend { font-size: 11px; line-height: 1.65; overflow-wrap: anywhere; }
-.library-main .recorded-history > h2 { font-size: 14px; line-height: 1.5; }
-.library-main .recorded-history > h2 + p { font-size: 12px; line-height: 1.5; }
-.history-scope summary { cursor: pointer; }
-.history-timeline { position: relative; box-sizing: border-box; inline-size: 100%; max-inline-size: 100%; max-block-size: 490px; overflow: auto; overscroll-behavior: contain; overflow-anchor: none; border: 1px solid var(--ca-line); border-radius: 6px; background: var(--ca-field); }
-.history-canvas { position: relative; min-inline-size: calc(100% - 40px); margin: 18px 20px; }
-.history-positive-lane { position: absolute; inset-block-start: 17px; inset-inline: 0; border-block-start: 2px solid var(--library-positive); color: var(--library-positive); font-size: 10px; line-height: 1.6; padding-inline-start: 38px; }
-.history-earlier-edge { position: absolute; inset: 0 auto 0 0; inline-size: 1px; }
-.history-context-branch { position: absolute; inset-inline-end: 0; block-size: 57px; border-block-start: 2px solid color-mix(in srgb, var(--library-negative) 60%, transparent); }
-.history-context-label { position: absolute; inset-inline-start: 43px; inset-block-start: 6px; inline-size: 370px; max-inline-size: calc(100% - 43px); font-size: 10px; line-height: 1.5; color: var(--ca-muted); overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: clip; }
-.history-run-list { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; }
-.history-run-column { position: absolute; inset-block: 0; inline-size: 64px; pointer-events: none; }
-.history-position { position: absolute; inset-block-start: 41px; font-size: 10px; color: var(--ca-muted); }
-.ca-tools .history-run-marker { position: absolute; pointer-events: auto; transform: translate(-50%, -50%); inline-size: 12px; block-size: 12px; min-inline-size: 12px; min-block-size: 12px; border: 2px solid var(--library-negative); border-radius: 50%; background: var(--ca-field); padding: 0; color: var(--ca-field); font-size: 16px; line-height: 1; }
-.ca-tools .history-context-anchor { inline-size: 29px; block-size: 29px; min-inline-size: 29px; min-block-size: 29px; background: var(--library-negative); color: #38151b; font-size: 12px; }
-.history-run-marker[aria-pressed="true"], .history-run-marker:focus-visible { outline: 3px solid var(--ca-focus); outline-offset: 4px; }
-.history-run-marker[aria-disabled="true"] { opacity: .6; cursor: progress; }
-.recorded-run-dialog { box-sizing: border-box; inline-size: min(1100px, calc(100% - 32px)); max-inline-size: calc(100% - 32px); max-block-size: calc(100dvh - 32px); padding: 22px; border: 1px solid var(--ca-border); border-radius: 7px; color: var(--ca-text); background: var(--ca-surface); overflow: auto; overscroll-behavior: contain; scrollbar-gutter: stable; overflow-wrap: anywhere; }
-.recorded-run-dialog::backdrop { background: #0009; }
-.recorded-run-header { display: flex; gap: 12px; align-items: center; justify-content: space-between; }
-.recorded-run-header h2 { margin: 0; font-size: 20px; }
-.recorded-run-body, .recorded-run-body > section { min-inline-size: 0; }
-.recorded-run-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; font-size: 12px; }
-.recorded-run-facts dt { color: var(--ca-muted); font-size: 10px; }
-.recorded-run-facts dd { margin: 4px 0 0; white-space: pre-wrap; }
-.recorded-run-anchors, .recorded-run-mode { display: flex; flex-wrap: wrap; gap: 8px; margin-block: 18px; }
-.recorded-run-dialog [aria-pressed="true"] { outline: 2px solid var(--ca-focus); outline-offset: 2px; }
-.recorded-run-dialog :where(.muted:not(.prompt-text), .recorded-output-status) { font-size: 11px; line-height: 1.65; }
-.recorded-provenance { border-block-start: 1px solid var(--ca-line); padding-block-start: 12px; margin-block: 18px; }
-.recorded-provenance summary { cursor: pointer; }
-.recorded-run-dialog pre { white-space: pre-wrap; overflow-wrap: anywhere; max-inline-size: 100%; }
-.recorded-outputs { margin-block: 24px; }
-.recorded-receipts { list-style: decimal; padding-inline-start: 24px; }
-.recorded-receipt { border: 1px solid var(--ca-line); border-radius: 5px; padding: 12px; margin-block: 10px; font-size: 12px; }
-.recorded-receipt > div { margin-block-end: 10px; }
-.recorded-receipt[aria-current="true"] { outline: 2px solid var(--ca-focus); outline-offset: 2px; }
-.recorded-current-file { border-block-start: 1px solid var(--ca-line); padding-block-start: 16px; }
-@media (max-width: 700px) {
- .library-main .library-surface { padding: 22px; }
- .recorded-run-dialog { padding: 16px; }
- .recorded-run-facts { grid-template-columns: minmax(0, 1fr); }
-}
-@media (pointer: coarse) {
- .recorded-history :where(.history-toolbar button, .history-scope summary) { min-inline-size: 44px; min-block-size: 44px; }
- .ca-tools .history-run-marker { inline-size: 44px; block-size: 44px; min-inline-size: 44px; min-block-size: 44px; border: 0; background: transparent; color: transparent; }
- .ca-tools .history-run-marker::before { content: ""; position: absolute; inset: 16px; box-sizing: border-box; border: 2px solid var(--library-negative); border-radius: 50%; background: var(--ca-field); }
- .ca-tools .history-context-anchor::before { content: "−"; inset: 7.5px; inline-size: 29px; block-size: 29px; line-height: 25px; color: #38151b; background: var(--library-negative); }
- /* D45: a coarse pointer reaches the concept dot through a 47px target. */
- .ca-tools .library-history .variant-dot::after { content: ""; position: absolute; inset: -9px; }
- /* D45: the 12px repeat-run dot (8px inside its border) gets a 44px coarse target the same way. */
- .ca-tools .library-history .run-dot::after { content: ""; position: absolute; inset: -18px; }
- .ca-tools .recorded-run-dialog :where(button, summary) { min-inline-size: 44px; min-block-size: 44px; }
-}
-@media (forced-colors: active) {
- .history-context-branch, .history-context-branch::before, .history-run-marker, .history-run-marker::before { border-color: CanvasText; }
- .history-run-marker[aria-pressed="true"] { outline-color: Highlight; }
-}
-
-/* Panel actions follow the concept (style.css 294-300, 540-549); the page heading is the concept's own (Task A5).
- These .row-actions rules restyle the concept's own selector for the history panels only (the shelf's card footer
- keeps the concept's) and are retired in PR F (history). */
-.library-main .library-history .row-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
-.ca-tools .library-main .library-history .row-actions button { min-block-size: 32px; font-size: 12px; max-inline-size: 100%; white-space: normal; }
-@media (pointer: coarse) { .ca-tools .library-main .library-history .row-actions button { min-block-size: 40px; } }
-/* D45: archived pairs (#235) are concept lanes at reduced emphasis (the .6 disabled-marker opacity); text stays at full contrast. */
-.library-history .variant-lane.library-variant-archived::before, .ca-tools .library-history .library-variant-archived .variant-dot { opacity: .6; }
-.ca-tools .library-main .library-variant-restore { position: static; margin-inline-start: 8px; min-block-size: 24px; padding: 2px 8px; font-size: 10px; vertical-align: middle; }
-@media (pointer: coarse) { .ca-tools .library-main .library-variant-restore { min-block-size: 40px; } }
-/* D45: at 390px and 200% zoom the panel heading and the legend row wrap, and the All images button wraps its label,
- so the page never scrolls sideways (the retired production footer did the same). */
-.library-history .timeline-panel > .panel-heading, .library-history .timeline-lower { flex-wrap: wrap; }
-/* D45: while the repeat-run dot shows (left: 190px), the lane text wraps inside the 57px lane before it (43px + 139px). */
-.library-history .variant-lane:has(> .run-dot:not([hidden])) > span { max-inline-size: 139px; overflow-wrap: anywhere; }
-.ca-tools .library-history .timeline-lower > button { /* D45 */ max-inline-size: 100%; white-space: normal; }
diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css
index d9df2b9..c5a8413 100644
--- a/web/js/workspace/workspace.css
+++ b/web/js/workspace/workspace.css
@@ -137,18 +137,7 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
#viewer-toolbar > .media-download { font-size: 10px; /* D34: as the concept's `.viewer-toolbar button` */ }
}
-/* Interim page guards: concept element rules (`.ca-tools dialog`, `pre`) now reach production dialogs and prompt text
- that keep their own design until their page PR rebuilds them in concept markup. */
-.ca-tools dialog.prompt-sender { width: min(760px, calc(100% - 32px)); max-width: none; max-height: calc(100dvh - 32px); padding: 22px; border-color: var(--ca-line); border-radius: 12px; } /* PR D */
-.ca-tools dialog.prompt-sender::backdrop { background: #000b; } /* PR D */
-.ca-tools dialog.recorded-run-dialog { width: min(1100px, calc(100% - 32px)); max-width: calc(100% - 32px); max-height: calc(100dvh - 32px); padding: 22px; border-radius: 7px; } /* PR F */
.prompt-text[tabindex]:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 3px; } /* production-only: a confirmed prompt stays keyboard-reachable (#249 D22) */
-@media (pointer: coarse) {
- .ca-tools .recorded-history .history-toolbar button { min-height: 44px; } /* PR F: concept coarse buttons are 40px */
-}
-@media (max-width: 700px) {
- .ca-tools dialog.recorded-run-dialog { padding: 16px; } /* PR F */
-}
/* Images side panel (#249 D2), production-only. Real prompts can be far longer than the concept's samples, so the sticky
panel scrolls within the window and its footer stays reachable; where the concept unsticks it, it grows instead. */
@@ -177,38 +166,6 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
/* #249 D42: a discovery alert a rescan leaves scrolls clear of the concept's fixed toast (22px inset, 12px at 700px and
below, two 12px lines at 390px), never under it; the toast itself keeps the concept's placement. */
.options-status { font-size: 12px; padding-inline: 4px; scroll-margin-block-end: 96px; /* D42 */ }
-/* Until their pages rebuild (PR F history): the history page's and the recorded run's prompt blocks keep their
- spacing (on the shelf's cards `.prompt-inspector` is `display: contents`, so its margin is inert there), the Archive
- negative dialog keeps `section.prompt-block`, and the shared prompt sender keeps its dialog and send button. */
-.prompt-inspector { margin-block-start: 22px; min-inline-size: 0; }
-.prompt-block { border-block-start: 1px solid var(--ca-line); padding-block: 13px; min-inline-size: 0; }
-.prompt-negative { --prompt-role: #ff8c98; }
-[data-ca-theme="light"] .prompt-negative { --prompt-role: #b71931; }
-.prompt-block-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
-.prompt-block h3 { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; margin: 0; color: var(--prompt-role); }
-.prompt-empty, .prompt-block > .muted { font-size: 10px; }
-/* PR E/F: the Archive dialog and the frozen field provenance keep `pre.prompt-text`, guarded against the concept `pre` rule. */
-pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space: pre-wrap; overflow-wrap: anywhere; margin-block: 10px; background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; }
-.prompt-send-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; color: var(--ca-focus); }
-.prompt-send-button svg { inline-size: 16px; block-size: 16px; flex-shrink: 0; }
-.prompt-sender { background: var(--ca-surface); color: var(--ca-text); border: 1px solid var(--ca-line); border-radius: 12px; padding: 22px; inline-size: min(760px, calc(100% - 32px)); max-inline-size: none; max-block-size: calc(100dvh - 32px); overflow: auto; }
-.prompt-sender::backdrop { background: #000b; }
-.prompt-sender-header, .prompt-sender-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
-.prompt-sender-header h2 { font-size: 20px; margin: 0; }
-.prompt-sender-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-block: 20px; }
-.prompt-sender-options .ca-field { display: flex; flex-direction: column; align-items: stretch; gap: 6px; }
-.prompt-sender select { inline-size: 100%; min-block-size: 38px; }
-.prompt-sender-status { font-size: 13px; line-height: 1.6; }
-.prompt-sender-status[role="alert"] { color: var(--ca-error-text); }
-.prompt-sender-preview { margin-block: 20px; min-inline-size: 0; }
-.prompt-sender-field { border: 1px solid var(--ca-line); border-radius: 7px; padding: 14px; margin-block: 12px; min-inline-size: 0; }
-.prompt-sender-field :where(pre, textarea), .prompt-sender-composition { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; font-size: 12px; line-height: 1.65; max-inline-size: 100%; }
-.prompt-sender-field textarea { inline-size: 100%; min-block-size: 100px; resize: vertical; }
-.prompt-sender-composition { padding: 12px; background: var(--ca-field); border-radius: 6px; }
-.prompt-sender-actions { justify-content: flex-end; margin-block-start: 18px; }
-@media (max-width: 780px) {
- .prompt-sender-options { grid-template-columns: minmax(0, 1fr); }
-}
/* Prompt library cards (#249 PR E): the concept's `article.prompt-card` (s653–708) with real prompts, titles and media. */
/* #249 D23: real prompts are far longer than the concept's samples, so the card clamps the positive to two lines; copy
@@ -230,3 +187,126 @@ pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space
/* #249 D23: a parent with one preview, or none, fills the art row instead of leaving the concept's second cell empty. */
.prompt-card .card-art > :only-child { grid-column: 1 / -1; /* D23 */ }
.prompt-card .card-art > .small { display: grid; place-content: center; min-width: 0; color: var(--ca-muted); /* D23 */ }
+
+/* Prompt library shelf (#249 PR E). #249 D3: rows stay windowed, as the Images grid's do; the concept's `.library-grid`
+ tracks and breakpoints lay them out, each row spans them, and appended rows never anchor the scroll. */
+.library-main .library-grid { overflow-anchor: none; /* D3 */ }
+.library-main .library-row { grid-column: 1 / -1; display: grid; grid-template-columns: inherit; gap: 18px; min-inline-size: 0; /* D3 */ }
+.library-main .library-row-spacer { grid-column: 1 / -1; pointer-events: none; /* D3 */ }
+/* #249 D40: the shelf's and history's production-only load, failure and continuation states. */
+.library-main .library-continuation { padding-block: 22px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; /* D40 */ }
+.library-main .library-status { flex: 1 1 240px; color: var(--ca-muted); /* D40 */ }
+
+/* History variants panel (#249 PR F). */
+/* D45: the panel's actions wrap their labels at 390px and 200% zoom (the toolkit's buttons never wrap). */
+.ca-tools .library-main .library-history .row-actions button { max-inline-size: 100%; white-space: normal; /* D45 */ }
+/* D45: the panel's paging buttons wrap like the recorded toolbar's. */
+.ca-tools .library-main :where(.timeline-panel, .history-toolbar) > button { max-inline-size: 100%; white-space: normal; /* D45 */ }
+/* D45: archived pairs (#235) are concept lanes at reduced emphasis (the .6 disabled-marker opacity); text stays at full contrast. */
+.library-history .variant-lane.library-variant-archived::before, .ca-tools .library-history .library-variant-archived .variant-dot { opacity: .6; /* D45 */ }
+.ca-tools .library-main .library-variant-restore { position: static; margin-inline-start: 8px; min-block-size: 24px; padding: 2px 8px; font-size: 10px; vertical-align: middle; /* D45 */ }
+@media (pointer: coarse) { .ca-tools .library-main .library-variant-restore { min-block-size: 40px; /* D45 */ } }
+/* D45: at 390px and 200% zoom the panel heading and the legend row wrap, and the All images button wraps its label,
+ so the page never scrolls sideways (the retired production footer did the same). */
+.library-history .timeline-panel > .panel-heading, .library-history .timeline-lower { flex-wrap: wrap; /* D45 */ }
+/* D45: while the repeat-run dot shows (left: 190px), the lane text wraps inside the 57px lane before it (43px + 139px). */
+.library-history .variant-lane:has(> .run-dot:not([hidden])) > span { max-inline-size: 139px; overflow-wrap: anywhere; /* D45 */ }
+.ca-tools .library-history .timeline-lower > button { max-inline-size: 100%; white-space: normal; /* D45 */ }
+@media (pointer: coarse) {
+ /* D45: a coarse pointer reaches the concept dot through a 47px target. */
+ .ca-tools .library-history .variant-dot::after { content: ""; position: absolute; inset: -9px; }
+ /* D45: the 12px repeat-run dot (8px inside its border) gets a 44px coarse target the same way. */
+ .ca-tools .library-history .run-dot::after { content: ""; position: absolute; inset: -18px; }
+}
+
+/* #249 D48: production surfaces the concept never shows keep their own rules, in the concept's tokens: recorded
+ executions and the Recorded run dialog (#209, D26), the Archive negative confirmation (#235) and the acknowledged
+ prompt sender (spec §2.2). The dialog rules name `dialog` so they win over the concept's `.ca-tools dialog`. */
+/* D48: recorded executions (#209) below the shared gallery. */
+.library-main .library-surface { padding: 22px; background: var(--ca-surface); border: 1px solid var(--ca-line); border-radius: 7px; margin-block: 18px; /* D48 */ }
+.history-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-block: 14px; /* D48 */ }
+.recorded-history > .muted, .history-count, .history-scope, .history-status, .history-legend { font-size: 11px; line-height: 1.65; overflow-wrap: anywhere; /* D48 */ }
+.library-main .recorded-history > h2 { font-size: 14px; line-height: 1.5; /* D48 */ }
+.library-main .recorded-history > h2 + p { font-size: 12px; line-height: 1.5; /* D48 */ }
+.history-timeline { position: relative; box-sizing: border-box; inline-size: 100%; max-inline-size: 100%; max-block-size: 490px; overflow: auto; overscroll-behavior: contain; overflow-anchor: none; border: 1px solid var(--ca-line); border-radius: 6px; background: var(--ca-field); /* D48 */ }
+.history-canvas { position: relative; min-inline-size: calc(100% - 40px); margin: 18px 20px; /* D48 */ }
+.history-positive-lane { position: absolute; inset-block-start: 17px; inset-inline: 0; border-block-start: 2px solid var(--green); color: var(--green); font-size: 10px; line-height: 1.6; padding-inline-start: 38px; /* D48 */ }
+.history-positive-lane::before { content: "+"; position: absolute; inset-block-start: -13px; inset-inline-start: 0; border-radius: 50%; inline-size: 25px; block-size: 25px; background: var(--green); color: #172b22; text-align: center; font-size: 14px; line-height: 21px; /* D48 */ }
+.history-positive-lane > span { position: absolute; inset-block-start: -25px; inset-inline-start: 38px; font-size: 10px; color: var(--green); /* D48 */ }
+.history-earlier-edge { position: absolute; inset: 0 auto 0 0; inline-size: 1px; /* D48 */ }
+.history-context-branch { position: absolute; inset-inline-end: 0; block-size: 57px; border-block-start: 2px solid color-mix(in srgb, var(--red) 60%, transparent); /* D48 */ }
+.history-context-branch::before { content: ""; position: absolute; inset-block-start: -25px; inset-inline-start: -30px; inline-size: 30px; block-size: 25px; border-inline-start: 2px solid var(--red); border-block-end: 2px solid var(--red); border-end-start-radius: 22px; /* D48 */ }
+.history-context-label { position: absolute; inset-inline-start: 43px; inset-block-start: 6px; inline-size: 370px; max-inline-size: calc(100% - 43px); font-size: 10px; line-height: 1.5; color: var(--ca-muted); overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: clip; /* D48 */ }
+.history-run-list { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; /* D48 */ }
+.history-run-column { position: absolute; inset-block: 0; inline-size: 64px; pointer-events: none; /* D48 */ }
+.history-position { position: absolute; inset-block-start: 41px; font-size: 10px; color: var(--ca-muted); /* D48 */ }
+.ca-tools .history-run-marker { position: absolute; pointer-events: auto; transform: translate(-50%, -50%); inline-size: 12px; block-size: 12px; min-inline-size: 12px; min-block-size: 12px; border: 2px solid var(--red); border-radius: 50%; background: var(--ca-field); padding: 0; color: var(--ca-field); font-size: 16px; line-height: 1; /* D48 */ }
+.ca-tools .history-context-anchor { inline-size: 29px; block-size: 29px; min-inline-size: 29px; min-block-size: 29px; background: var(--red); color: #38151b; font-size: 12px; /* D48 */ }
+.history-run-marker[aria-pressed="true"], .history-run-marker:focus-visible { outline: 3px solid var(--ca-focus); outline-offset: 4px; /* D48 */ }
+.history-run-marker[aria-disabled="true"] { opacity: .6; cursor: progress; /* D48 */ }
+/* D48: the Recorded run dialog (#209, D26). */
+.ca-tools dialog.recorded-run-dialog { box-sizing: border-box; inline-size: min(1100px, calc(100% - 32px)); max-inline-size: calc(100% - 32px); max-block-size: calc(100dvh - 32px); padding: 22px; border-radius: 7px; overscroll-behavior: contain; scrollbar-gutter: stable; overflow-wrap: anywhere; /* D48 */ }
+.recorded-run-dialog::backdrop { background: #0009; /* D48 */ }
+.recorded-run-header { display: flex; gap: 12px; align-items: center; justify-content: space-between; /* D48 */ }
+.recorded-run-header h2 { font-size: 20px; /* D48 */ }
+.recorded-run-body, .recorded-run-body > section { min-inline-size: 0; /* D48 */ }
+.recorded-run-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; font-size: 12px; /* D48 */ }
+.recorded-run-facts dt { color: var(--ca-muted); font-size: 10px; /* D48 */ }
+.recorded-run-facts dd { margin: 4px 0 0; white-space: pre-wrap; /* D48 */ }
+.recorded-run-anchors, .recorded-run-mode { display: flex; flex-wrap: wrap; gap: 8px; margin-block: 18px; /* D48 */ }
+.recorded-run-dialog [aria-pressed="true"] { outline: 2px solid var(--ca-focus); outline-offset: 2px; /* D48 */ }
+.recorded-run-dialog :where(.muted:not(.prompt-text), .recorded-output-status) { font-size: 11px; line-height: 1.65; /* D48 */ }
+/* D48: the current file's prompt blocks sit below the dialog's evidence line (elsewhere their mount point is `display: contents`). */
+.recorded-run-dialog .prompt-inspector { margin-block-start: 22px; /* D48 */ }
+.recorded-provenance { border-block-start: 1px solid var(--ca-line); padding-block-start: 12px; margin-block: 18px; /* D48 */ }
+.recorded-outputs { margin-block: 24px; /* D48 */ }
+.recorded-receipts { list-style: decimal; padding-inline-start: 24px; /* D48 */ }
+.recorded-receipt { border: 1px solid var(--ca-line); border-radius: 5px; padding: 12px; margin-block: 10px; font-size: 12px; /* D48 */ }
+.recorded-receipt > div { margin-block-end: 10px; /* D48 */ }
+.recorded-receipt[aria-current="true"] { outline: 2px solid var(--ca-focus); outline-offset: 2px; /* D48 */ }
+.recorded-current-file { border-block-start: 1px solid var(--ca-line); padding-block-start: 16px; /* D48 */ }
+/* D48: the frozen field provenance and the Archive confirmation show stored text in `pre.prompt-text`, not as the
+ concept's code `pre`. */
+pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; margin-block: 10px; background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; /* D48 */ }
+/* D48: the Archive negative confirmation's negative block (#235). */
+.prompt-block { border-block-start: 1px solid var(--ca-line); padding-block: 13px; min-inline-size: 0; /* D48 */ }
+.prompt-block-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; /* D48 */ }
+.prompt-block.prompt-negative h3 { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; margin: 0; color: var(--red); /* D48 */ }
+.prompt-empty { font-size: 10px; /* D48 */ }
+/* D48: the acknowledged prompt sender (prompt-sender.mjs) and its labelled pair send button. */
+.prompt-send-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; color: var(--ca-focus); /* D48 */ }
+.prompt-send-button svg { inline-size: 16px; block-size: 16px; flex-shrink: 0; /* D48 */ }
+.ca-tools dialog.prompt-sender { inline-size: min(760px, calc(100% - 32px)); max-inline-size: none; max-block-size: calc(100dvh - 32px); padding: 22px; border-color: var(--ca-line); border-radius: 12px; /* D48 */ }
+.ca-tools dialog.prompt-sender::backdrop { background: #000b; /* D48 */ }
+.prompt-sender-header, .prompt-sender-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; /* D48 */ }
+.prompt-sender-header h2 { font-size: 20px; /* D48 */ }
+.prompt-sender-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-block: 20px; /* D48 */ }
+.prompt-sender-options .ca-field { display: flex; flex-direction: column; align-items: stretch; gap: 6px; /* D48 */ }
+.prompt-sender select { inline-size: 100%; min-block-size: 38px; /* D48 */ }
+.prompt-sender-status { font-size: 13px; line-height: 1.6; /* D48 */ }
+.prompt-sender-status[role="alert"] { color: var(--ca-error-text); /* D48 */ }
+.prompt-sender-preview { margin-block: 20px; min-inline-size: 0; /* D48 */ }
+.prompt-sender-field { border: 1px solid var(--ca-line); border-radius: 7px; padding: 14px; margin-block: 12px; min-inline-size: 0; /* D48 */ }
+.prompt-sender-field :where(pre, textarea), .prompt-sender-composition { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; font-size: 12px; line-height: 1.65; max-inline-size: 100%; /* D48 */ }
+.prompt-sender-field textarea { inline-size: 100%; min-block-size: 100px; resize: vertical; /* D48 */ }
+.prompt-sender-composition { padding: 12px; background: var(--ca-field); border-radius: 6px; /* D48 */ }
+.prompt-sender-actions { justify-content: flex-end; margin-block-start: 18px; /* D48 */ }
+@media (max-width: 780px) {
+ .prompt-sender-options { grid-template-columns: minmax(0, 1fr); /* D48 */ }
+}
+@media (max-width: 700px) {
+ .ca-tools dialog.recorded-run-dialog { padding: 16px; /* D48 */ }
+ .recorded-run-facts { grid-template-columns: minmax(0, 1fr); /* D48 */ }
+}
+@media (pointer: coarse) {
+ /* D48: the recorded toolbar, its scope disclosure and the dialog's controls keep 44px targets (concept coarse buttons are 40px). */
+ .ca-tools .recorded-history :is(.history-toolbar button, .history-scope summary) { min-inline-size: 44px; min-block-size: 44px; }
+ .ca-tools .history-run-marker { inline-size: 44px; block-size: 44px; min-inline-size: 44px; min-block-size: 44px; border: 0; background: transparent; color: transparent; }
+ .ca-tools .history-run-marker::before { content: ""; position: absolute; inset: 16px; box-sizing: border-box; border: 2px solid var(--red); border-radius: 50%; background: var(--ca-field); }
+ .ca-tools .history-context-anchor::before { content: "−"; inset: 7.5px; inline-size: 29px; block-size: 29px; line-height: 25px; color: #38151b; background: var(--red); }
+ .ca-tools .recorded-run-dialog :where(button, summary) { min-inline-size: 44px; min-block-size: 44px; }
+}
+@media (forced-colors: active) {
+ .history-context-branch, .history-context-branch::before, .history-run-marker, .history-run-marker::before { border-color: CanvasText; }
+ .history-run-marker[aria-pressed="true"] { outline-color: Highlight; }
+}
diff --git a/web/workspace.html b/web/workspace.html
index 87f833e..0c8eba5 100644
--- a/web/workspace.html
+++ b/web/workspace.html
@@ -8,7 +8,6 @@
-