diff --git a/README.md b/README.md index 9cc47d0..7afbaab 100644 --- a/README.md +++ b/README.md @@ -1403,9 +1403,13 @@ Things I chose while building, with the reasoning: grass, glossy glass with the classic horizon gloss, aqua buttons) and the scene (the glacier blobs become clouds, the orbs soap bubbles) — so the light/dark toggles (header, rail, chat header) hide while it is on and the picker says so. The pre-paint script applies the class - before the first paint, the PWA status bar follows it (#3fa3e0), and its muted/frost text is - darker than Light's because those labels sit directly on the sky. Covered by the axe and - pixel-contrast scans in all three themes. + before the first paint and the PWA status bar follows it (#2fa7e6). The scene is an underwater + one: sun shafts, caustic patches, reeds on the left, a city silhouette on the right and ten + rising bubbles (`.ice-bubble` — `display: none` outside aero, so they cost nothing in the other + themes, and they sit still under reduced motion). Its glass is a pale blue at 0.8 opacity: + transparent enough to read as glass, opaque enough that the dark navy text clears 4.5:1 over the + water — and the phone tab bar mixes the sheet glass rather than the page, which in aero *is* the + water. Covered by the axe and pixel-contrast scans in all three themes. 62. **Accessibility: axe is clean everywhere, and contrast is measured on real pixels.** axe cannot judge text on blurred glass — it reports those nodes as "incomplete" — so `tests/contrast.mjs` @@ -1584,8 +1588,9 @@ Freezmo ships its own plain-language documents, in both app languages, rendered | `/cookies` | No cookies at all (verified: no `Set-Cookie`, no `document.cookie`), what the strictly-necessary local storage holds, no tracking, and how to clear it. | A cookie notice (`components/CookieConsent.tsx`) tells every new browser the same thing once and -remembers the dismissal (`freezmo.consent.v1`). It sits below the immersive feeds in the stacking -order, so it can never cover a study session, and above the phone tab bar. Settings links all three +remembers the dismissal (`freezmo.consent.v1`). It is floating glass — blurred backdrop, rim and +shadow — so it reads as a notice above the page rather than part of it. It sits below the immersive +feeds in the stacking order, so it can never cover a study session, and above the phone tab bar. Settings links all three documents. The documents describe what the code actually does — keep them in step with it. ## License diff --git a/app/globals.css b/app/globals.css index 5b95a1a..afa5051 100644 --- a/app/globals.css +++ b/app/globals.css @@ -196,93 +196,138 @@ --ice-grain-opacity: 0.045; } -/* Frutiger Aero — the third theme (Settings → Theme). A bright sky with - clouds and a strip of grass, glossy glass with the classic horizon gloss, - aqua accents. Token-driven like light/dark; the light/dark toggles hide - while it is on (components/ThemeToggle.tsx). */ +/* Frutiger Aero — the third theme (Settings → Theme). An underwater scene: + sunlight shafts, caustic patches, reeds on the left, a city silhouette on + the right and bubbles rising through it; glass is a pale blue with the + classic horizon gloss. Token-driven like light/dark; the light/dark toggles + hide while it is on (components/ThemeToggle.tsx). */ .aero { color-scheme: light; - --page: #bfe4fb; - --sidebar: #e3f4ff; + --page: #1f86c9; + --sidebar: #d6efff; --surface: #ffffff; --surface-raised: #ffffff; --surface-strong: rgb(255 255 255 / 0.72); --card-hover-bg: rgb(255 255 255 / 0.9); - --surface-sunken: rgb(255 255 255 / 0.55); - --hairline: rgb(255 255 255 / 0.8); + --surface-sunken: rgb(255 255 255 / 0.5); + --hairline: rgb(255 255 255 / 0.75); --card-border: var(--glass-border); - --sidebar-divider: rgb(255 255 255 / 0.7); - --hairline-soft: rgb(255 255 255 / 0.6); + --sidebar-divider: rgb(255 255 255 / 0.65); + --hairline-soft: rgb(255 255 255 / 0.55); --glass-tint: rgb(255 255 255 / 0.6); - --text: #05203a; /* 12.6:1 on the aero glass */ - --text-muted: #1b3a54; /* 8.4:1 */ - --text-subtle: #16324a; /* 7.6:1 on glass, 5.4:1 on the sky */ + --text: #05203a; /* 10.9:1 on the aero glass */ + --text-muted: #0a2137; /* 11.5:1 on glass, 5.0:1 on the water */ + --text-subtle: #061726; /* 13.6:1 on glass, 5.5:1 on the water */ /* A few components use the frost scale directly (hero descriptions, the - wizard's step help): darker here, because they sit on the sky. */ - --color-frost-600: #1d3a55; - --color-frost-700: #16324a; - --hover: rgb(255 255 255 / 0.5); - --focus: #0284c7; + wizard's step help): darker here, because they sit on the water. */ + --color-frost-600: #16324a; + --color-frost-700: #102a41; + --hover: rgb(255 255 255 / 0.45); + --focus: #0369a1; - --glass-bg: rgb(255 255 255 / 0.62); + /* Pale blue glass, like the mock: bright enough for dark navy text. */ + --glass-bg: rgb(228 246 255 / 0.8); --glass-border: rgb(255 255 255 / 0.9); --glass-highlight: rgb(255 255 255 / 0.98); - --glass-shadow: 0 18px 44px -20px rgb(2 90 150 / 0.5); - --glass-solid-bg: rgb(255 255 255 / 0.82); - --glass-sheet-bg: rgb(255 255 255 / 0.9); - --glass-fallback: rgb(238 248 255 / 0.97); + --glass-shadow: 0 18px 44px -20px rgb(2 62 110 / 0.55); + --glass-solid-bg: rgb(232 248 255 / 0.9); + --glass-sheet-bg: rgb(232 246 255 / 0.94); + --glass-fallback: rgb(226 242 253 / 0.97); --nav-active-bg: rgb(255 255 255 / 0.85); - --nav-active-border: rgb(14 165 233 / 0.65); - --gate-card: rgb(255 255 255 / 0.66); + --nav-active-border: rgb(14 165 233 / 0.7); + --gate-card: rgb(226 244 255 / 0.7); --gate-callout-bg: rgb(255 255 255 / 0.6); - --gate-callout-border: rgb(56 189 248 / 0.4); - --field: rgb(255 255 255 / 0.75); + --gate-callout-border: rgb(56 189 248 / 0.45); + --field: rgb(255 255 255 / 0.78); --field-border: rgb(255 255 255 / 0.95); - --shadow-gate: 0 30px 70px -34px rgb(2 62 110 / 0.45), 0 1px 2px rgb(2 62 110 / 0.08); - --install-sheet: rgb(255 255 255 / 0.65); - --install-card: rgb(255 255 255 / 0.75); - --install-chip: rgb(255 255 255 / 0.6); - --install-border: rgb(255 255 255 / 0.85); - --shadow-card: 0 2px 4px rgb(2 62 110 / 0.1), 0 12px 28px -14px rgb(2 62 110 / 0.35); + --shadow-gate: 0 30px 70px -34px rgb(2 62 110 / 0.5), 0 1px 2px rgb(2 62 110 / 0.08); + --install-sheet: rgb(226 244 255 / 0.7); + --install-card: rgb(236 248 255 / 0.78); + --install-chip: rgb(255 255 255 / 0.62); + --install-border: rgb(255 255 255 / 0.88); + --shadow-card: 0 2px 4px rgb(2 62 110 / 0.12), 0 14px 30px -16px rgb(2 62 110 / 0.4); --card-shadow: var(--shadow-card); - --shadow-card-hover: 0 26px 60px -26px rgb(2 90 150 / 0.55), 0 3px 8px rgb(2 90 150 / 0.2); + --shadow-card-hover: 0 26px 60px -26px rgb(2 90 150 / 0.6), 0 3px 8px rgb(2 90 150 / 0.25); - /* The scene: sky, clouds, bubbles, grass. */ - --ice-base-top: #3fa3e0; - --ice-base-bottom: #d9f1ff; - --ice-blob-opacity: 0.55; - --ice-orb-opacity: 0.55; + /* Scene tokens: the water itself is painted below. */ + --ice-base-top: #2fa7e6; + --ice-base-bottom: #0a5490; + --ice-blob-opacity: 0.5; + --ice-orb-opacity: 0.6; --ice-grain-opacity: 0.02; } -/* The blobs are clouds here, not glacier gradients. */ +/* The water: sun shafts through a bright surface, deepening below. */ +.aero .ice-scene { + background: + repeating-linear-gradient( + 102deg, + rgb(255 255 255 / 0) 0px, + rgb(255 255 255 / 0.13) 46px, + rgb(255 255 255 / 0) 104px + ), + linear-gradient(180deg, #45b3ec 0%, #93daf9 26%, #d8f2ff 34%, #58b6e8 37%, #3f9ad4 66%, #2b86c4 100%); +} + +/* The blobs are caustic light patches here, not glacier gradients. */ .aero .ice-blob-a, .aero .ice-blob-b, .aero .ice-blob-c, .aero .ice-blob-d { - background: radial-gradient(circle, rgb(255 255 255 / 0.95) 0%, rgb(255 255 255 / 0.6) 42%, transparent 72%); + background: radial-gradient(circle, rgb(255 255 255 / 0.85) 0%, rgb(191 240 255 / 0.5) 45%, transparent 74%); } -/* The orbs are soap bubbles: a glassy rim with a bright specular. */ +/* The orbs are the big soap bubbles: a glassy rim with a bright specular. */ .aero .ice-orb { background: radial-gradient( - circle at 30% 25%, + circle at 32% 28%, rgb(255 255 255 / 0.95) 0%, - rgb(224 247 255 / 0.45) 20%, - rgb(125 211 252 / 0.2) 52%, - rgb(56 189 248 / 0.35) 100% + rgb(226 248 255 / 0.4) 22%, + rgb(125 211 252 / 0.14) 56%, + rgb(56 189 248 / 0.38) 100% ); - box-shadow: 0 16px 34px -18px rgb(2 90 150 / 0.4), inset 0 -8px 18px -10px rgb(2 90 150 / 0.35); + box-shadow: inset -8px -12px 20px -10px rgb(3 74 124 / 0.5), inset 8px 10px 16px -10px rgb(255 255 255 / 0.95), 0 12px 26px -16px rgb(3 74 124 / 0.55); } -/* A strip of grass along the bottom, like the wallpapers it comes from. */ +/* Reeds on the left, drawn on the scene's own pseudo-elements (the grain + layer is at 2% opacity, so anything drawn on it is invisible). */ +.aero .ice-scene::before { + content: ""; + position: absolute; + left: -6vw; + bottom: -4vh; + width: 40vw; + height: 52vh; + opacity: 0.75; + background: + linear-gradient(74deg, transparent 40%, rgb(34 150 84 / 0.85) 40% 41.6%, transparent 41.6%), + linear-gradient(84deg, transparent 58%, rgb(74 222 128 / 0.7) 58% 59.4%, transparent 59.4%), + linear-gradient(64deg, transparent 22%, rgb(20 130 74 / 0.8) 22% 23.6%, transparent 23.6%), + linear-gradient(92deg, transparent 70%, rgb(56 189 116 / 0.6) 70% 71.2%, transparent 71.2%), + linear-gradient(52deg, transparent 8%, rgb(15 110 64 / 0.7) 8% 9.8%, transparent 9.8%); + pointer-events: none; +} + +/* A city silhouette on the right: a row of blocks of different heights, + anchored to the water line and fading upward. */ .aero .ice-scene::after { content: ""; position: absolute; - inset-inline: 0; - bottom: 0; - height: 18vh; - background: linear-gradient(180deg, rgb(134 239 172 / 0) 0%, rgb(74 222 128 / 0.5) 55%, rgb(22 163 74 / 0.6) 100%); + right: 3vw; + bottom: 33vh; + width: 36vw; + height: 17vh; + opacity: 0.38; + background: + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 0% 100% / 9% 64% no-repeat, + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 12% 100% / 12% 100% no-repeat, + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 27% 100% / 8% 52% no-repeat, + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 38% 100% / 11% 78% no-repeat, + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 52% 100% / 7% 44% no-repeat, + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 62% 100% / 13% 88% no-repeat, + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 78% 100% / 9% 58% no-repeat, + linear-gradient(rgb(7 58 104 / 0.95), rgb(7 58 104 / 0.95)) 90% 100% / 10% 72% no-repeat; + mask-image: linear-gradient(180deg, rgb(0 0 0 / 0.9) 0%, rgb(0 0 0 / 0.45) 75%, transparent 100%); pointer-events: none; } @@ -296,13 +341,59 @@ .aero .gate-card { background-image: linear-gradient( 180deg, - rgb(255 255 255 / 0.7) 0%, - rgb(255 255 255 / 0.28) 46%, - rgb(255 255 255 / 0.04) 47%, - rgb(255 255 255 / 0.22) 100% + rgb(255 255 255 / 0.75) 0%, + rgb(255 255 255 / 0.3) 46%, + rgb(255 255 255 / 0.06) 47%, + rgb(255 255 255 / 0.24) 100% ); } +/* Rising bubbles. Hidden entirely in the other themes, so they cost nothing + there; under reduced motion they sit still instead of rising. */ +.ice-bubble { + display: none; +} +.aero .ice-bubble { + display: block; + position: absolute; + bottom: -14vh; + border-radius: 50%; + opacity: 0; + background: radial-gradient( + circle at 32% 28%, + rgb(255 255 255 / 0.95) 0%, + rgb(226 248 255 / 0.38) 24%, + rgb(125 211 252 / 0.12) 58%, + rgb(56 189 248 / 0.34) 100% + ); + box-shadow: inset -6px -10px 16px -8px rgb(3 74 124 / 0.45), inset 6px 8px 14px -8px rgb(255 255 255 / 0.9), 0 10px 22px -14px rgb(3 74 124 / 0.5); + animation: aero-bubble-rise var(--bubble-duration, 20s) linear infinite; + animation-delay: var(--bubble-delay, 0s); + will-change: transform, opacity; +} +.ice-paused .ice-bubble { + animation-play-state: paused; +} + +@keyframes aero-bubble-rise { + 0% { transform: translate3d(0, 0, 0) scale(0.85); opacity: 0; } + 6% { opacity: var(--bubble-opacity, 0.9); } + 50% { transform: translate3d(var(--bubble-sway, 3vw), -52vh, 0) scale(1); } + 94% { opacity: var(--bubble-opacity, 0.9); } + 100% { transform: translate3d(calc(var(--bubble-sway, 3vw) * -0.5), -104vh, 0) scale(1.06); opacity: 0; } +} + +.aero .ice-bubble-1 { width: 104px; height: 104px; left: 7%; --bubble-duration: 19s; --bubble-delay: -2s; --bubble-sway: 4vw; } +.aero .ice-bubble-2 { width: 34px; height: 34px; left: 16%; --bubble-duration: 13s; --bubble-delay: -7s; --bubble-sway: -3vw; } +.aero .ice-bubble-3 { width: 64px; height: 64px; left: 28%; --bubble-duration: 22s; --bubble-delay: -12s; --bubble-sway: 3vw; } +.aero .ice-bubble-4 { width: 20px; height: 20px; left: 38%; --bubble-duration: 11s; --bubble-delay: -4s; --bubble-sway: 2vw; } +.aero .ice-bubble-5 { width: 88px; height: 88px; left: 48%; --bubble-duration: 25s; --bubble-delay: -16s; --bubble-sway: -4vw; } +.aero .ice-bubble-6 { width: 28px; height: 28px; left: 58%; --bubble-duration: 12s; --bubble-delay: -9s; --bubble-sway: 3vw; } +.aero .ice-bubble-7 { width: 72px; height: 72px; left: 68%; --bubble-duration: 20s; --bubble-delay: -5s; --bubble-sway: -2vw; } +.aero .ice-bubble-8 { width: 46px; height: 46px; left: 78%; --bubble-duration: 16s; --bubble-delay: -13s; --bubble-sway: 4vw; } +.aero .ice-bubble-9 { width: 24px; height: 24px; left: 87%; --bubble-duration: 14s; --bubble-delay: -6s; --bubble-sway: -3vw; } +.aero .ice-bubble-10 { width: 120px; height: 120px; left: 92%; --bubble-duration: 27s; --bubble-delay: -20s; --bubble-sway: -5vw; --bubble-opacity: 0.75; } + /* Glossy aqua primary buttons and segments. */ .aero .grad-ice, .aero .btn-primary, @@ -465,6 +556,8 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) { @media (prefers-reduced-motion: reduce) { .ice-blob { animation: none; } + /* Bubbles sit still (and visible) instead of rising. */ + .aero .ice-bubble { animation: none; opacity: 0.55; transform: translate3d(0, -60vh, 0); } /* Orbs go back to the pre-rise static layout (a paused animation is not applied at all in Chrome, which would leave them parked below the fold) and give their composited layers back. */ @@ -736,6 +829,11 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) { .tabbar { background: color-mix(in oklab, var(--page) 84%, transparent); } + /* Aero's page is the water itself, so mixing it would leave a saturated + blue bar under dark labels (measured ~3.9:1). Mix the pale sheet glass. */ + .aero .tabbar { + background: color-mix(in oklab, var(--glass-sheet-bg) 84%, transparent); + } } /* Soft tinted chip for activity types (a hint of colour, not a rainbow) -- */ diff --git a/app/layout.tsx b/app/layout.tsx index 3098424..cfaba70 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -82,7 +82,7 @@ export const viewport: Viewport = { * `theme-color` meta in step with the *app* theme (the SSR metas follow the * OS preference, which is wrong once the user has picked a theme here). */ -const themeScript = `(function(){try{var DEV=${devInstanceJs};var s=localStorage.getItem('freezmo.theme');var a=s==='aero';var d=s&&!a?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark');if(a)document.documentElement.classList.add('aero');var l=localStorage.getItem('freezmo.locale.v1');if(l==='cs'||l==='en')document.documentElement.lang=l;var m=document.querySelector('meta[name="theme-color"][data-app]');if(!m){m=document.createElement('meta');m.setAttribute('name','theme-color');m.setAttribute('data-app','1');document.head.insertBefore(m,document.head.firstChild);}m.setAttribute('content',DEV?'#b45309':(a?'#3fa3e0':(d?'#0e1424':'#f4f7fd')));}catch(e){}})();`; +const themeScript = `(function(){try{var DEV=${devInstanceJs};var s=localStorage.getItem('freezmo.theme');var a=s==='aero';var d=s&&!a?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark');if(a)document.documentElement.classList.add('aero');var l=localStorage.getItem('freezmo.locale.v1');if(l==='cs'||l==='en')document.documentElement.lang=l;var m=document.querySelector('meta[name="theme-color"][data-app]');if(!m){m=document.createElement('meta');m.setAttribute('name','theme-color');m.setAttribute('data-app','1');document.head.insertBefore(m,document.head.firstChild);}m.setAttribute('content',DEV?'#b45309':(a?'#2fa7e6':(d?'#0e1424':'#f4f7fd')));}catch(e){}})();`; /** * Captures Chrome's `beforeinstallprompt` (it can fire before React hydrates) diff --git a/components/CookieConsent.tsx b/components/CookieConsent.tsx index 68892ae..a36d983 100644 --- a/components/CookieConsent.tsx +++ b/components/CookieConsent.tsx @@ -41,7 +41,9 @@ export function CookieConsent() {

{t("consent.body")}{" "} diff --git a/components/IceBackdrop.tsx b/components/IceBackdrop.tsx index 19dcca6..290b12e 100644 --- a/components/IceBackdrop.tsx +++ b/components/IceBackdrop.tsx @@ -31,6 +31,18 @@ export function IceBackdrop() { + {/* Aero-only bubbles: display:none unless the aero theme is on, so they + cost nothing in the other themes (see .ice-bubble in globals.css). */} + + + + + + + + + +

); diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx index 856ffc9..6a0fbac 100644 --- a/components/ThemeToggle.tsx +++ b/components/ThemeToggle.tsx @@ -9,7 +9,7 @@ import { MoonIcon, SunIcon } from "@/components/icons"; const STORAGE_KEY = "freezmo.theme"; -const THEME_COLORS = { light: "#f4f7fd", dark: "#0e1424", aero: "#3fa3e0" } as const; +const THEME_COLORS = { light: "#f4f7fd", dark: "#0e1424", aero: "#2fa7e6" } as const; /** The three themes. Aero is not a dark/light variant: it replaces the palette * and the scene, and the light/dark toggles hide while it is on. */ diff --git a/lib/i18n-dict.ts b/lib/i18n-dict.ts index 0ed45b1..a7d2291 100644 --- a/lib/i18n-dict.ts +++ b/lib/i18n-dict.ts @@ -1303,7 +1303,7 @@ const cs: Record = { "emptyStateHomework": "Nový úkol", /* library --------------------------------------------------------- */ - "library.title": "Tvoje mraznička", + "library.title": "Tvůj mrazák", "library.subtitle": "Sady zmrazené z tvých poznámek, uložené v tomto prohlížeči.", "library.import": "Importovat JSON", "library.moreActions": "Další akce", diff --git a/package.json b/package.json index c07de24..eac86b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freezmo", - "version": "0.1.8", + "version": "0.2.1", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/public/sw.js b/public/sw.js index 89e958c..716d61f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v54"; +const CACHE_VERSION = "freezmo-v55"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/i18n-test.mjs b/tests/i18n-test.mjs index d0926cd..f602803 100644 --- a/tests/i18n-test.mjs +++ b/tests/i18n-test.mjs @@ -29,7 +29,7 @@ const browser = await launchBrowser(); await seed(page, { locale: "cs", decks: [DECK] }); await page.goto(BASE, { waitUntil: "networkidle" }); await page.waitForTimeout(900); - check("library title is Czech", await page.getByRole("heading", { name: "Tvoje mraznička" }).isVisible()); + check("library title is Czech", await page.getByRole("heading", { name: "Tvůj mrazák" }).isVisible()); check("sidebar says Všechny sady", await page.getByRole("button", { name: /Všechny sady/ }).first().isVisible()); check("sidebar says Nezařazené", await page.getByRole("button", { name: /Nezařazené/ }).first().isVisible()); check("deck card CTA is Studovat", (await page.getByRole("link", { name: "Studovat" }).count()) === 1); diff --git a/tests/mobile-test.mjs b/tests/mobile-test.mjs index d60315a..3b28599 100644 --- a/tests/mobile-test.mjs +++ b/tests/mobile-test.mjs @@ -237,11 +237,17 @@ const AXE = axeSource(); })); check("aero: applied without the dark class", state.aero && !state.dark, JSON.stringify(state)); check( - "aero: the scene becomes a sky", + "aero: the scene becomes water", state.sky.length > 0 && state.sky !== "#f4f9ff" && state.sky !== "#070b16", state.sky, ); - check("aero: the status bar follows the sky", state.themeColor === "#3fa3e0", String(state.themeColor)); + check( + "aero: bubbles rise", + (await page.locator(".ice-bubble").count()) === 10 && + (await page.locator(".ice-bubble").first().evaluate((el) => getComputedStyle(el).animationName)) === + "aero-bubble-rise", + ); + check("aero: the status bar follows the water", state.themeColor === "#2fa7e6", String(state.themeColor)); check( "aero: the light/dark toggle is gone", (await page.getByRole("button", { name: /Switch to (light|dark) mode/ }).count()) === 0,