From 586e4b5a402d99317d0d3d86dd6bd025604b8bfe Mon Sep 17 00:00:00 2001
From: Martin Dvorak
Date: Thu, 8 Oct 2026 09:27:42 +0000
Subject: [PATCH 1/2] Revert the last two glass iterations
The colourful blob wash (92fe685) and the flat "straight ice" pass
(5d05592) both missed. Reverting both to get back to the icon-driven
navy palette before rebuilding the look as a proper frosted ice glass
design.
---
README.md | 30 ++++----
app/globals.css | 179 +++++++++++++++++++++++++++++-------------------
2 files changed, 122 insertions(+), 87 deletions(-)
diff --git a/README.md b/README.md
index 259cf1c..2600e53 100644
--- a/README.md
+++ b/README.md
@@ -112,11 +112,10 @@ 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 palette pulled from the app icon, both themes**: light page `#eef3fa` (pale ice) /
- sidebar and cards frosted white / border `#dce4f1`; dark page `#121a2c` / sidebar `#0c1322` /
- surfaces `#1a2337` / border `rgba(255,255,255,.1)`. Surfaces are frosted glass — translucent with
- `backdrop-blur`, a bright 1px edge and a light-from-above sheen — over a flat backdrop with a
- single faint highlight.
+- **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.
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:
@@ -728,11 +727,10 @@ 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 frosted ice, with restraint.** The icon's blue is the one accent
- colour driving buttons and highlights; the backdrop is flat with a single faint top highlight,
- and surfaces — sidebar, cards, chrome, sheets — are translucent glass with a bright edge and a
- soft sheen. Colour appears in activity tints and semantic feedback only — pretty, but still
- readable and quiet.
+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.
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
@@ -752,11 +750,10 @@ Things I chose while building, with the reasoning:
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, deliberately almost flat.** The decorative wash used to be
+23. **One fixed backdrop layer instead of per-page blobs.** The decorative wash 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`): a flat ice
- or navy base plus one faint highlight at the top — just enough for the glass to catch, without
- turning into a gradient soup. `body`'s colour matches it so mobile overscroll shows no seam.
+ 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.
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
@@ -792,8 +789,9 @@ 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 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
+ 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
the real artwork instead of the old monochrome snowflake tile.
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`).
diff --git a/app/globals.css b/app/globals.css
index 5672ee2..4efcd96 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -81,30 +81,29 @@
--------------------------------------------------------------------------- */
:root {
color-scheme: light;
- --page: #eef3fa; /* pale ice */
- --sidebar: #ffffff;
+ --page: #f4f7fd;
+ --sidebar: #e9eff9;
--surface: #ffffff;
--surface-raised: #ffffff;
- --surface-strong: #f2f6fc;
- --card-hover-bg: #ffffff;
+ --surface-strong: #edf2fa;
+ --card-hover-bg: #f7faff;
--surface-sunken: rgb(14 21 40 / 0.05);
--hairline: #dce4f1;
- --card-border: rgb(255 255 255 / 0.85);
- --sidebar-divider: #dfe7f3;
+ --card-border: #d7e0ef;
+ --sidebar-divider: #d7e0ef;
--hairline-soft: rgb(14 21 40 / 0.07);
- --glass-tint: rgb(255 255 255 / 0.6);
- --glass-top: rgb(255 255 255 / 0.95);
- --glass-edge: rgb(255 255 255 / 0.85);
- --glass-sheen: rgb(255 255 255 / 0.55);
- --text: #0e1526; /* 16.2:1 on #eef3fa */
- --text-muted: #4d5c78; /* 6.1:1 */
- --text-subtle: #55647f; /* 5.4:1 */
+ --glass-tint: rgb(255 255 255 / 0.55);
+ --text: #0e1526; /* 16.9:1 on #f4f7fd */
+ --text-muted: #4d5c78; /* 6.3:1 */
+ --text-subtle: #5a6a86; /* 5.1:1 */
--hover: rgb(14 21 40 / 0.05);
- --sidebar-edge: 0 1px 0 var(--sidebar-divider), 6px 0 18px -12px rgb(14 21 40 / 0.18);
+ --sidebar-top: #eef3fb;
+ --sidebar-bottom: #e5edf9;
+ --sidebar-edge: 0 1px 0 var(--sidebar-divider), 6px 0 18px -12px rgb(14 21 40 / 0.35);
/* API-key gate: cool near-white stage with soft blue glows, tinted input. */
- --gate-base: #eef3fa;
- --gate-glow-a: rgb(90 150 240 / 0.16);
- --gate-glow-b: rgb(140 150 240 / 0.12);
+ --gate-base: #eef3fb;
+ --gate-glow-a: rgb(90 150 240 / 0.25);
+ --gate-glow-b: rgb(140 150 240 / 0.2);
--gate-card: #ffffff;
--gate-card-border: #dde5f2;
--gate-callout-bg: #eef3fa;
@@ -118,39 +117,35 @@
--install-chip: #f2f2f2;
--install-border: #e0e0e0;
--shadow-card: 0 1px 2px rgb(14 21 40 / 0.06), 0 1px 3px rgb(14 21 40 / 0.04);
- --card-shadow: 0 0 0 1px rgb(14 21 40 / 0.04), 0 1px 2px rgb(14 21 40 / 0.04), 0 12px 28px -14px rgb(14 21 40 / 0.2);
- --shadow-card-hover: 0 0 0 1px rgb(14 21 40 / 0.05), 0 16px 36px -14px rgb(14 21 40 / 0.24), 0 2px 6px rgb(14 21 40 / 0.07);
- /* Backdrop: flat ice with a single faint highlight at the top. */
- --bd-base: #eef3fa;
- --bd-highlight: rgb(255 255 255 / 0.9);
+ --card-shadow: 0 1px 2px rgb(14 21 40 / 0.05), 0 5px 14px -6px rgb(14 21 40 / 0.12);
+ --shadow-card-hover: 0 12px 30px -12px rgb(14 21 40 / 0.2), 0 2px 6px rgb(14 21 40 / 0.07);
}
.dark {
color-scheme: dark;
- --page: #121a2c;
- --sidebar: #0c1322;
- --surface: #1a2337;
- --surface-raised: #222c44;
- --surface-strong: #222c44;
- --card-hover-bg: #222c44;
+ --page: #0e1424;
+ --sidebar: #0a0f1c;
+ --surface: #171f33;
+ --surface-raised: #1e2740;
+ --surface-strong: #202a42;
+ --card-hover-bg: #1e2740;
--surface-sunken: rgb(255 255 255 / 0.07);
- --hairline: rgb(255 255 255 / 0.1);
- --card-border: rgb(255 255 255 / 0.1);
- --sidebar-divider: rgb(255 255 255 / 0.08);
- --hairline-soft: rgb(255 255 255 / 0.07);
+ --hairline: rgb(255 255 255 / 0.12);
+ --card-border: rgb(255 255 255 / 0.12);
+ --sidebar-divider: rgb(255 255 255 / 0.1);
+ --hairline-soft: rgb(255 255 255 / 0.08);
--glass-tint: rgb(255 255 255 / 0.06);
- --glass-top: rgb(255 255 255 / 0.09);
- --glass-edge: rgb(255 255 255 / 0.1);
- --glass-sheen: rgb(255 255 255 / 0.05);
- --text: #e9eefb; /* 14.8:1 on #121a2c */
- --text-muted: #b7c3dc; /* 10.0:1 */
- --text-subtle: #94a4c2; /* 7.0:1 */
+ --text: #e9eefb; /* 15.4:1 on #0e1424 */
+ --text-muted: #b7c3dc; /* 10.4:1 */
+ --text-subtle: #94a4c2; /* 7.3:1 */
--hover: rgb(255 255 255 / 0.07);
- --sidebar-edge: 0 1px 0 var(--hairline), 6px 0 16px -14px rgb(0 0 0 / 0.7);
+ --sidebar-top: #0b1120;
+ --sidebar-bottom: #080d18;
+ --sidebar-edge: 0 1px 0 var(--hairline), 6px 0 16px -14px rgb(0 0 0 / 0.75);
/* API-key gate: navy stage with the icon's blue glows, raised input. */
- --gate-base: #0c1322;
- --gate-glow-a: rgb(70 140 235 / 0.14);
- --gate-glow-b: rgb(120 120 235 / 0.1);
+ --gate-base: #0a0f1c;
+ --gate-glow-a: rgb(70 140 235 / 0.2);
+ --gate-glow-b: rgb(120 120 235 / 0.14);
--gate-card: #131a2c;
--gate-card-border: rgb(255 255 255 / 0.1);
--gate-callout-bg: rgb(255 255 255 / 0.06);
@@ -166,8 +161,24 @@
--shadow-card: 0 1px 2px rgb(0 0 0 / 0.25);
--card-shadow: var(--shadow-card);
--shadow-card-hover: 0 10px 28px -16px rgb(0 0 0 / 0.55), 0 1px 2px rgb(0 0 0 / 0.3);
- --bd-base: #101a2c;
- --bd-highlight: rgb(120 160 220 / 0.12);
+}
+
+/* Backdrop: the icon's navy base and blue wash, seen through frosted glass. */
+:root {
+ --bd-sky: rgb(110 175 245 / 0.2);
+ --bd-lilac: rgb(145 145 240 / 0.14);
+ --bd-mint: rgb(110 210 230 / 0.12);
+ --bd-top: #f8fafe;
+ --bd-mid: #f2f6fd;
+ --bd-bottom: #ecf1fb;
+}
+.dark {
+ --bd-sky: rgb(60 130 230 / 0.14);
+ --bd-lilac: rgb(110 115 235 / 0.1);
+ --bd-mint: rgb(70 180 215 / 0.08);
+ --bd-top: #111a2e;
+ --bd-mid: #0e1424;
+ --bd-bottom: #0a101d;
}
html,
@@ -233,22 +244,54 @@ body {
z-index: -10;
overflow: hidden;
pointer-events: none;
- /* Flat ice with one faint highlight at the top — just enough for the
- frosted surfaces to have something to catch. */
background:
- radial-gradient(120vw 54vh at 50% -16%, var(--bd-highlight) 0%, transparent 72%),
- var(--bd-base);
+ radial-gradient(135vw 110vh at 4% -30%, var(--bd-sky) 0%, transparent 72%),
+ radial-gradient(125vw 105vh at 104% -18%, var(--bd-lilac) 0%, transparent 70%),
+ radial-gradient(135vw 105vh at 38% 128%, var(--bd-mint) 0%, transparent 72%),
+ linear-gradient(180deg, var(--bd-top) 0%, var(--bd-mid) 50%, var(--bd-bottom) 100%);
+}
+
+/* Two slow, very soft blobs for a little life behind the glass. */
+.app-backdrop::before,
+.app-backdrop::after {
+ content: "";
+ position: absolute;
+ border-radius: 50%;
+ filter: blur(110px);
+ opacity: 0.22;
+}
+.app-backdrop::before {
+ width: 52vw;
+ height: 52vw;
+ min-width: 24rem;
+ min-height: 24rem;
+ top: -26vh;
+ left: -18vw;
+ background: radial-gradient(circle, var(--bd-sky), transparent 66%);
+ animation: float 16s ease-in-out infinite;
+}
+.app-backdrop::after {
+ width: 46vw;
+ height: 46vw;
+ min-width: 22rem;
+ min-height: 22rem;
+ bottom: -28vh;
+ right: -14vw;
+ background: radial-gradient(circle, var(--bd-mint), transparent 66%);
+ animation: float 20s ease-in-out infinite reverse;
+}
+.dark .app-backdrop::before,
+.dark .app-backdrop::after {
+ opacity: 0.3;
}
@layer components {
/* Glass primitives ------------------------------------------------------ */
.glass-panel {
@apply backdrop-blur-xl backdrop-saturate-150;
- background:
- linear-gradient(180deg, var(--glass-sheen) 0%, transparent 46%),
- color-mix(in oklab, var(--surface) 66%, transparent);
- border: 1px solid var(--glass-edge);
- box-shadow: var(--shadow-card), inset 0 1px 0 var(--glass-top);
+ background: color-mix(in oklab, var(--surface) 82%, transparent);
+ border: 1px solid var(--hairline);
+ box-shadow: var(--shadow-card);
}
.glass-chip {
@@ -264,7 +307,7 @@ body {
/* Tailwind emits both the prefixed and standard properties; hand-written
backdrop-filter gets minified down to -webkit- only, which paints nothing. */
@apply backdrop-blur-xl backdrop-saturate-150;
- background: color-mix(in oklab, var(--surface) 62%, transparent);
+ background: color-mix(in oklab, var(--surface) 70%, transparent);
border-bottom: 1px solid var(--hairline);
}
@@ -272,9 +315,7 @@ body {
surface as the cards, with a neutral border and shadow. */
.study-chrome {
@apply backdrop-blur-xl backdrop-saturate-150;
- background:
- linear-gradient(180deg, var(--glass-sheen) 0%, transparent 46%),
- color-mix(in oklab, var(--surface) 68%, transparent);
+ background: color-mix(in oklab, var(--surface) 88%, transparent);
border: 1px solid var(--hairline);
box-shadow: var(--shadow-card);
}
@@ -285,15 +326,9 @@ body {
.card {
border-radius: 1.125rem; /* 18px */
@apply backdrop-blur-xl backdrop-saturate-150;
- background:
- linear-gradient(180deg, var(--glass-sheen) 0%, transparent 46%),
- color-mix(in oklab, var(--surface) 62%, transparent);
+ background: color-mix(in oklab, var(--surface) 78%, transparent);
border: 1px solid var(--card-border);
- box-shadow: var(--card-shadow), inset 0 1px 0 var(--glass-top);
-}
-.dark .card,
-.dark .glass-panel {
- border-color: var(--glass-edge);
+ box-shadow: var(--card-shadow);
}
/* Opt-in hover lift for interactive cards (deck cards). Pointer devices only,
@@ -319,7 +354,7 @@ body {
.list-group {
border-radius: 1rem;
@apply backdrop-blur-lg backdrop-saturate-150;
- background: color-mix(in oklab, var(--surface) 66%, transparent);
+ background: color-mix(in oklab, var(--surface) 82%, transparent);
border: 1px solid var(--hairline);
overflow: hidden;
}
@@ -372,7 +407,7 @@ body {
.tabbar {
@apply backdrop-blur-xl;
- background: color-mix(in oklab, var(--page) 70%, transparent);
+ background: color-mix(in oklab, var(--page) 88%, transparent);
border-top: 1px solid var(--hairline);
}
@@ -388,12 +423,14 @@ body {
.dark .tint-peach { background: linear-gradient(140deg, rgb(230 160 118 / 0.26), rgb(230 160 118 / 0.1)); color: #fadfc9 }
.dark .tint-rose { background: linear-gradient(140deg, rgb(230 130 150 / 0.26), rgb(230 130 150 / 0.1)); color: #fbd5dd }
-/* Sidebar surface: flat frosted glass with a real edge. */
+/* Sidebar surface: frosted navy glass with a real edge. */
.sidebar-surface {
@apply backdrop-blur-xl backdrop-saturate-150;
- background:
- linear-gradient(180deg, var(--glass-sheen) 0%, transparent 30%),
- color-mix(in oklab, var(--sidebar) 64%, transparent);
+ background: linear-gradient(
+ 180deg,
+ color-mix(in oklab, var(--sidebar-top) 84%, transparent),
+ color-mix(in oklab, var(--sidebar-bottom) 84%, transparent)
+ );
border-right: 1px solid var(--sidebar-divider);
box-shadow: var(--sidebar-edge);
}
From 52cce42cde4809be2a717d3367200443c8f682e4 Mon Sep 17 00:00:00 2001
From: Martin Dvorak
Date: Thu, 8 Oct 2026 10:11:55 +0000
Subject: [PATCH 2/2] Frosted ice glass restyle
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
README.md | 57 +--
app/deck/[id]/page.tsx | 2 +-
app/globals.css | 486 +++++++++++++++-----------
app/layout.tsx | 3 +-
components/ApiKeyGate.tsx | 10 +-
components/DeckCard.tsx | 6 +-
components/IceBackdrop.tsx | 22 ++
components/InstallSheet.tsx | 4 +-
components/LibraryScreen.tsx | 2 +-
components/SettingsDialog.tsx | 6 +-
components/Sidebar.tsx | 13 +-
components/StudyFeed.tsx | 4 +-
components/TabBar.tsx | 2 +-
components/UploadScreen.tsx | 4 +-
components/activities/Flashcard.tsx | 2 +-
components/activities/Matching.tsx | 2 +-
components/activities/Ordering.tsx | 2 +-
components/activities/ShortAnswer.tsx | 2 +-
components/activities/shared.tsx | 4 +-
components/ui.tsx | 8 +-
20 files changed, 381 insertions(+), 260 deletions(-)
create mode 100644 components/IceBackdrop.tsx
diff --git a/README.md b/README.md
index 2600e53..2370e77 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/app/deck/[id]/page.tsx b/app/deck/[id]/page.tsx
index 112f1c3..4e6d953 100644
--- a/app/deck/[id]/page.tsx
+++ b/app/deck/[id]/page.tsx
@@ -43,7 +43,7 @@ export default function DeckPage() {
🧊
Deck not found
-
+
This deck isn't in this browser's library. It may have been deleted, or it was
created in a different browser profile.