diff --git a/README.md b/README.md index 7afbaab..cd73518 100644 --- a/README.md +++ b/README.md @@ -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 + (, 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` diff --git a/app/globals.css b/app/globals.css index afa5051..a262978 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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, diff --git a/components/AiDisclaimer.tsx b/components/AiDisclaimer.tsx index 17b463e..6e05f65 100644 --- a/components/AiDisclaimer.tsx +++ b/components/AiDisclaimer.tsx @@ -15,7 +15,8 @@ import { cn } from "@/lib/utils"; export function AiDisclaimer({ className }: { className?: string }) { const { t } = useI18n(); return ( -

+ // hint-line: Aero gives this its own glass chip (it sits on the wallpaper). +

{t("common.aiDisclaimer")}

); diff --git a/components/ThemeSection.tsx b/components/ThemeSection.tsx index 1d4533e..6cfb9fa 100644 --- a/components/ThemeSection.tsx +++ b/components/ThemeSection.tsx @@ -38,7 +38,20 @@ export function ThemeSection() { ))} {theme === "aero" ? ( -

{t("theme.aeroNote")}

+ <> +

{t("theme.aeroNote")}

+

+ {t("theme.aeroCredit")}{" "} + + wallpapersafari.com/w/IlWuz7 + +

+ ) : null} ); diff --git a/components/activities/shared.tsx b/components/activities/shared.tsx index 9c520fd..8dad7c0 100644 --- a/components/activities/shared.tsx +++ b/components/activities/shared.tsx @@ -225,7 +225,8 @@ export function FeedbackPanel({ export function Hint({ children }: { children: React.ReactNode }) { const noHints = useNoHints(); if (noHints) return null; - return

{children}

; + // hint-line: Aero gives this its own glass chip (it sits on the wallpaper). + return

{children}

; } /** diff --git a/lib/i18n-dict.ts b/lib/i18n-dict.ts index a7d2291..8a472e7 100644 --- a/lib/i18n-dict.ts +++ b/lib/i18n-dict.ts @@ -1004,6 +1004,7 @@ const en: Record = { "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 = { "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.", diff --git a/package.json b/package.json index eac86b8..a4f5940 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freezmo", - "version": "0.2.1", + "version": "0.2.2", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/public/aero/background.webp b/public/aero/background.webp new file mode 100644 index 0000000..eddfa81 Binary files /dev/null and b/public/aero/background.webp differ diff --git a/public/sw.js b/public/sw.js index 716d61f..e2eb758 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-v55"; +const CACHE_VERSION = "freezmo-v56"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/review-test.mjs b/tests/review-test.mjs index a8cc54a..1f1dd12 100644 --- a/tests/review-test.mjs +++ b/tests/review-test.mjs @@ -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(); }