diff --git a/context/code/clients.md b/context/code/clients.md
index 96080d7c..ac7c8ee5 100644
--- a/context/code/clients.md
+++ b/context/code/clients.md
@@ -1726,6 +1726,18 @@ stylesheet rather than the page working out which row is selected. And
`.notice-toggle`'s glyph is chosen by `[open]`, which is the one piece of state
on this page that genuinely is the browser's and that nothing else knows.
+**One rule in it is load-bearing and is the stylesheet's own failure mode.**
+`td.error` is the only place on this page where colour carries *meaning*
+rather than emphasis — a circular reference or a `mixed` aggregate reading as
+an ordinary number is worse than it reading as nothing. It used to coexist with
+the dimming of unsettable cells because they set **different** properties
+(`color` against `opacity`); once both are `color` at the same specificity,
+only source order separates them, and nothing in any language fails when the
+order is wrong. So the dimming sits **above** the semantic colours, and a
+selection is a ring and a tint and never a colour. That is the class of defect
+a stylesheet has and a program does not, and it is why the one assertion this
+file makes about colour is about which rule wins rather than about a value.
+
**The harness cannot see any of this**, and that is the honest scope. Playwright
asserts what a browser can answer: that the notice starts collapsed and
remembers, that it opens from the keyboard, and that the minibuffer is inside
diff --git a/context/test-evidence/clients.md b/context/test-evidence/clients.md
index 2337db63..83a74129 100644
--- a/context/test-evidence/clients.md
+++ b/context/test-evidence/clients.md
@@ -3257,3 +3257,37 @@ body's before and is now strictly larger.
The precondition matters: `#mode` must read `INSERT` before anything is
measured, because `getBoundingClientRect` on a hidden element returns zeroes
and every comparison below would then be about the origin.
+
+### "an error cell stays an error cell when it is also dimmed or selected"
+
+Found reviewing the styling change that introduced it, before it was pushed.
+`td.error` and `td:not(.editable)` used to set **different** properties —
+`color: var(--error)` and `opacity: 0.7` — so both applied and a dimmed error
+cell was a dimmed red. The restyling made the dimming a `color` too, at the
+same specificity, and put it **after** `td.error` in the file, so an error in
+an aggregate cell rendered plain grey: measured `rgb(107, 103, 95)` where the
+error colour is `rgb(168, 50, 45)`. `td.selected` and `td.typing` had the same
+effect by setting `color: var(--fg)`, which is how a *selected* error lost its
+colour as well.
+
+That is the whole class of defect a stylesheet has and a program does not:
+two rules that were independent become one property, and nothing in any
+language fails. `#grid td.error` is the only place in this page where colour
+carries meaning rather than emphasis, and it is the one a reader takes at face
+value — a circular reference or a `mixed` aggregate reading as an ordinary
+number is worse than it reading as nothing.
+
+The fix is ordering plus a rule: the dimming goes **above** the semantic
+colours, and a selection is a ring and a tint and never a colour. The test is
+aimed at the stylesheet rather than at a document, deliberately — the question
+is which rule wins, and a fixture per combination (a dimmed error, a selected
+error, an error being typed over) would be three documents testing three
+documents. It appends a `
` carrying each combination to the real page's
+real table, reads `getComputedStyle`, and compares against two controls taken
+the same way: a plain unsettable cell (what it must not be) and an error the
+dimming cannot reach (what it must be). Reading absolute colours instead would
+pin the palette, which is not the property.
+
+Inject the fault by moving the `td:not(.editable)` block back below
+`td.error`, or by restoring either `color: var(--fg)`; each makes exactly the
+corresponding assertion fail.
diff --git a/harness/tests/contract.spec.js b/harness/tests/contract.spec.js
index 95f3aa6e..b679cfac 100644
--- a/harness/tests/contract.spec.js
+++ b/harness/tests/contract.spec.js
@@ -505,3 +505,48 @@ test.describe("the shell puts the application first", () => {
await expect(page.locator("#minibuffer")).toBeHidden();
});
});
+
+// The stylesheet's own cascade, which no other test here can reach: an
+// error is a colour now, where it used to be a colour AND an opacity, so
+// the order of two equally-specific rules decides it. See
+// context/test-evidence/clients.md.
+test.describe("the grid's semantic colours survive the cascade", () => {
+ test("an error cell stays an error cell when it is also dimmed or selected", async ({
+ page,
+ }) => {
+ await page.goto("/app/index.html?doc=error-colour.improv");
+ await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
+
+ // The question is about the STYLESHEET, so the combinations are put to it
+ // directly: the document that produces each of them is a separate matter,
+ // and a fixture per combination would test the fixture.
+ const colours = await page.evaluate(() => {
+ const row = document.querySelector("#grid tbody tr");
+ const read = (classes) => {
+ const td = document.createElement("td");
+ td.className = classes;
+ td.textContent = "#err";
+ row.appendChild(td);
+ const colour = getComputedStyle(td).color;
+ td.remove();
+ return colour;
+ };
+ return {
+ // An aggregate error, which is the common case: `editable` is false
+ // for every cell that is not a stored record.
+ dimmed: read("error"),
+ selected: read("error selected"),
+ typing: read("error typing"),
+ // What "dimmed" must NOT be, read off a plain unsettable cell.
+ dim: read(""),
+ // …and what it must be, read off an error the dimming cannot reach.
+ error: read("error editable"),
+ };
+ });
+
+ expect(colours.error).not.toBe(colours.dim);
+ expect(colours.dimmed).toBe(colours.error);
+ expect(colours.selected).toBe(colours.error);
+ expect(colours.typing).toBe(colours.error);
+ });
+});
diff --git a/web/style.css b/web/style.css
index 98f55683..ce0e8b9d 100644
--- a/web/style.css
+++ b/web/style.css
@@ -203,6 +203,10 @@ header h1 {
}
.notice-body {
+ /* Expanded, this must not take the whole window: `main` is free to shrink
+ to nothing, so a short viewport would lose the grid entirely. */
+ max-height: 40vh;
+ overflow: auto;
padding: 0 0.9rem 0.6rem;
max-width: 100ch;
color: var(--dim);
@@ -391,6 +395,14 @@ td.numeric {
text-align: right;
}
+/* Everything else on the grid is an aggregate, and dimming it is what makes
+ the settable cells read as the surface you can act on. */
+td:not(.editable) {
+ color: var(--dim);
+}
+
+/* …and the semantic colours come AFTER it, because they are now the same
+ property at the same specificity and only the order separates them. */
td.error {
color: var(--error);
font-weight: 600;
@@ -405,12 +417,6 @@ td.editable:hover {
background: color-mix(in srgb, var(--accent) 8%, transparent);
}
-/* Everything else on the grid is an aggregate, and dimming it is what makes
- the settable cells read as the surface you can act on. */
-td:not(.editable) {
- color: var(--dim);
-}
-
/* The selected cell, and the row header that names it. `:has()` is what lets
the header light up without the page deriving which row is selected — the
selection is already on the cell, decided in Rust. */
@@ -418,7 +424,6 @@ td.selected {
background: var(--accent-soft);
outline: 2px solid var(--accent);
outline-offset: -2px;
- color: var(--fg);
font-weight: 700;
}
@@ -434,7 +439,6 @@ td.typing {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--accent-soft);
- color: var(--fg);
font-weight: 700;
}
|