diff --git a/client/index.html b/client/index.html
index 7ea758aacc..bb05828ff7 100644
--- a/client/index.html
+++ b/client/index.html
@@ -32,15 +32,35 @@
const loadingContainerStyle = document.createElement('style');
let backgroundColor;
- if (theme === 'dark') {
+ if (theme === 'high-contrast-dark') {
+ backgroundColor = '#000000';
+ } else if (theme === 'high-contrast-light') {
+ backgroundColor = '#ffffff';
+ } else if (theme === 'dark') {
backgroundColor = '#0d0d0d';
} else if (theme === 'light') {
backgroundColor = '#ffffff';
- } else if (theme === 'system') {
- const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
- backgroundColor = prefersDarkScheme ? '#0d0d0d' : '#ffffff';
} else {
- backgroundColor = '#ffffff';
+ /* `system`, and anything unset or unrecognised, which ThemeProvider also
+ * resolves as `system`. The OS decides the scheme and the contrast, and
+ * a contrast request moves the dark canvas to pure black, so keying off
+ * the scheme alone would flash #0d0d0d behind the black palette.
+ * The three contrast queries mirror ThemeProvider's `CONTRAST_QUERIES`:
+ * a Windows Contrast Theme reports `forced-colors: active` with
+ * `prefers-contrast: custom`, never `more`. */
+ const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ const prefersMoreContrast = [
+ '(prefers-contrast: more)',
+ '(prefers-contrast: custom)',
+ '(forced-colors: active)',
+ ].some(function (query) {
+ return window.matchMedia(query).matches;
+ });
+ backgroundColor = prefersDarkScheme
+ ? prefersMoreContrast
+ ? '#000000'
+ : '#0d0d0d'
+ : '#ffffff';
}
loadingContainerStyle.innerHTML = `
diff --git a/client/src/__tests__/loadingCanvas.spec.ts b/client/src/__tests__/loadingCanvas.spec.ts
new file mode 100644
index 0000000000..9d7369e6ad
--- /dev/null
+++ b/client/src/__tests__/loadingCanvas.spec.ts
@@ -0,0 +1,76 @@
+import { join } from 'node:path';
+import { readFileSync } from 'node:fs';
+
+/** The pre-React canvas is painted by an inline bootstrap in `index.html`, which
+ * no bundle imports, so the only way to hold it to the resolved palette is to
+ * run the script the document actually ships. */
+const bootstrap = (() => {
+ const html = readFileSync(join(__dirname, '..', '..', 'index.html'), 'utf8');
+ const script = [...html.matchAll(/