diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0372f2e..f1b3512 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@ at boot. Entries start at 0.3.0.
## Unreleased
+### The tests fail on a stylesheet feature the browser floor lacks
+
+Every stylesheet in the repo, the examples' included, is checked against Chromium 87, Safari 15 and
+Firefox 115 using MDN's compat data; a feature one of them lacks passes only inside an `@supports`
+that names it. The core stylesheets now guard each such feature, which those engines skip instead
+of misreading. Pages render as before in current browsers.
+
### Every release reaches Docker Hub, and the Hub page has a short description
Releases 0.4.1 to 0.4.20 were tagged in git but never published to Docker Hub or as semver tags in
diff --git a/README.md b/README.md
index c05f5e6..2c1b130 100644
--- a/README.md
+++ b/README.md
@@ -1059,9 +1059,11 @@ Chromium 87, Safari 15 and Firefox 115, the engines in years-old TVs, kiosks and
features [Baseline Widely available](https://web-platform-dx.github.io/baseline/) (in every
major engine for 30 months) may shape how a page looks; anything newer, like popovers or anchor
positioning, only enhances. A feature the floor engines lack, such as `:has()`, may
-decorate a page but never decide whether something shows or can be read. A colour is hex, `rgb()` or
-`hsl()`, never `oklch()`: a custom property holding it passes unchecked, and the `var()` reading it
-then renders transparent.
+decorate a page but never decide whether something shows or can be read. The tests hold every
+stylesheet in `public/` and `examples/` to this floor, against MDN's compat data: a feature a floor
+engine lacks fails them unless it sits inside an `@supports` whose condition names it
+(`src/ui/stylesheet-floor.ts` lists what it checks). A plugin stylesheet mounted from outside the
+repo is not checked.
**A field whose options depend on another field is asked for in steps, one GET form each**, so every
choice is in the URL, and nothing typed is lost to one because nothing is typed until they are made.
diff --git a/docs/reading-cost.md b/docs/reading-cost.md
index e8ce3f5..853a87c 100644
--- a/docs/reading-cost.md
+++ b/docs/reading-cost.md
@@ -209,3 +209,20 @@ Read most (opens and searches):
- `examples/plugins/admin/i18n/en-US.ts` 2
- `CHANGELOG.md` 1
- `examples/plugins/admin/admin-groups.test.ts` 1
+
+## Reading cost, chunk c90b25a50479, finished 2026-10-31T10:40:23Z
+
+90935 new input tokens before the first commit.
+
+Read most (opens and searches):
+
+- `README.md` 9
+- `todo.md` 5
+- `AGENTS.md` 3
+- `CHANGELOG.md` 1
+- `package.json` 1
+- `registry-cleanup/cleanup.ts` 1
+- `release-tooling/dockerhub-overview.md.tmpl` 1
+- `release-tooling/dockerhub-overview.test.ts` 1
+- `release-tooling/dockerhub-overview.ts` 1
+- `src/ci-gate.test.ts` 1
diff --git a/package-lock.json b/package-lock.json
index 4ecde74..ecfb573 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"postgres": "3.4.9"
},
"devDependencies": {
+ "@mdn/browser-compat-data": "8.1.7",
"@types/ejs": "3.1.5",
"@types/node": "24.19.2",
"acorn": "8.19.0",
@@ -30,6 +31,13 @@
"node": ">=18"
}
},
+ "node_modules/@mdn/browser-compat-data": {
+ "version": "8.1.7",
+ "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-8.1.7.tgz",
+ "integrity": "sha512-TJ+rX530bFoE8678PuH2gjCSzwpBcaLlcElRhnnxFdkLHHT1clUAxtjImPFZDAlWQ005px9xaJ4Nd+aQp/rylQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/@types/ejs": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz",
diff --git a/package.json b/package.json
index 7771026..20213c4 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"postgres": "3.4.9"
},
"devDependencies": {
+ "@mdn/browser-compat-data": "8.1.7",
"@types/ejs": "3.1.5",
"@types/node": "24.19.2",
"acorn": "8.19.0",
diff --git a/public/css/auth.css b/public/css/auth.css
index b8fbe20..45fda1d 100644
--- a/public/css/auth.css
+++ b/public/css/auth.css
@@ -1,9 +1,11 @@
.auth-stage {
- min-height: 100dvh;
display: grid; place-items: center;
padding: 40px 20px 96px;
background: var(--bg);
}
+@supports (min-height: 100dvh) {
+ .auth-stage { min-height: 100dvh; }
+}
.auth {
width: 100%; max-width: 408px;
display: flex; flex-direction: column; gap: 22px;
@@ -103,8 +105,10 @@
.input:focus { outline: none; border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-bg); }
/* gentle validation feedback only after the user has interacted */
-.input:user-invalid { border-color: var(--neg); }
-.input:user-invalid:focus { box-shadow: 0 0 0 2px var(--neg-bg); }
+@supports selector(:user-invalid) {
+ .input:user-invalid { border-color: var(--neg); }
+ .input:user-invalid:focus { box-shadow: 0 0 0 2px var(--neg-bg); }
+}
.field-hint { font-size: var(--fz-xs); color: var(--text-faint); }
diff --git a/public/css/styles.css b/public/css/styles.css
index 92add70..7a14441 100644
--- a/public/css/styles.css
+++ b/public/css/styles.css
@@ -71,9 +71,24 @@
line-height: 1.45;
}
-/* shared DARK mapping, applied to Auto-via-OS and forced-dark */
-@media (prefers-color-scheme: dark) {
- :root:not(:has(#theme-light:checked)):not(:has(#theme-dark:checked)) {
+@supports selector(:has(a)) {
+ /* shared DARK mapping, applied to Auto-via-OS and forced-dark */
+ @media (prefers-color-scheme: dark) {
+ :root:not(:has(#theme-light:checked)):not(:has(#theme-dark:checked)) {
+ --bg:var(--d-bg); --surface:var(--d-surface); --surface-2:var(--d-surface-2); --surface-3:var(--d-surface-3);
+ --border:var(--d-border); --border-2:var(--d-border-2);
+ --text:var(--d-text); --text-muted:var(--d-text-muted); --text-faint:var(--d-text-faint);
+ --accent:var(--d-accent); --accent-bg:var(--d-accent-bg); --accent-bd:var(--d-accent-bd); --focus:var(--d-focus); --on-accent:var(--d-on-accent);
+ --pos:var(--d-pos); --pos-bg:var(--d-pos-bg); --pos-bd:var(--d-pos-bd);
+ --neg:var(--d-neg); --neg-bg:var(--d-neg-bg); --neg-bd:var(--d-neg-bd);
+ --warn:var(--d-warn); --warn-bg:var(--d-warn-bg); --warn-bd:var(--d-warn-bd);
+ --info:var(--d-info); --info-bg:var(--d-info-bg); --info-bd:var(--d-info-bd);
+ color-scheme: dark;
+ }
+ }
+
+ /* forced DARK (overrides everything) */
+ html:has(#theme-dark:checked) {
--bg:var(--d-bg); --surface:var(--d-surface); --surface-2:var(--d-surface-2); --surface-3:var(--d-surface-3);
--border:var(--d-border); --border-2:var(--d-border-2);
--text:var(--d-text); --text-muted:var(--d-text-muted); --text-faint:var(--d-text-faint);
@@ -84,32 +99,19 @@
--info:var(--d-info); --info-bg:var(--d-info-bg); --info-bd:var(--d-info-bd);
color-scheme: dark;
}
-}
-/* forced DARK (overrides everything) */
-html:has(#theme-dark:checked) {
- --bg:var(--d-bg); --surface:var(--d-surface); --surface-2:var(--d-surface-2); --surface-3:var(--d-surface-3);
- --border:var(--d-border); --border-2:var(--d-border-2);
- --text:var(--d-text); --text-muted:var(--d-text-muted); --text-faint:var(--d-text-faint);
- --accent:var(--d-accent); --accent-bg:var(--d-accent-bg); --accent-bd:var(--d-accent-bd); --focus:var(--d-focus); --on-accent:var(--d-on-accent);
- --pos:var(--d-pos); --pos-bg:var(--d-pos-bg); --pos-bd:var(--d-pos-bd);
- --neg:var(--d-neg); --neg-bg:var(--d-neg-bg); --neg-bd:var(--d-neg-bd);
- --warn:var(--d-warn); --warn-bg:var(--d-warn-bg); --warn-bd:var(--d-warn-bd);
- --info:var(--d-info); --info-bg:var(--d-info-bg); --info-bd:var(--d-info-bd);
- color-scheme: dark;
-}
-
-/* forced LIGHT (overrides the OS-dark media query) */
-html:has(#theme-light:checked) {
- --bg:var(--l-bg); --surface:var(--l-surface); --surface-2:var(--l-surface-2); --surface-3:var(--l-surface-3);
- --border:var(--l-border); --border-2:var(--l-border-2);
- --text:var(--l-text); --text-muted:var(--l-text-muted); --text-faint:var(--l-text-faint);
- --accent:var(--l-accent); --accent-bg:var(--l-accent-bg); --accent-bd:var(--l-accent-bd); --focus:var(--l-focus); --on-accent:var(--l-on-accent);
- --pos:var(--l-pos); --pos-bg:var(--l-pos-bg); --pos-bd:var(--l-pos-bd);
- --neg:var(--l-neg); --neg-bg:var(--l-neg-bg); --neg-bd:var(--l-neg-bd);
- --warn:var(--l-warn); --warn-bg:var(--l-warn-bg); --warn-bd:var(--l-warn-bd);
- --info:var(--l-info); --info-bg:var(--l-info-bg); --info-bd:var(--l-info-bd);
- color-scheme: light;
+ /* forced LIGHT (overrides the OS-dark media query) */
+ html:has(#theme-light:checked) {
+ --bg:var(--l-bg); --surface:var(--l-surface); --surface-2:var(--l-surface-2); --surface-3:var(--l-surface-3);
+ --border:var(--l-border); --border-2:var(--l-border-2);
+ --text:var(--l-text); --text-muted:var(--l-text-muted); --text-faint:var(--l-text-faint);
+ --accent:var(--l-accent); --accent-bg:var(--l-accent-bg); --accent-bd:var(--l-accent-bd); --focus:var(--l-focus); --on-accent:var(--l-on-accent);
+ --pos:var(--l-pos); --pos-bg:var(--l-pos-bg); --pos-bd:var(--l-pos-bd);
+ --neg:var(--l-neg); --neg-bg:var(--l-neg-bg); --neg-bd:var(--l-neg-bd);
+ --warn:var(--l-warn); --warn-bg:var(--l-warn-bg); --warn-bd:var(--l-warn-bd);
+ --info:var(--l-info); --info-bg:var(--l-info-bg); --info-bd:var(--l-info-bd);
+ color-scheme: light;
+ }
}
/* ---------- 2. RESET ---------------------------------------- */
@@ -122,10 +124,12 @@ a { color: inherit; text-decoration: none; }
summary::-webkit-details-marker { display: none; }
summary { list-style: none; cursor: pointer; }
-:focus-visible {
- outline: 2px solid var(--focus);
- outline-offset: 1px;
- border-radius: 3px;
+@supports selector(:focus-visible) {
+ :focus-visible {
+ outline: 2px solid var(--focus);
+ outline-offset: 1px;
+ border-radius: 3px;
+ }
}
@media (prefers-reduced-motion: no-preference) {
.sidebar, .scrim, summary, .nav-item, .btn, .chip, .profile, .kebab { transition: .15s ease; }
@@ -153,7 +157,9 @@ summary { list-style: none; cursor: pointer; }
.app {
display: grid;
grid-template-columns: var(--nav-w) minmax(0, 1fr);
- min-height: 100dvh;
+}
+@supports (min-height: 100dvh) {
+ .app { min-height: 100dvh; }
}
/* ---------- 4. SIDEBAR -------------------------------------- */
@@ -219,7 +225,10 @@ summary { list-style: none; cursor: pointer; }
min-height: 30px; padding: 4px 8px;
border-radius: var(--radius);
color: var(--text-muted); font-size: var(--fz);
- position: relative; user-select: none;
+ position: relative;
+}
+@supports (user-select: none) {
+ .nav-self { user-select: none; }
}
a.nav-self:hover { background: var(--surface-2); color: var(--text); }
.nav-self .ico { color: var(--text-faint); }
@@ -277,12 +286,14 @@ span.nav-self { cursor: default; } /* static / non-clickable */
}
.theme-switch label:hover { color: var(--text); }
.theme-switch input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
-.theme-switch label:has(input:checked) {
- background: var(--surface); color: var(--text);
- box-shadow: 0 1px 2px rgba(0,0,0,.06); font-weight: 550;
-}
-.theme-switch label:has(input:focus-visible) {
- outline: 2px solid var(--focus); outline-offset: 1px;
+@supports selector(:has(:focus-visible)) {
+ .theme-switch label:has(input:checked) {
+ background: var(--surface); color: var(--text);
+ box-shadow: 0 1px 2px rgba(0,0,0,.06); font-weight: 550;
+ }
+ .theme-switch label:has(input:focus-visible) {
+ outline: 2px solid var(--focus); outline-offset: 1px;
+ }
}
/* Without :has() every branch is open and every theme rule is inert: neither control does anything.
@@ -414,7 +425,7 @@ span.nav-self { cursor: default; } /* static / non-clickable */
position: relative; display: inline-flex; align-items: center;
}
.select select {
- appearance: none; height: 30px; padding: 0 26px 0 9px;
+ height: 30px; padding: 0 26px 0 9px;
background: var(--surface); color: var(--text);
border: 1px solid var(--border-2); border-radius: var(--radius);
font: inherit; font-size: var(--fz-sm); cursor: pointer;
@@ -424,8 +435,13 @@ span.nav-self { cursor: default; } /* static / non-clickable */
width: 7px; height: 7px; border-right: 1.5px solid var(--text-faint);
border-bottom: 1.5px solid var(--text-faint); transform: translateY(-2px) rotate(45deg);
}
-.select select:focus-visible { border-color: var(--accent); outline: none;
- box-shadow: 0 0 0 2px var(--accent-bg); }
+@supports (appearance: none) {
+ .select select { appearance: none; }
+}
+@supports selector(:focus-visible) {
+ .select select:focus-visible { border-color: var(--accent); outline: none;
+ box-shadow: 0 0 0 2px var(--accent-bg); }
+}
/* segmented toggle (radios) */
.segmented {
@@ -440,11 +456,13 @@ span.nav-self { cursor: default; } /* static / non-clickable */
}
.segmented label:hover { color: var(--text); }
.segmented input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
-.segmented label:has(input:checked) {
- background: var(--surface); color: var(--text); font-weight: 550;
- box-shadow: 0 1px 2px rgba(0,0,0,.06);
+@supports selector(:has(:focus-visible)) {
+ .segmented label:has(input:checked) {
+ background: var(--surface); color: var(--text); font-weight: 550;
+ box-shadow: 0 1px 2px rgba(0,0,0,.06);
+ }
+ .segmented label:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 1px; }
}
-.segmented label:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 1px; }
.seg-count { font-size: var(--fz-xs); color: var(--text-faint); }
/* chips (multi-select checkboxes) */
@@ -460,12 +478,14 @@ span.nav-self { cursor: default; } /* static / non-clickable */
.chip input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.chip .chip-dot { width: 7px; height: 7px; border-radius: 50%;
background: var(--border-2); }
-.chip:has(input:checked) {
- background: var(--accent-bg); border-color: var(--accent-bd);
- color: var(--text); font-weight: 550;
+@supports selector(:has(:focus-visible)) {
+ .chip:has(input:checked) {
+ background: var(--accent-bg); border-color: var(--accent-bd);
+ color: var(--text); font-weight: 550;
+ }
+ .chip:has(input:checked) .chip-dot { background: var(--accent); }
+ .chip:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 1px; }
}
-.chip:has(input:checked) .chip-dot { background: var(--accent); }
-.chip:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 1px; }
/* date range */
.daterange {
@@ -486,8 +506,10 @@ span.nav-self { cursor: default; } /* static / non-clickable */
display: inline-flex; align-items: center; gap: 7px;
font-size: var(--fz-sm); color: var(--text-muted); cursor: pointer;
}
-.check input, .radio input { width: 15px; height: 15px; accent-color: var(--accent);
- margin: 0; cursor: pointer; }
+.check input, .radio input { width: 15px; height: 15px; margin: 0; cursor: pointer; }
+@supports (accent-color: auto) {
+ .check input, .radio input { accent-color: var(--accent); }
+}
.check:hover, .radio:hover { color: var(--text); }
/* A stacked group of .check rows in a