Aero uses the real wallpaper + liquid glass buttons

The wallpaper is the one from the mockup: "The S Aesthetic That You
Definitely Recognize Frutiger Aero" via WallpaperSafari
(https://wallpapersafari.com/w/IlWuz7) — the URL in the request pointed at a
different (teal abstract) wallpaper, this is the grass/sky/water one that
matches the screenshot. Re-encoded to a 237 kB WebP in public/aero/, used as
the scene in aero only (light/dark never download it), and credited in
Settings next to the theme picker.

The drawn CSS scene (rays, reeds, skyline, caustics) is off over the photo;
the ten rising bubbles stay, and the orbs became extra bubbles. Buttons and
segments are liquid glass now — translucent, glossed top half, bright rim,
backdrop blur — instead of the solid aqua fill. Helper lines that sit
straight on the wallpaper (the keyboard tip, the AI note) get a small glass
chip: dark text on grass measured 2.5:1.

Also carries: the underwater scene + bubbles work, the floating cookie
notice, the Czech title (Tvůj mrazák) and the review-suite fix that made the
last deploy fail its gate.

Contrast: 0 below-minimum findings, 0 axe violations across all three
themes. Version 0.2.2, SW -> freezmo-v56.
This commit is contained in:
Martin Dvořák 2026-10-13 06:21:43 +00:00
commit f1c4a89adc
10 changed files with 83 additions and 78 deletions

View file

@ -1399,17 +1399,17 @@ Things I chose while building, with the reasoning:
monthly-limit body, a two-device sync round-trip, and the generation guards below).
63. **Frutiger Aero is the third theme.** Settings → Theme offers Light, Dark and Frutiger Aero.
Aero is not a dark/light variant — it replaces the palette (a sky gradient with a strip of
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 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.
Aero is not a dark/light variant — it replaces the palette and the scene: the wallpaper is
*“The S Aesthetic That You Definitely Recognize Frutiger Aero”* via WallpaperSafari
(<https://wallpapersafari.com/w/IlWuz7>, re-encoded to a 237 kB WebP in `public/aero/` and
credited in Settings), with ten drawn bubbles rising over it (`.ice-bubble` — `display: none`
outside aero so they cost nothing elsewhere, still under reduced motion). Buttons and segments
are **liquid glass** (translucent, glossed top half, bright rim, backdrop blur), the cards are
pale blue glass, and the helper lines that sit straight on the wallpaper (the keyboard tip, the
AI note) get their own small glass chip — dark text on grass measured 2.5:1. The light/dark
toggles (header, rail, chat header) hide while it is on and the picker says why; the PWA status
bar follows the wallpaper's sky (#2fa7e6). 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`

View file

@ -257,27 +257,25 @@
--ice-grain-opacity: 0.02;
}
/* The water: sun shafts through a bright surface, deepening below. */
/* The scene is the wallpaper itself: Frutiger Aero 4k Wallpaper (1920x1200)
via WallpaperSafari — https://wallpapersafari.com/w/sgI2Dq — credited in
Settings and the README. Only the moving bubbles are drawn on top; the
photo already has grass, sun, water and clouds. */
.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%);
background-image: url("/aero/background.webp");
background-size: cover;
background-position: center 42%;
background-repeat: no-repeat;
}
/* 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.85) 0%, rgb(191 240 255 / 0.5) 45%, transparent 74%);
/* The drawn extras belong to the old CSS scene: off over the photo. */
.aero .ice-scene::before,
.aero .ice-scene::after,
.aero .ice-blob {
display: none;
}
/* The orbs are the big soap bubbles: a glassy rim with a bright specular. */
/* The orbs become extra bubbles over the photo. */
.aero .ice-orb {
background: radial-gradient(
circle at 32% 28%,
@ -289,48 +287,6 @@
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);
}
/* 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;
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;
}
/* The Aero gloss: the classic white top half with a horizon line. */
.aero .card,
.aero .glass-panel,
@ -394,13 +350,45 @@
.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. */
/* Helper lines that sit straight on the wallpaper (the keyboard tip, the AI
note) need their own surface in Aero: dark text on the grass measured
2.5:1. A small glass chip fixes it without hiding the wallpaper. */
.aero .hint-line {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 0.75rem;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
backdrop-filter: blur(8px) saturate(1.4);
-webkit-backdrop-filter: blur(8px) saturate(1.4);
}
/* Liquid glass buttons: translucent, glossed top half, bright rim — the
Vista/Aero button, not a solid fill. */
.aero .grad-ice,
.aero .btn-primary,
.aero .segmented > button[aria-pressed="true"],
.aero .segmented > a[aria-current="page"] {
background-image: linear-gradient(180deg, #a5e4ff 0%, #38bdf8 52%, #0ea5e9 53%, #0284c7 100%);
color: #052437;
background-image: linear-gradient(
180deg,
rgb(255 255 255 / 0.9) 0%,
rgb(226 245 255 / 0.62) 48%,
rgb(168 224 255 / 0.55) 52%,
rgb(219 242 255 / 0.7) 100%
);
border: 1px solid rgb(255 255 255 / 0.9);
color: #05243d;
box-shadow: 0 10px 24px -14px rgb(2 62 110 / 0.6), inset 0 1px 0 rgb(255 255 255 / 0.98), inset 0 -8px 14px -10px rgb(255 255 255 / 0.7);
backdrop-filter: blur(10px) saturate(1.5);
-webkit-backdrop-filter: blur(10px) saturate(1.5);
}
/* The glass itself, a touch more transparent so the wallpaper reads through
it like liquid, still opaque enough for the dark text (contrast scanned). */
.aero {
--glass-bg: rgb(222 244 255 / 0.72);
--glass-solid-bg: rgb(228 246 255 / 0.82);
--glass-sheet-bg: rgb(230 246 255 / 0.92);
}
html,

View file

@ -15,7 +15,8 @@ import { cn } from "@/lib/utils";
export function AiDisclaimer({ className }: { className?: string }) {
const { t } = useI18n();
return (
<p className={cn("text-center text-[11.5px] leading-snug text-[var(--text-subtle)]", className)}>
// hint-line: Aero gives this its own glass chip (it sits on the wallpaper).
<p className={cn("hint-line text-center text-[11.5px] leading-snug text-[var(--text-subtle)]", className)}>
{t("common.aiDisclaimer")}
</p>
);

View file

@ -38,7 +38,20 @@ export function ThemeSection() {
))}
</div>
{theme === "aero" ? (
<p className="text-[11px] leading-snug text-[var(--text-subtle)]">{t("theme.aeroNote")}</p>
<>
<p className="text-[11px] leading-snug text-[var(--text-subtle)]">{t("theme.aeroNote")}</p>
<p className="text-[11px] leading-snug text-[var(--text-subtle)]">
{t("theme.aeroCredit")}{" "}
<a
href="https://wallpapersafari.com/w/IlWuz7"
target="_blank"
rel="noreferrer"
className="underline decoration-dotted"
>
wallpapersafari.com/w/IlWuz7
</a>
</p>
</>
) : null}
</section>
);

