diff --git a/web/scripts/camo-theme.test.mjs b/web/scripts/camo-theme.test.mjs index 6521f02..208ba52 100644 --- a/web/scripts/camo-theme.test.mjs +++ b/web/scripts/camo-theme.test.mjs @@ -10,7 +10,7 @@ * * So the rule this checks is structural rather than visual: inside the * editor's section of the stylesheet, a colour is a custom property. The - * literals live in one token block at the top with a light-mode counterpart, + * literals live in one token block at the top, each one a light-dark() pair, * and every rule below reads them. Anything else is the bug above waiting to * happen again. * @@ -34,25 +34,25 @@ const end = css.indexOf("/* --- the sign-in dialog", start); assert.notEqual(end, -1, "the section after the camo editor moved"); const section = css.slice(start, end); -// The one place literals are allowed: the token block and its light-mode -// counterpart, which is the whole point of having them. +// The one place literals are allowed: the token block, which is the whole +// point of having them. const tokensEnd = section.indexOf(".camo-editor canvas"); assert.notEqual(tokensEnd, -1, "the editor token block moved"); const rules = section.slice(tokensEnd); test("the editor defines its surfaces in both schemes", () => { const tokens = section.slice(0, tokensEnd); - const names = [...tokens.matchAll(/(--camo-[a-z-]+):/g)].map((m) => m[1]); - assert.ok(names.length >= 5, "expected a block of --camo-* tokens"); + const declarations = [...tokens.matchAll(/(--camo-[a-z-]+):\s*([^;]+);/g)]; + assert.ok(declarations.length >= 5, "expected a block of --camo-* tokens"); - const light = tokens.slice(tokens.indexOf("prefers-color-scheme: light")); - for (const name of names) { + for (const [, name, value] of declarations) { // --camo-chip-ink is deliberately one value in both schemes: it forwards // to --ink, which the site already flips. if (name === "--camo-chip-ink") continue; - assert.ok( - light.includes(`${name}:`), - `${name} has no light-mode value, so it keeps its dark one on a light card`, + assert.match( + value.trim(), + /^light-dark\(.+,.+\)$/, + `${name} is not a light-dark() pair, so it keeps one value in both schemes`, ); } }); diff --git a/web/src/styles.css b/web/src/styles.css index 706a9d4..8824add 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1,19 +1,28 @@ /* One stylesheet. Dark by default, because the game is. */ :root { + /* Dark unless the browser asks for light, and unless the account menu's + theme control has been used — that writes data-theme on this element and + the two rules below narrow color-scheme to one value. + + Every colour that depends on the scheme is a light-dark() pair here + rather than a second block of tokens under a media query. A media query + cannot see an explicit choice that disagrees with the browser's, and a + token that appears in one block and not the other keeps its dark value on + a light page — which is the bug the camo editor already had once. */ color-scheme: dark light; - --bg: #0b1017; - --panel: #131b26; - --line: #22303f; - --ink: #e6edf5; - --muted: #8ea0b5; - --accent: #4c8dff; - --danger: #ff8a7a; + --bg: light-dark(#f4f6fa, #0b1017); + --panel: light-dark(#ffffff, #131b26); + --line: light-dark(#d8dfea, #22303f); + --ink: light-dark(#15202b, #e6edf5); + --muted: light-dark(#5a6b80, #8ea0b5); + --accent: light-dark(#1f5fd0, #4c8dff); + --danger: light-dark(#b3261e, #ff8a7a); /* The insignia's teal. Reserved for the masthead, so the brand mark and the site chrome are visibly the same object. */ - --brand: #4fc1d6; + --brand: light-dark(#0e7a8c, #4fc1d6); --radius: 6px; --measure: 34rem; @@ -25,17 +34,16 @@ --shell: var(--measure); } -@media (prefers-color-scheme: light) { - :root { - --bg: #f4f6fa; - --panel: #ffffff; - --line: #d8dfea; - --ink: #15202b; - --muted: #5a6b80; - --accent: #1f5fd0; - --danger: #b3261e; - --brand: #0e7a8c; - } +/* What the theme control does, and all it does. Narrowing color-scheme is the + whole mechanism: every light-dark() on the page resolves against it, form + controls and scrollbars included. theme.ts sets the attribute, and the + inline script in Base.astro sets it again before the first paint. */ +:root[data-theme="light"] { + color-scheme: light; +} + +:root[data-theme="dark"] { + color-scheme: dark; } * { @@ -1307,26 +1315,15 @@ footer .debug { alternative was thirty literal hexes, which is how the roll buttons came to be var(--ink) on a fixed near-black - invisible in light mode. */ .camo-editor { - --camo-well: #0e1116; - --camo-chip: #161b23; - --camo-chip-on: #1d2836; - --camo-edge: #262d38; - --camo-edge-strong: #2b3648; - --camo-pick: #4f7fc4; + --camo-well: light-dark(#e8edf4, #0e1116); + --camo-chip: light-dark(#f2f5fa, #161b23); + --camo-chip-on: light-dark(#dbe7fb, #1d2836); + --camo-edge: light-dark(#ccd6e4, #262d38); + --camo-edge-strong: light-dark(#b6c4d8, #2b3648); + --camo-pick: light-dark(#1f5fd0, #4f7fc4); --camo-chip-ink: var(--ink); } -@media (prefers-color-scheme: light) { - .camo-editor { - --camo-well: #e8edf4; - --camo-chip: #f2f5fa; - --camo-chip-on: #dbe7fb; - --camo-edge: #ccd6e4; - --camo-edge-strong: #b6c4d8; - --camo-pick: #1f5fd0; - } -} - .camo-editor canvas { image-rendering: pixelated; display: block;