Frosted ice glass restyle

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.
This commit is contained in:
Martin Dvořák 2026-10-08 10:11:55 +00:00
commit 52cce42cde
20 changed files with 383 additions and 262 deletions

View file

@ -112,10 +112,11 @@ no database, no data leaving your machine except the requests you send to Ollama
list with per-folder counts, Settings/Dark mode pinned to the bottom). It **collapses** to a 68px
icon rail (remembered in `localStorage`) and becomes a slide-in **drawer** behind a hamburger on
phones; mobile also keeps the frosted header + iOS tab bar
- **One navy palette pulled from the app icon, both themes**: light page `#f4f7fd` / sidebar
`#e9eff9` / border `#dce4f1`; dark page `#0e1424` / sidebar `#0a0f1c` / surfaces `#171f33` /
border `rgba(255,255,255,.12)`. Surfaces are frosted glass (translucent + `backdrop-blur`) over
the icon's navy backdrop.
- **Frosted ice glass, both themes**: light page `#eef5fd` (pale ice) with white glass panels;
dark page `#0a1120` with `rgba(255,255,255,.06)` glass. Every large surface shares one recipe —
translucent tint, 24px `backdrop-blur` (16px on phones), a bright 1px rim, an inner top highlight
and a blue-tinted shadow — over a fixed ice scene (navy/pale base, drifting glacier blobs, glossy
ice orbs and a whisper of grain).
Text tiers are documented AA ratios (see below), and the cards fill the row with
`repeat(auto-fill, minmax(280px, 1fr))` — no stranded cards, no dead space
- Deck cards: title → muted metadata → neutral tags (difficulty carries the only colour:
@ -727,10 +728,12 @@ Things I chose while building, with the reasoning:
have no `folderId`), deleting a folder re-homes its decks instead of deleting them, and folder
names travel in exports so an imported deck lands where it came from. 13 browser checks cover
create → freeze → move → rename → delete → reload → import.
18. **The visual language is glassmorphism, with restraint.** The icon's blue is the one accent
colour driving buttons and highlights; the backdrop is a navy wash; surfaces — sidebar, cards,
chrome, sheets — are frosted and translucent. Colour appears in activity tints and semantic
feedback only — pretty, but still readable and quiet.
18. **The visual language is frosted ice glass.** Glacier cyan to ice blue (`#38bdf8` → `#0ea5e9`)
is the accent for focus rings, progress bars and selected states; the primary CTA is an icy
gradient (deep blue in light, white-to-`#bfe3ff` in dark). The background is a fixed ice scene;
the sidebar, cards, chrome, sheets and gate are translucent glass with a bright rim and a soft
blue shadow. Colour appears in activity tints and semantic feedback only — pretty, but still
readable and quiet.
19. **Custom CSS lives in `@layer components`, and glass uses Tailwind's backdrop utilities.**
Two build-level traps, both found by looking at real screenshots: unlayered rules beat Tailwind
utilities (which silently broke `fixed` on the feed), and a hand-written `backdrop-filter` gets
@ -739,21 +742,24 @@ Things I chose while building, with the reasoning:
20. **There is no demo/sample deck.** Every activity comes from the user's own notes; the empty
state points at the create flow and Settings instead of shipping fake content. Browser tests seed
a fixture deck into `localStorage` rather than relying on app code.
21. **Dark mode is token-driven, and contrast is audited.** Text colours come from
21. **Dark mode is token-driven, and contrast is audited on glass.** Text colours come from
`--text` / `--text-muted` / `--text-subtle` (never hardcoded), and a `contrast.mjs` audit checks
every pairing at WCAG AA — currently **21/21 pairs pass** (e.g. dark secondary `#b7c3dc` on
`#0e1424` = 10.4:1, white on the light blue CTA `#1d61ab` = 6.3:1). Difficulty colours were
darkened until they cleared 4.5:1 on their pills.
every pairing at WCAG AA against the *effective* frosted background — currently **18/18 pairs
pass** (e.g. dark secondary `#b3c4e0` on glass `#1c2535` = 10:1, white on the light CTA
mid-gradient `#1a5fd0` = 5.6:1). Difficulty colours were darkened until they cleared 4.5:1 on
their pills.
This was a real bug: an earlier edit silently failed, so `.dark` never overrode `--text`, leaving
near-black text on a dark page.
22. **The dashboard follows the icon's navy system.** Frosted navy/white surfaces, hairline borders,
pill CTAs, 18px radii and restrained shadows; the sidebar, cards and chrome are translucent
glass over the backdrop. Deck cards got real hierarchy: title → muted metadata → tags → summary
→ secondary Study CTA, with secondary actions behind a quiet menu.
23. **One fixed backdrop layer instead of per-page blobs.** The decorative wash used to be
22. **The dashboard is one inset glass rail plus glass cards.** The sidebar is a single tall
rounded glass panel inset from the viewport edges, so the ice scene shows around it; deck cards
are 24px-radius glass panels with a secondary Study CTA and quiet metadata. Headings are
semibold with tighter tracking. Nested glass (chips, inputs) is more opaque but never adds a
second blur layer.
23. **One fixed ice scene instead of per-page blobs.** The decorative background used to be
rendered inside the content column, so on wide screens it was clipped into a visible hard edge.
It now lives in a single `position: fixed` layer (`.app-backdrop`, `z-index: -10`) in the
icon's navy and blue, and `body`'s colour matches its bottom so mobile overscroll shows no seam.
It now lives in a single `position: fixed` layer (`.ice-scene`, `z-index: -1`, pointer-events
none): base gradient, four drifting glacier blobs, five glossy ice orbs and an SVG-noise grain.
`prefers-reduced-motion` stops the drift.
24. **The UI was deliberately made quieter.** One accent colour (`accent-*`), a desaturated
`frost-*` scale, hairline borders and flat surfaces replaced the saturated gradients and the
seven-colour activity badges. Semantic colours (green/amber/red) survive only where they carry
@ -789,10 +795,17 @@ Things I chose while building, with the reasoning:
Desktop keeps the designed 12–14px field sizes.
31. **The app icon drives the palette.** The navy rounded-square artwork in
`assets/app-icon-source.jpeg` is the source of truth: `frost-*` is navy-tinted, the accent is
the snowflake blue, the backdrop is the same navy wash, the primary CTA is the icon blue (deep
in light mode, bright in dark), and every icon/splash/favicon is rendered from it by
`scripts/make-icons.mjs`. The sidebar, header, key gate, empty state and install sheet all show
glacier cyan, the ice scene uses the artwork's blues, the primary CTA is an icy gradient, and
every icon/splash/favicon is rendered from it by `scripts/make-icons.mjs`. The sidebar, header, key gate, empty state and install sheet all show
the real artwork instead of the old monochrome snowflake tile.
32. **The ice scene was profiled, and the blur moved off the animated layer.** A mid-range phone
viewport with 4x CPU throttling showed the drifting blobs were the only expensive part: a
`filter: blur(100px)` on a huge element that is also being transformed forces a
re-rasterisation every frame (150ms vs 33ms per frame in a software renderer). The blobs are now
soft radial gradients with no filter — visually the same, 4-5x cheaper — phones get two blobs
instead of four and a 16px (instead of 24px) glass blur, and nested glass never blurs. Result:
60fps idle and ~17ms median frames while scrolling under 4x throttle.
14. **`gpt-oss:120b` does all content work — but it cannot read images** (`this model does not
support image input`), so `VISION_MODEL` has to stay a multimodal model (`gemma4:31b-cloud`).
Text-only inputs — pasted text, PDFs with a text layer, DOCX/PPTX/ODT — never touch the vision