View file

@ -225,7 +225,8 @@ export function FeedbackPanel({
export function Hint({ children }: { children: React.ReactNode }) {
const noHints = useNoHints();
if (noHints) return null;
return <p className="text-sm text-frost-600 dark:text-frost-300">{children}</p>;
// hint-line: Aero gives this its own glass chip (it sits on the wallpaper).
return <p className="hint-line text-sm text-frost-600 dark:text-frost-300">{children}</p>;
}
/**

View file

@ -1004,6 +1004,7 @@ const en: Record<string, string> = {
"settings.theme": "Theme",
"theme.aero": "Frutiger Aero",
"theme.aeroNote": "Aero has no light/dark switch — pick Light or Dark to get it back.",
"theme.aeroCredit": "Background: “The S Aesthetic That You Definitely Recognize Frutiger Aero” via WallpaperSafari.",
"settings.legal": "Legal",
"consent.title": "About cookies",
"consent.body": "Freezmo uses no cookies and no trackers — your decks, homework and settings stay in this browser.",
@ -1959,6 +1960,7 @@ const cs: Record<string, string> = {
"settings.theme": "Téma",
"theme.aero": "Frutiger Aero",
"theme.aeroNote": "Aero nemá přepínač světlý/tmavý — vyber Světlý nebo Tmavý, pokud ho chceš zpět.",
"theme.aeroCredit": "Pozadí: „The S Aesthetic That You Definitely Recognize Frutiger Aero“ přes WallpaperSafari.",
"settings.legal": "Právní informace",
"consent.title": "O cookies",
"consent.body": "Freezmo nepoužívá cookies ani sledování — sady, úkoly a nastavení zůstávají v tomto prohlížeči.",

View file

@ -1,6 +1,6 @@
{
"name": "freezmo",
"version": "0.2.1",
"version": "0.2.2",
"private": true,
"license": "GPL-3.0-only",
"scripts": {

BIN
public/aero/background.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View file

@ -9,7 +9,7 @@
*
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
*/
const CACHE_VERSION = "freezmo-v55";
const CACHE_VERSION = "freezmo-v56";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`;

View file

@ -105,7 +105,7 @@ async function makeReviewPage(locale = "en") {
check("cs: results slide after answering", await page.getByText("100%").isVisible());
await page.locator('[data-testid="review-feed"]').getByRole("button", { name: "Zpět do knihovny" }).click();
await page.waitForTimeout(900);
check("cs: back in the library", await page.getByRole("heading", { name: "Tvoje mraznička" }).isVisible());
check("cs: back in the library", await page.getByRole("heading", { name: "Tvůj mrazák" }).isVisible());
await context.close();
}