The scene is the user's own Frutiger Aero image now: green hills, a river,
an eco-city, bubbles and daisies. Re-encoded to public/aero/background.webp
(152 kB) with a fresh 3 kB pre-blurred variant for the glass, and the
WallpaperSafari credit line, its i18n keys and the README credit are gone
(the image is made by the user with Gemini).
Contrast: 0 below-minimum findings and 0 axe violations, all three themes.
Version 0.2.8, SW -> freezmo-v60.
Frutiger Aero (Settings -> Theme): a third theme, not a dark/light variant.
A sky gradient with a strip of grass, the glacier blobs become clouds, the
orbs soap bubbles, glass gets the classic horizon gloss and the buttons a
glossy aqua. The light/dark toggles (header, rail, chat header) hide while it
is on and the picker says why; the pre-paint script applies the class before
first paint and the PWA status bar follows (#3fa3e0). Its muted/frost text is
darker than Light's because those labels sit directly on the sky.
Contrast, measured on real pixels (tests/contrast.mjs, now covering aero):
- The light settings sheet was translucent over a dark scrim, so its muted
text measured ~2.8:1. Sheets and dialogs now use --glass-sheet-bg (a more
opaque surface), and Light's --text-muted is a touch darker.
- Result: 0 below-minimum findings and 0 axe violations across every screen,
all three themes, phone and desktop.
Also: the suites now seed the cookie notice as already dismissed (they run as
a returning user); the regression suite opts out to test the notice itself —
the previous deploy failed its gate because the notice blocked a chat chip
click. The notice also hides during immersive sessions (overlay-open), so it
can never cover a study feed.
Version 0.1.8, SW -> freezmo-v54.
English stays the default; Czech is a full translation. Only the app chrome
is translated - notes and generated decks keep their own language, which is a
core feature, not a setting.
i18n
- lib/i18n-dict.ts: flat en/cs dictionaries (441 keys each), translate() and
translatePlural() with real Czech plural rules (1 sada / 2-4 sady / 5+ sad).
English is the default and the fallback for a missing key.
- lib/i18n.ts: browser store (freezmo.locale.v1), useI18n() hook, and the
x-freezmo-locale header. The hook's t/plural are bound to the locale React
is rendering with, so the first client render matches the server HTML and
then re-renders in the stored language; module-level t is only for event
handlers and non-React helpers.
- <html lang> follows the choice (inline script + setLocale).
UI
- Settings -> Language segmented control (English / Cestina), plus an EN/CS
button on the key gate for first run.
- Every screen translated: sidebar, header, tab bar, library, deck cards,
folder bar, create flow, all seven activities, study chrome, results,
settings + health panel, API-key gate and help, install sheet, offline and
the deck-not-found page.
- Plural-aware counts everywhere (decks, folders, activities, runs, photos,
documents, pages, words, wrong attempts).
Server
- API routes and guards (generate, grade, health, http-guard, rate-limit,
ollama error mapping) answer in the request's language, so error cards and
the health panel never fall back to English mid-sentence.
Fixes
- A hydration mismatch when a non-default locale was stored silently dropped
the .dark class after hydration; render-time t() is now bound to the
hook's locale (UploadScreen.describeExtraction, ResultsScreen.answerFor,
DeckCard's folder fallback).
- Bumped the service worker cache to freezmo-v4 for this deploy.
Tests: i18n 27/27 (gate toggle, persistence, html lang, Czech plurals,
settings switch) and a rebuilt regression suite 72/72 (English default,
library, create, all 7 activity types, results, settings, install sheet,
offline, PWA, security, Czech smoke).
Rebuilds the look around the spec: an ice scene, one shared glass
recipe, icy accents and glass on every large surface. Visual only —
layout, behaviour, copy and functionality are untouched.
Background
- .ice-scene: fixed, z-index -1, pointer-events none. Navy base
(#070b16 to #0c1426) in dark, pale (#f4f9ff to #e6f1fb) in light,
four drifting glacier blobs (#38bdf8/#60a5fa/#818cf8, 30-60s
transform-only loops), five glossy ice orbs (40-220px radial
gradients with a highlight at 30% 25% and a blue shadow) placed so the
glass blurs them, and an SVG feTurbulence grain at 4-5%.
- prefers-reduced-motion stops the drift; phones get two blobs and two
orbs fewer.
Glass
- One shared recipe (.glass-panel, .card, .sidebar-surface,
.study-chrome, .tabbar, .glass, .gate-card): rgba(255,255,255,.06)
dark / .55 light, blur(24px) saturate(140%) (16px on phones), a bright
1px rim, an inset top highlight and a blue-tinted outer shadow.
- Nested glass (chips, inputs, secondary buttons) is more opaque and
never adds a second blur layer. @supports fallback for no
backdrop-filter. Radii: 24-28px cards/sheets, 14px inputs/buttons,
pills for chips. Cards hover brighter + cyan rim + 2px lift, 180ms.
Colour and type
- Accent is glacier cyan (#38bdf8) to ice blue (#0ea5e9); progress bars
and selected states use the gradient; the active sidebar item is a
frosted highlight with a cyan rim.
- Primary CTA: icy gradient — white to #bfe3ff with ink text in dark,
deep blue in light (biased so the label sits on the darker half and
keeps 5.6:1).
- Leftover slate/black greys replaced with navy-tinted frost tokens.
- Headings are semibold with tighter tracking.
Screens
- Dashboard: the sidebar is one tall inset rounded glass panel; deck
cards are glass.
- Gate: one centred glass card over the scene, orbs visible behind it.
- Sheets/modals: glass over the dimmed, lightly blurred backdrop.
- Study view: question card more opaque (.glass-solid); top bar and
pager are slim glass pills.
Perf: measured on a phone viewport with 4x CPU throttling. The animated
blobs' filter: blur() was the whole cost (150ms vs 33ms per frame), so
they are soft radial gradients with no filter now; idle 60fps, ~17ms
median while scrolling.
Tests updated for the new tokens: contrast 18/18 AA, deck chrome 15/15,
key gate 49/49, install sheet 64/64, deck fixes 15/15, retry 14/14,
zoom 6/6, final ux 12/12, security 8/8, pwa 16/16.
The icon's artwork is now the source of truth for the palette:
- frost-* is a navy-tinted neutral scale and accent-* is the icon's
snowflake blue; light page #f4f7fd / sidebar #e9eff9 / border
#dce4f1, dark page #0e1424 / sidebar #0a0f1c / surfaces #171f33.
- Surfaces are frosted glass: .card, .glass-panel, .list-group and
.sidebar-surface are translucent with backdrop-blur/saturate, and the
page wrapper no longer paints an opaque --page, so the fixed navy/blue
backdrop reads through the sidebar, cards, chrome and sheets.
- The primary CTA is the icon's blue (#1d61ab in light, #59a8f7 in dark
with ink text).
- The gate stage, fields and glows are navy/blue; viewport theme-color
and the manifest background/theme colour are #0e1424.
- The old monochrome snowflake tile is gone from the brand: sidebar,
header, key gate and empty state now show the real app icon (the
install sheet already did). Decorative snowflakes stay.
- --surface-strong is finally defined (it was referenced but never
declared, so those inputs had no fill).
- Icon URLs carry ?v=2 so browser favicon caches refetch.
Updated the contrast audit (21 pairs) and the deck-chrome token
assertions to the new theme. Verified: contrast 21/21, deck chrome
15/15, key gate 49/49, install sheet 64/64, final ux 12/12, retry
14/14, zoom 6/6, security 8/8, pwa 16/16, deck fixes 15/15.
On phones the gate card (447px) is taller than the keyboard-reduced
viewport, so the browser panned the page to reveal the field and the user
could keep scrolling/dragging while typing the API key.
- Pin the gate to the visual viewport (height + offsetTop tracked from
visualViewport resize/scroll), so the browser never needs to pan the
page to reveal the focused field.
- When the keyboard is detected (visual viewport >120px shorter than
innerHeight), lock the card: overflow hidden + touch-action none, and
keep the field and Continue button in view by scrolling the wrapper
programmatically. Re-aligns on resize and when the card grows (error
message / busy state).
- Corner controls are now absolute within the pinned gate so they stay in
the visual viewport.
- Don't auto-focus the field on touch devices (no forced keyboard on
load); desktop still focuses.
Verified by shrinking the viewport to 330px: card locked (wheel/swipe
does nothing), field 182-230 and Continue 242-290 both visible, doc
scrollHeight == viewport; error state keeps Continue at 270-318.
key-gate 49/49, install sheet 64/64, final-ux 12/12, zoom 6/6.
- API-key gate is now a fixed, out-of-flow overlay (inset-0, overflow
hidden, overscroll-none) with its own internal scroller, so the document
has no scrollable height on phones. It can't be panned or bounced.
- Home/create mains are flex columns with min-h-dvh and a flex-1 content
area, and the empty state's mobile padding was trimmed, so a short/empty
library is exactly viewport height instead of overflowing by ~76px.
Long pages (the create form, decks) still scroll normally.
- html/body get overscroll-behavior: none to stop pull-to-refresh and
rubber-banding on fitting screens.
Verified with a scroll probe (gate and empty library: scrollHeight ==
viewport; create still scrolls) plus zoom 6/6, install sheet 64/64,
pwa 16/16, final-ux 12/12, retry 14/14, security 8/8, contrast 20/20.
- Disabled Continue is now the primary style at 45% opacity with
cursor: not-allowed; full opacity while validating and once text is
entered.
- Move "How do I get a key?" below the Continue button (small ? icon,
muted, underline on hover) and tighten input+button spacing.
- Shorten the subtitle to "One step to freeze your first deck." and
balance it.
- Replace the faint gradient with two real blurred blobs (top-right and
bottom-left, 120px blur, pointer-events: none, behind the card).
- Dark: page #0f0f0f, card #1a1a1a, 8% white border.
- Phone: 24px heading, 15px subtitle, wider card padding, 40px corner
buttons with safe-area-aware offsets, robust 100dvh centering.
- Modal: 480px wide with text-pretty steps, stronger tinted free-tier
callout, handle removed from the phone sheet.
- Tests: gate suite now 49 checks incl. disabled opacity, glow pseudo,
card separation and help-link placement; new state screenshots for
invalid/offline/validating in both themes.
- Add a theme toggle to the gate (sun/moon ghost button next to the help
button); it switches instantly and persists to localStorage.
- Strengthen the corner glows so the stage reads atmospheric in both themes
(bigger, slightly stronger radial gradients behind the card).
- Center the logo, heading, subtitle and footnote inside the card.
- Dark mode: replace the solid white logo tile with a subtle raised tile
(thin border, light snowflake); make the card only slightly lighter than
the near-black page with a low-opacity border and soft shadow.
- Disabled Continue keeps the primary fill at ~45% opacity with a crisp
label instead of a faded muddy one; fully solid once text is entered.
- Bigger 36px corner buttons and a visible "How do I get a key?" text link
under the input that opens the same modal.
- Shorten and balance the reassurance line.
- Help modal: step circles aligned to the first text line, emphasised
free-tier callout with a check icon, underlined links with an
external-link icon, muted medium-weight UI labels, 36px close button
aligned to the title.
- Gate test extended to 44 checks (theme toggle, text link, modal polish).
In user-key mode a browser with no stored Ollama key now gets a polished
gate instead of the app shell: one masked input with a show/hide toggle,
a single Continue button, inline validation errors, and a "?" help sheet
with the steps to a free key.
- Validates the draft key through /api/health (real 1-token probe) before
saving; invalid-key and network failures read differently.
- Key still lives only in this browser's localStorage; nothing server-side.
- Gate wraps /, /create and deck study; server-key and local-daemon modes
skip it; removing the key in Settings brings it back.
- Removes the now-redundant dashboard/create API-key banner.
- Monochrome corner glows (dark near-black / light near-white), 24px card,
48px field and button, gentle fade/slide-in, reduced-motion aware.
- Help modal: centred dialog on desktop, bottom sheet on mobile, closes via
X, Escape or backdrop.
- README and browser suites updated (34 gate checks, 15 key-flow checks,
12 shipped-default checks; ui/folders/docs/pwa/security/contrast green).